add camera rig, click-to-move locomotion, and click interaction
Camera: Cinemachine-based rig with an anchor, a state director, point-of- interest framing, pan input, and dialogue framing volumes. Player: click-to-move over the NavMesh, a pointer input layer, and a motion arbiter that reconciles direct stick input with navigation-driven movement. The pure math is isolated in NightclubArcadia.Locomotion.Math with an EditMode suite, so the arbitration rules are testable without a scene. Interaction: IClickInteractable plus a bridge that routes pointer clicks to it. Supporting project settings: adds the Cinemachine package, the Interactable and Ground tags the raycasts depend on, and NavMesh agent dimensions matched to the player capsule (radius 0.3, height 1.8, climb 0.25). Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1647a101742545fe8af6187e3a743858
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using Unity.Cinemachine;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace NightclubArcadia.Cinematics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Raises a reveal camera priority while the player is inside the volume.
|
||||||
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(BoxCollider))]
|
||||||
|
public sealed class CameraFramingVolume : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] CinemachineCamera revealCamera;
|
||||||
|
[SerializeField] int activePriority = 20;
|
||||||
|
[SerializeField] float minDwell = 0.5f;
|
||||||
|
|
||||||
|
CameraStateDirector stateDirector;
|
||||||
|
int defaultPriority;
|
||||||
|
int occupants;
|
||||||
|
float exitTimer;
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
var box = GetComponent<BoxCollider>();
|
||||||
|
box.isTrigger = true;
|
||||||
|
|
||||||
|
if (revealCamera != null)
|
||||||
|
{
|
||||||
|
defaultPriority = revealCamera.Priority.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
stateDirector = FindFirstObjectByType<CameraStateDirector>();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnTriggerEnter(Collider other)
|
||||||
|
{
|
||||||
|
if (!other.CompareTag("Player") || revealCamera == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
occupants++;
|
||||||
|
exitTimer = 0f;
|
||||||
|
revealCamera.Priority.Value = activePriority;
|
||||||
|
stateDirector?.RegisterReveal();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnTriggerExit(Collider other)
|
||||||
|
{
|
||||||
|
if (!other.CompareTag("Player") || revealCamera == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
occupants = Mathf.Max(0, occupants - 1);
|
||||||
|
if (occupants == 0)
|
||||||
|
{
|
||||||
|
exitTimer = minDwell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
if (occupants > 0 || revealCamera == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exitTimer > 0f)
|
||||||
|
{
|
||||||
|
exitTimer -= Time.deltaTime;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (revealCamera.Priority.Value != defaultPriority)
|
||||||
|
{
|
||||||
|
revealCamera.Priority.Value = defaultPriority;
|
||||||
|
stateDirector?.UnregisterReveal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7cccc93b78eb84019b4524d65305612a
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
using NightclubArcadia.Player;
|
||||||
|
using NightclubArcadia.Player.Math;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace NightclubArcadia.Cinematics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts mouse drag-pan into CameraRigAnchor manual offset with recenter rules.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CameraPanInput : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] Camera brainCamera;
|
||||||
|
[SerializeField] CameraRigAnchor anchor;
|
||||||
|
[SerializeField] CameraStateDirector stateDirector;
|
||||||
|
[SerializeField] PlayerPointerInput pointerInput;
|
||||||
|
[SerializeField] float panMetresPerPixel = 0.02f;
|
||||||
|
[SerializeField] float maxPanDistance = 12f;
|
||||||
|
[SerializeField] float recenterDelay = 2.5f;
|
||||||
|
[SerializeField] float recenterSmoothTime = 0.8f;
|
||||||
|
[SerializeField] float movementRecenterDelay = 0.4f;
|
||||||
|
|
||||||
|
float releaseTimer = float.MaxValue;
|
||||||
|
Vector3 recenterVelocity;
|
||||||
|
Vector3 camRight;
|
||||||
|
Vector3 camForward;
|
||||||
|
bool basisInitialized;
|
||||||
|
|
||||||
|
public bool IsManualPanActive => pointerInput != null && pointerInput.IsPanning || releaseTimer < recenterDelay;
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
if (anchor == null)
|
||||||
|
{
|
||||||
|
anchor = GetComponent<CameraRigAnchor>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateDirector == null)
|
||||||
|
{
|
||||||
|
stateDirector = GetComponent<CameraStateDirector>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (brainCamera == null)
|
||||||
|
{
|
||||||
|
brainCamera = Camera.main;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Start()
|
||||||
|
{
|
||||||
|
if (pointerInput == null)
|
||||||
|
{
|
||||||
|
var player = GameObject.FindGameObjectWithTag("Player");
|
||||||
|
if (player != null)
|
||||||
|
{
|
||||||
|
pointerInput = player.GetComponent<PlayerPointerInput>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pointerInput != null)
|
||||||
|
{
|
||||||
|
pointerInput.PanDelta += OnPanDelta;
|
||||||
|
pointerInput.PanEnded += OnPanEnded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnDestroy()
|
||||||
|
{
|
||||||
|
if (pointerInput != null)
|
||||||
|
{
|
||||||
|
pointerInput.PanDelta -= OnPanDelta;
|
||||||
|
pointerInput.PanEnded -= OnPanEnded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
if (anchor == null || stateDirector == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateDirector.AllowsManualPan && pointerInput != null && pointerInput.IsPanning)
|
||||||
|
{
|
||||||
|
releaseTimer = 0f;
|
||||||
|
}
|
||||||
|
else if (releaseTimer < recenterDelay)
|
||||||
|
{
|
||||||
|
releaseTimer += Time.deltaTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ShouldShortenRecenter())
|
||||||
|
{
|
||||||
|
releaseTimer = Mathf.Max(releaseTimer, recenterDelay - movementRecenterDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stateDirector.AllowsManualPan)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (releaseTimer >= recenterDelay && (pointerInput == null || !pointerInput.IsPanning))
|
||||||
|
{
|
||||||
|
anchor.ManualPanOffset = Vector3.SmoothDamp(
|
||||||
|
anchor.ManualPanOffset,
|
||||||
|
Vector3.zero,
|
||||||
|
ref recenterVelocity,
|
||||||
|
recenterSmoothTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
anchor.ClampManualPan(maxPanDistance);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnPanDelta(Vector2 delta)
|
||||||
|
{
|
||||||
|
if (stateDirector == null || !stateDirector.AllowsManualPan || anchor == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureCameraBasis();
|
||||||
|
anchor.Nudge(MotionInputMath.ScreenDeltaToWorldPan(delta, camRight, camForward, panMetresPerPixel));
|
||||||
|
anchor.ClampManualPan(maxPanDistance);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnPanEnded()
|
||||||
|
{
|
||||||
|
releaseTimer = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShouldShortenRecenter()
|
||||||
|
{
|
||||||
|
var player = GameObject.FindGameObjectWithTag("Player");
|
||||||
|
if (player == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var arbiter = player.GetComponent<PlayerMotionArbiter>();
|
||||||
|
return arbiter != null && arbiter.ActiveSource != MotionSource.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnsureCameraBasis()
|
||||||
|
{
|
||||||
|
if (brainCamera == null)
|
||||||
|
{
|
||||||
|
brainCamera = Camera.main;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (brainCamera == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!basisInitialized)
|
||||||
|
{
|
||||||
|
camRight = brainCamera.transform.right;
|
||||||
|
camForward = brainCamera.transform.forward;
|
||||||
|
basisInitialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f9b06ade88f2643a3b9f52a80e06081d
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace NightclubArcadia.Cinematics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Marker on NPCs and props that the POI director may bias toward.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CameraPointOfInterest : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] float weight = 1f;
|
||||||
|
[SerializeField] float triggerRadius = 8f;
|
||||||
|
[SerializeField] float maxBias = 2.5f;
|
||||||
|
[SerializeField] float extraDistance = 1.5f;
|
||||||
|
|
||||||
|
public float Weight => weight;
|
||||||
|
public float TriggerRadius => triggerRadius;
|
||||||
|
public float MaxBias => maxBias;
|
||||||
|
public float ExtraDistance => extraDistance;
|
||||||
|
public Transform Target => transform;
|
||||||
|
|
||||||
|
void OnEnable()
|
||||||
|
{
|
||||||
|
CameraPointOfInterestDirector.Register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnDisable()
|
||||||
|
{
|
||||||
|
CameraPointOfInterestDirector.Unregister(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7862f80331f384d28b27600075971a77
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace NightclubArcadia.Cinematics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Scores registered POIs and writes bias onto the camera anchor.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CameraPointOfInterestDirector : MonoBehaviour
|
||||||
|
{
|
||||||
|
static readonly List<CameraPointOfInterest> Registered = new List<CameraPointOfInterest>();
|
||||||
|
|
||||||
|
[SerializeField] CameraRigAnchor anchor;
|
||||||
|
[SerializeField] CameraStateDirector stateDirector;
|
||||||
|
[SerializeField] float poiSmoothTime = 1f;
|
||||||
|
[SerializeField] float poiReleaseTime = 1.4f;
|
||||||
|
[SerializeField] float maxHeadingAngle = 100f;
|
||||||
|
|
||||||
|
Vector3 poiVelocity;
|
||||||
|
float distanceVelocity;
|
||||||
|
CameraPointOfInterest activePoi;
|
||||||
|
|
||||||
|
public bool HasActivePoi => activePoi != null;
|
||||||
|
|
||||||
|
public static void Register(CameraPointOfInterest poi)
|
||||||
|
{
|
||||||
|
if (poi != null && !Registered.Contains(poi))
|
||||||
|
{
|
||||||
|
Registered.Add(poi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Unregister(CameraPointOfInterest poi)
|
||||||
|
{
|
||||||
|
Registered.Remove(poi);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
if (anchor == null)
|
||||||
|
{
|
||||||
|
anchor = GetComponent<CameraRigAnchor>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateDirector == null)
|
||||||
|
{
|
||||||
|
stateDirector = GetComponent<CameraStateDirector>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LateUpdate()
|
||||||
|
{
|
||||||
|
if (anchor == null || stateDirector == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var follow = anchor.FollowTarget;
|
||||||
|
if (follow == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stateDirector.AllowsPointOfInterest)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateDirector.Current == CameraState.ManualPan)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activePoi = FindBestPoi(follow);
|
||||||
|
if (activePoi == null)
|
||||||
|
{
|
||||||
|
ReleasePoi();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var toPoi = activePoi.Target.position - follow.position;
|
||||||
|
toPoi.y = 0f;
|
||||||
|
var targetBias = Vector3.ClampMagnitude(toPoi, activePoi.MaxBias);
|
||||||
|
|
||||||
|
anchor.PoiBias = Vector3.SmoothDamp(
|
||||||
|
anchor.PoiBias,
|
||||||
|
targetBias,
|
||||||
|
ref poiVelocity,
|
||||||
|
poiSmoothTime);
|
||||||
|
|
||||||
|
anchor.DistanceBias = Mathf.SmoothDamp(
|
||||||
|
anchor.DistanceBias,
|
||||||
|
activePoi.ExtraDistance,
|
||||||
|
ref distanceVelocity,
|
||||||
|
poiSmoothTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
CameraPointOfInterest FindBestPoi(Transform follow)
|
||||||
|
{
|
||||||
|
CameraPointOfInterest best = null;
|
||||||
|
var bestScore = 0f;
|
||||||
|
var forward = follow.forward;
|
||||||
|
|
||||||
|
foreach (var poi in Registered)
|
||||||
|
{
|
||||||
|
if (poi == null || !poi.isActiveAndEnabled)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = poi.Target.position - follow.position;
|
||||||
|
offset.y = 0f;
|
||||||
|
var distance = offset.magnitude;
|
||||||
|
if (distance > poi.TriggerRadius || distance <= 0.01f)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var angle = Vector3.Angle(forward, offset);
|
||||||
|
if (angle > maxHeadingAngle)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var score = poi.Weight * (1f - distance / poi.TriggerRadius);
|
||||||
|
if (score > bestScore)
|
||||||
|
{
|
||||||
|
bestScore = score;
|
||||||
|
best = poi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReleasePoi()
|
||||||
|
{
|
||||||
|
if (anchor == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
anchor.PoiBias = Vector3.SmoothDamp(
|
||||||
|
anchor.PoiBias,
|
||||||
|
Vector3.zero,
|
||||||
|
ref poiVelocity,
|
||||||
|
poiReleaseTime);
|
||||||
|
|
||||||
|
anchor.DistanceBias = Mathf.SmoothDamp(
|
||||||
|
anchor.DistanceBias,
|
||||||
|
0f,
|
||||||
|
ref distanceVelocity,
|
||||||
|
poiReleaseTime);
|
||||||
|
|
||||||
|
if (anchor.PoiBias.sqrMagnitude < 0.0001f && Mathf.Abs(anchor.DistanceBias) < 0.01f)
|
||||||
|
{
|
||||||
|
activePoi = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e3343417e1de440f095b73e55c6977b6
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
using Unity.Cinemachine;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace NightclubArcadia.Cinematics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Proxy transform the follow camera tracks. Combines player follow,
|
||||||
|
/// POI bias, manual pan offset, and distance bias.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CameraRigAnchor : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] Transform followTarget;
|
||||||
|
[SerializeField] CinemachinePositionComposer composer;
|
||||||
|
[SerializeField] float followSmoothTime = 0.12f;
|
||||||
|
[SerializeField] float distanceSmoothTime = 0.8f;
|
||||||
|
|
||||||
|
float defaultCameraDistance = 10f;
|
||||||
|
float distanceVelocity;
|
||||||
|
Vector3 followVelocity;
|
||||||
|
|
||||||
|
public Vector3 PoiBias { get; set; }
|
||||||
|
public Vector3 ManualPanOffset { get; set; }
|
||||||
|
public float DistanceBias { get; set; }
|
||||||
|
|
||||||
|
public Transform FollowTarget => followTarget;
|
||||||
|
|
||||||
|
void Start()
|
||||||
|
{
|
||||||
|
if (composer == null)
|
||||||
|
{
|
||||||
|
composer = FindFirstObjectByType<CinemachinePositionComposer>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (composer != null)
|
||||||
|
{
|
||||||
|
defaultCameraDistance = composer.CameraDistance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LateUpdate()
|
||||||
|
{
|
||||||
|
if (followTarget == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetPosition = followTarget.position + PoiBias + ManualPanOffset;
|
||||||
|
transform.position = Vector3.SmoothDamp(
|
||||||
|
transform.position,
|
||||||
|
targetPosition,
|
||||||
|
ref followVelocity,
|
||||||
|
followSmoothTime);
|
||||||
|
|
||||||
|
if (composer != null)
|
||||||
|
{
|
||||||
|
var targetDistance = defaultCameraDistance + DistanceBias;
|
||||||
|
composer.CameraDistance = Mathf.SmoothDamp(
|
||||||
|
composer.CameraDistance,
|
||||||
|
targetDistance,
|
||||||
|
ref distanceVelocity,
|
||||||
|
distanceSmoothTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Nudge(Vector3 worldDelta)
|
||||||
|
{
|
||||||
|
ManualPanOffset += worldDelta;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClampManualPan(float maxDistance)
|
||||||
|
{
|
||||||
|
if (followTarget == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = ManualPanOffset;
|
||||||
|
offset.y = 0f;
|
||||||
|
if (offset.magnitude > maxDistance)
|
||||||
|
{
|
||||||
|
ManualPanOffset = offset.normalized * maxDistance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 16d6717a3703a4bb3a8c3bb0bb0392de
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
using NightclubArcadia.UI;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace NightclubArcadia.Cinematics
|
||||||
|
{
|
||||||
|
public enum CameraState
|
||||||
|
{
|
||||||
|
Follow,
|
||||||
|
ManualPan,
|
||||||
|
PointOfInterest,
|
||||||
|
Reveal,
|
||||||
|
Dialogue,
|
||||||
|
Menu
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves which camera subsystem may write each frame.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CameraStateDirector : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] CameraRigAnchor anchor;
|
||||||
|
[SerializeField] float dialogueRecenterTime = 0.5f;
|
||||||
|
|
||||||
|
int activeRevealCount;
|
||||||
|
float dialogueRecenterTimer;
|
||||||
|
bool wasDialogueActive;
|
||||||
|
bool wasMenuOpen;
|
||||||
|
|
||||||
|
public CameraState Current { get; private set; } = CameraState.Follow;
|
||||||
|
|
||||||
|
public bool AllowsManualPan =>
|
||||||
|
Current != CameraState.Dialogue
|
||||||
|
&& Current != CameraState.Menu
|
||||||
|
&& Current != CameraState.Reveal;
|
||||||
|
|
||||||
|
public bool AllowsPointOfInterest =>
|
||||||
|
Current == CameraState.Follow || Current == CameraState.PointOfInterest;
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
if (anchor == null)
|
||||||
|
{
|
||||||
|
anchor = GetComponent<CameraRigAnchor>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Start()
|
||||||
|
{
|
||||||
|
if (UIStateController.Instance != null)
|
||||||
|
{
|
||||||
|
UIStateController.Instance.StateChanged += OnUiStateChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResolveState(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnDestroy()
|
||||||
|
{
|
||||||
|
if (UIStateController.Instance != null)
|
||||||
|
{
|
||||||
|
UIStateController.Instance.StateChanged -= OnUiStateChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
ResolveState(true);
|
||||||
|
|
||||||
|
if (Current == CameraState.Dialogue && anchor != null)
|
||||||
|
{
|
||||||
|
dialogueRecenterTimer += Time.deltaTime;
|
||||||
|
var t = Mathf.Clamp01(dialogueRecenterTimer / dialogueRecenterTime);
|
||||||
|
anchor.ManualPanOffset = Vector3.Lerp(anchor.ManualPanOffset, Vector3.zero, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnUiStateChanged() => ResolveState(true);
|
||||||
|
|
||||||
|
public void RegisterReveal() => activeRevealCount++;
|
||||||
|
|
||||||
|
public void UnregisterReveal() => activeRevealCount = Mathf.Max(0, activeRevealCount - 1);
|
||||||
|
|
||||||
|
void ResolveState(bool allowManualPanPromotion)
|
||||||
|
{
|
||||||
|
var ui = UIStateController.Instance;
|
||||||
|
var menuOpen = ui != null && ui.IsMenuOpen;
|
||||||
|
var dialogueActive = ui != null && ui.IsDialogueActive;
|
||||||
|
|
||||||
|
if (menuOpen)
|
||||||
|
{
|
||||||
|
Current = CameraState.Menu;
|
||||||
|
}
|
||||||
|
else if (dialogueActive)
|
||||||
|
{
|
||||||
|
if (!wasDialogueActive)
|
||||||
|
{
|
||||||
|
dialogueRecenterTimer = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
Current = CameraState.Dialogue;
|
||||||
|
}
|
||||||
|
else if (activeRevealCount > 0)
|
||||||
|
{
|
||||||
|
Current = CameraState.Reveal;
|
||||||
|
}
|
||||||
|
else if (allowManualPanPromotion && IsManualPanActive())
|
||||||
|
{
|
||||||
|
Current = CameraState.ManualPan;
|
||||||
|
}
|
||||||
|
else if (HasActivePointOfInterest())
|
||||||
|
{
|
||||||
|
Current = CameraState.PointOfInterest;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Current = CameraState.Follow;
|
||||||
|
}
|
||||||
|
|
||||||
|
wasDialogueActive = dialogueActive;
|
||||||
|
wasMenuOpen = menuOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsManualPanActive()
|
||||||
|
{
|
||||||
|
var pan = GetComponent<CameraPanInput>();
|
||||||
|
return pan != null && pan.IsManualPanActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasActivePointOfInterest()
|
||||||
|
{
|
||||||
|
var director = GetComponent<CameraPointOfInterestDirector>();
|
||||||
|
return director != null && director.HasActivePoi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ca0160e4d996040238dab18725a2e844
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using NightclubArcadia.UI;
|
||||||
|
using Unity.Cinemachine;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace NightclubArcadia.Cinematics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Shifts CinemachinePositionComposer.Composition.ScreenPosition so the
|
||||||
|
/// player is centred in the screen space left of the dialogue panel.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DialogueCameraFraming : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] CinemachinePositionComposer composer;
|
||||||
|
[SerializeField] float dialogueScreenX = -0.17f;
|
||||||
|
[SerializeField] bool suppressDeadZoneXInDialogue = true;
|
||||||
|
|
||||||
|
float defaultDeadZoneX = 0.2f;
|
||||||
|
|
||||||
|
void Start()
|
||||||
|
{
|
||||||
|
if (composer == null)
|
||||||
|
{
|
||||||
|
composer = FindFirstObjectByType<CinemachinePositionComposer>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (composer != null)
|
||||||
|
{
|
||||||
|
defaultDeadZoneX = composer.Composition.DeadZone.Size.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UIStateController.Instance != null)
|
||||||
|
{
|
||||||
|
UIStateController.Instance.StateChanged += OnStateChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyFraming();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnDestroy()
|
||||||
|
{
|
||||||
|
if (UIStateController.Instance != null)
|
||||||
|
{
|
||||||
|
UIStateController.Instance.StateChanged -= OnStateChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnStateChanged() => ApplyFraming();
|
||||||
|
|
||||||
|
void ApplyFraming()
|
||||||
|
{
|
||||||
|
if (composer == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var active = UIStateController.Instance != null && UIStateController.Instance.IsDialogueActive;
|
||||||
|
var target = active ? dialogueScreenX : 0f;
|
||||||
|
|
||||||
|
var composition = composer.Composition;
|
||||||
|
composition.ScreenPosition = new Vector2(target, composition.ScreenPosition.y);
|
||||||
|
|
||||||
|
if (suppressDeadZoneXInDialogue)
|
||||||
|
{
|
||||||
|
var size = composition.DeadZone.Size;
|
||||||
|
size.x = active ? 0f : defaultDeadZoneX;
|
||||||
|
composition.DeadZone.Size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
composer.Composition = composition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 56142a5f8795401498df8e01dc74b258
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 97642050992484049b6c021481f2baf2
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace NightclubArcadia.Interaction
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adapts NPCStandIn to the click-to-interact pipeline without modifying
|
||||||
|
/// the existing proximity + keypress path.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ClickInteractableBridge : MonoBehaviour, IClickInteractable
|
||||||
|
{
|
||||||
|
[SerializeField] NPCStandIn npc;
|
||||||
|
|
||||||
|
public Transform ApproachPoint => transform;
|
||||||
|
public float ApproachRadius => npc != null ? Mathf.Max(0.15f, npc.interactionRange - 0.5f) : 0.15f;
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
if (npc == null)
|
||||||
|
{
|
||||||
|
npc = GetComponent<NPCStandIn>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnApproached()
|
||||||
|
{
|
||||||
|
npc?.Interact();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5257f07d188de44eb9a04ba4d3a65fec
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace NightclubArcadia.Interaction
|
||||||
|
{
|
||||||
|
public interface IClickInteractable
|
||||||
|
{
|
||||||
|
Transform ApproachPoint { get; }
|
||||||
|
float ApproachRadius { get; }
|
||||||
|
void OnApproached();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4812c7fb9cf2d41c28966640827dd0c8
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 381786de30b3e49e7a693028cc2127f5
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
using System;
|
||||||
|
using NightclubArcadia.Interaction;
|
||||||
|
using NightclubArcadia.Player.Math;
|
||||||
|
using StarterAssets;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.AI;
|
||||||
|
|
||||||
|
namespace NightclubArcadia.Player
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// NavMeshAgent in oracle mode: computes desired move input for the arbiter
|
||||||
|
/// without moving the CharacterController directly.
|
||||||
|
/// </summary>
|
||||||
|
[DefaultExecutionOrder(-60)]
|
||||||
|
[RequireComponent(typeof(NavMeshAgent))]
|
||||||
|
public sealed class ClickToMoveController : MonoBehaviour
|
||||||
|
{
|
||||||
|
const float SampleRadius = 1.5f;
|
||||||
|
const float StallWindow = 0.4f;
|
||||||
|
const float StallDistance = 0.05f;
|
||||||
|
|
||||||
|
[SerializeField] Camera brainCamera;
|
||||||
|
|
||||||
|
NavMeshAgent agent;
|
||||||
|
ThirdPersonController locomotion;
|
||||||
|
IClickInteractable pendingInteractable;
|
||||||
|
|
||||||
|
bool forceRunForPath;
|
||||||
|
[SerializeField] float runDistanceThreshold = 6f;
|
||||||
|
Vector3 lastPosition;
|
||||||
|
float stallTimer;
|
||||||
|
bool hasPath;
|
||||||
|
|
||||||
|
public bool HasPath => hasPath;
|
||||||
|
public Vector3 Destination { get; private set; }
|
||||||
|
public Vector2 DesiredMoveInput { get; private set; }
|
||||||
|
public float RemainingDistance => agent != null ? agent.remainingDistance : 0f;
|
||||||
|
public bool WantsSprint => hasPath && (forceRunForPath || agent.remainingDistance > runDistanceThreshold);
|
||||||
|
|
||||||
|
public event Action<bool> PathFinished;
|
||||||
|
public event Action RunRequested;
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
agent = GetComponent<NavMeshAgent>();
|
||||||
|
locomotion = GetComponent<ThirdPersonController>();
|
||||||
|
|
||||||
|
agent.updatePosition = false;
|
||||||
|
agent.updateRotation = false;
|
||||||
|
agent.updateUpAxis = false;
|
||||||
|
agent.angularSpeed = 0f;
|
||||||
|
agent.acceleration = 1000f;
|
||||||
|
agent.autoBraking = true;
|
||||||
|
agent.stoppingDistance = 0.15f;
|
||||||
|
agent.radius = 0.3f;
|
||||||
|
agent.height = 1.8f;
|
||||||
|
|
||||||
|
if (brainCamera == null)
|
||||||
|
{
|
||||||
|
brainCamera = Camera.main;
|
||||||
|
}
|
||||||
|
|
||||||
|
SyncAgentSpeed(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
if (!hasPath)
|
||||||
|
{
|
||||||
|
DesiredMoveInput = Vector2.zero;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SyncAgentSpeed(forceRunForPath || agent.remainingDistance > runDistanceThreshold);
|
||||||
|
|
||||||
|
if (brainCamera == null)
|
||||||
|
{
|
||||||
|
brainCamera = Camera.main;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cameraYaw = brainCamera != null ? brainCamera.transform.eulerAngles.y : 0f;
|
||||||
|
DesiredMoveInput = MotionInputMath.WorldToCameraRelativeInput(
|
||||||
|
agent.desiredVelocity,
|
||||||
|
agent.speed,
|
||||||
|
cameraYaw);
|
||||||
|
|
||||||
|
CheckStall();
|
||||||
|
CheckArrival();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LateUpdate()
|
||||||
|
{
|
||||||
|
if (agent != null)
|
||||||
|
{
|
||||||
|
agent.nextPosition = transform.position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RequestRun()
|
||||||
|
{
|
||||||
|
forceRunForPath = true;
|
||||||
|
RunRequested?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SetDestination(Vector3 worldPoint)
|
||||||
|
{
|
||||||
|
pendingInteractable = null;
|
||||||
|
|
||||||
|
if (!TrySampleNavMesh(worldPoint, out var hit))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.stoppingDistance = 0.15f;
|
||||||
|
agent.isStopped = false;
|
||||||
|
agent.SetDestination(hit.position);
|
||||||
|
Destination = hit.position;
|
||||||
|
hasPath = true;
|
||||||
|
ResetStallTracking();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SetInteractionTarget(IClickInteractable interactable)
|
||||||
|
{
|
||||||
|
if (interactable == null || interactable.ApproachPoint == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingInteractable = interactable;
|
||||||
|
var approach = interactable.ApproachPoint.position;
|
||||||
|
|
||||||
|
if (!TrySampleNavMesh(approach, out var hit))
|
||||||
|
{
|
||||||
|
pendingInteractable = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.stoppingDistance = Mathf.Max(0.15f, interactable.ApproachRadius);
|
||||||
|
agent.isStopped = false;
|
||||||
|
agent.SetDestination(hit.position);
|
||||||
|
Destination = hit.position;
|
||||||
|
hasPath = true;
|
||||||
|
ResetStallTracking();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cancel()
|
||||||
|
{
|
||||||
|
if (agent != null)
|
||||||
|
{
|
||||||
|
agent.ResetPath();
|
||||||
|
agent.isStopped = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPath = false;
|
||||||
|
forceRunForPath = false;
|
||||||
|
pendingInteractable = null;
|
||||||
|
DesiredMoveInput = Vector2.zero;
|
||||||
|
ResetStallTracking();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TrySampleNavMesh(Vector3 point, out NavMeshHit hit)
|
||||||
|
{
|
||||||
|
return NavMesh.SamplePosition(point, out hit, SampleRadius, NavMesh.AllAreas);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CheckArrival()
|
||||||
|
{
|
||||||
|
if (!hasPath || agent.pathPending)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agent.remainingDistance > agent.stoppingDistance)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var success = agent.pathStatus != NavMeshPathStatus.PathInvalid;
|
||||||
|
hasPath = false;
|
||||||
|
agent.ResetPath();
|
||||||
|
|
||||||
|
var interactable = pendingInteractable;
|
||||||
|
pendingInteractable = null;
|
||||||
|
|
||||||
|
PathFinished?.Invoke(success);
|
||||||
|
|
||||||
|
if (success && interactable != null)
|
||||||
|
{
|
||||||
|
interactable.OnApproached();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CheckStall()
|
||||||
|
{
|
||||||
|
var displacement = Vector3.Distance(
|
||||||
|
new Vector3(transform.position.x, 0f, transform.position.z),
|
||||||
|
new Vector3(lastPosition.x, 0f, lastPosition.z));
|
||||||
|
|
||||||
|
if (displacement < StallDistance)
|
||||||
|
{
|
||||||
|
stallTimer += Time.deltaTime;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
stallTimer = 0f;
|
||||||
|
lastPosition = transform.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stallTimer >= StallWindow && agent.remainingDistance > agent.stoppingDistance)
|
||||||
|
{
|
||||||
|
Cancel();
|
||||||
|
PathFinished?.Invoke(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResetStallTracking()
|
||||||
|
{
|
||||||
|
lastPosition = transform.position;
|
||||||
|
stallTimer = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SyncAgentSpeed(bool sprint)
|
||||||
|
{
|
||||||
|
if (locomotion == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.speed = sprint ? locomotion.SprintSpeed : locomotion.MoveSpeed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c2762b30721644611b9672bd4781cf15
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c32faf711a4044ed88ff1be2790915f7
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace NightclubArcadia.Player.Math
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pure math helpers shared by click-to-move and camera pan.
|
||||||
|
/// </summary>
|
||||||
|
public static class MotionInputMath
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a NavMeshAgent desired velocity into the camera-relative
|
||||||
|
/// Vector2 that ThirdPersonController expects on StarterAssetsInputs.move.
|
||||||
|
/// </summary>
|
||||||
|
public static Vector2 WorldToCameraRelativeInput(Vector3 worldVelocity, float agentSpeed, float cameraYaw)
|
||||||
|
{
|
||||||
|
if (worldVelocity.sqrMagnitude < 0.0001f || agentSpeed <= 0f)
|
||||||
|
{
|
||||||
|
return Vector2.zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
var horizontal = new Vector3(worldVelocity.x, 0f, worldVelocity.z);
|
||||||
|
var magnitude = horizontal.magnitude;
|
||||||
|
var fraction = Mathf.Clamp01(magnitude / agentSpeed);
|
||||||
|
var local = Quaternion.Euler(0f, -cameraYaw, 0f) * horizontal.normalized;
|
||||||
|
return new Vector2(local.x, local.z) * fraction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts screen-space pointer delta into a world-plane pan offset.
|
||||||
|
/// Drag direction is inverted so the world follows the cursor.
|
||||||
|
/// </summary>
|
||||||
|
public static Vector3 ScreenDeltaToWorldPan(
|
||||||
|
Vector2 pixelDelta,
|
||||||
|
Vector3 camRight,
|
||||||
|
Vector3 camForward,
|
||||||
|
float metresPerPixel)
|
||||||
|
{
|
||||||
|
var forward = Vector3.ProjectOnPlane(camForward, Vector3.up).normalized;
|
||||||
|
return (-pixelDelta.x * camRight + -pixelDelta.y * forward) * metresPerPixel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9af00d68e15e24c02811500cdf0dd2d4
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "NightclubArcadia.Locomotion.Math",
|
||||||
|
"rootNamespace": "NightclubArcadia.Player.Math",
|
||||||
|
"references": [],
|
||||||
|
"includePlatforms": [],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"overrideReferences": false,
|
||||||
|
"precompiledReferences": [],
|
||||||
|
"autoReferenced": true,
|
||||||
|
"defineConstraints": [],
|
||||||
|
"versionDefines": [],
|
||||||
|
"noEngineReferences": false
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 12491a5edc87346d28ffc48f9ab6562c
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
using NightclubArcadia.Interaction;
|
||||||
|
using NightclubArcadia.UI;
|
||||||
|
using StarterAssets;
|
||||||
|
using UnityEngine;
|
||||||
|
#if ENABLE_INPUT_SYSTEM
|
||||||
|
using UnityEngine.InputSystem;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace NightclubArcadia.Player
|
||||||
|
{
|
||||||
|
public enum MotionSource
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Direct,
|
||||||
|
Navigation
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sole writer of StarterAssetsInputs move/sprint. Picks one input source
|
||||||
|
/// per frame: direct keyboard/gamepad or click-to-move navigation.
|
||||||
|
/// </summary>
|
||||||
|
[DefaultExecutionOrder(-50)]
|
||||||
|
public sealed class PlayerMotionArbiter : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] float directInputCancelThreshold = 0.15f;
|
||||||
|
[SerializeField] float runDistanceThreshold = 6f;
|
||||||
|
|
||||||
|
StarterAssetsInputs playerInputs;
|
||||||
|
PlayerControlLock controlLock;
|
||||||
|
ClickToMoveController clickToMove;
|
||||||
|
#if ENABLE_INPUT_SYSTEM
|
||||||
|
PlayerInput playerInput;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public MotionSource ActiveSource { get; private set; }
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
playerInputs = GetComponent<StarterAssetsInputs>();
|
||||||
|
controlLock = GetComponent<PlayerControlLock>();
|
||||||
|
clickToMove = GetComponent<ClickToMoveController>();
|
||||||
|
#if ENABLE_INPUT_SYSTEM
|
||||||
|
playerInput = GetComponent<PlayerInput>();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetState()
|
||||||
|
{
|
||||||
|
ActiveSource = MotionSource.None;
|
||||||
|
clickToMove?.Cancel();
|
||||||
|
WriteInput(Vector2.zero, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
if (controlLock != null && controlLock.IsLocked)
|
||||||
|
{
|
||||||
|
clickToMove?.Cancel();
|
||||||
|
ActiveSource = MotionSource.None;
|
||||||
|
WriteInput(Vector2.zero, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if ENABLE_INPUT_SYSTEM
|
||||||
|
var raw = playerInput.actions["Move"].ReadValue<Vector2>();
|
||||||
|
#else
|
||||||
|
var raw = Vector2.zero;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (raw.sqrMagnitude > directInputCancelThreshold * directInputCancelThreshold)
|
||||||
|
{
|
||||||
|
clickToMove?.Cancel();
|
||||||
|
ActiveSource = MotionSource.Direct;
|
||||||
|
WriteInput(raw, ReadDirectSprint());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clickToMove != null && clickToMove.HasPath)
|
||||||
|
{
|
||||||
|
ActiveSource = MotionSource.Navigation;
|
||||||
|
WriteInput(clickToMove.DesiredMoveInput, ShouldNavigationSprint());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveSource = MotionSource.None;
|
||||||
|
WriteInput(Vector2.zero, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReadDirectSprint()
|
||||||
|
{
|
||||||
|
#if ENABLE_INPUT_SYSTEM
|
||||||
|
return playerInput.actions["Sprint"].IsPressed();
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShouldNavigationSprint()
|
||||||
|
{
|
||||||
|
return clickToMove != null && clickToMove.WantsSprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteInput(Vector2 move, bool sprint)
|
||||||
|
{
|
||||||
|
if (playerInputs == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
playerInputs.MoveInput(move);
|
||||||
|
playerInputs.SprintInput(sprint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 91bf24256c6694185b4247a736593727
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
#if ENABLE_INPUT_SYSTEM
|
||||||
|
using UnityEngine.InputSystem;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace NightclubArcadia.Player
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Receives pointer and camera-pan SendMessages from PlayerInput and
|
||||||
|
/// exposes them as C# events for navigation and camera systems.
|
||||||
|
/// </summary>
|
||||||
|
[DefaultExecutionOrder(-100)]
|
||||||
|
public sealed class PlayerPointerInput : MonoBehaviour
|
||||||
|
{
|
||||||
|
public Vector2 ScreenPosition { get; private set; }
|
||||||
|
public bool IsPanning { get; private set; }
|
||||||
|
|
||||||
|
public event Action Clicked;
|
||||||
|
public event Action PanStarted;
|
||||||
|
public event Action<Vector2> PanDelta;
|
||||||
|
public event Action PanEnded;
|
||||||
|
|
||||||
|
#if ENABLE_INPUT_SYSTEM
|
||||||
|
void OnPoint(InputValue value)
|
||||||
|
{
|
||||||
|
ScreenPosition = value.Get<Vector2>();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnClick(InputValue value)
|
||||||
|
{
|
||||||
|
if (value.isPressed)
|
||||||
|
{
|
||||||
|
Clicked?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnCameraPan(InputValue value)
|
||||||
|
{
|
||||||
|
if (value.isPressed && !IsPanning)
|
||||||
|
{
|
||||||
|
IsPanning = true;
|
||||||
|
PanStarted?.Invoke();
|
||||||
|
}
|
||||||
|
else if (!value.isPressed && IsPanning)
|
||||||
|
{
|
||||||
|
IsPanning = false;
|
||||||
|
PanEnded?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnPointerDelta(InputValue value)
|
||||||
|
{
|
||||||
|
if (IsPanning)
|
||||||
|
{
|
||||||
|
PanDelta?.Invoke(value.Get<Vector2>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ff77faf7e79154095ad57ad50df8c605
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
using System;
|
||||||
|
using NightclubArcadia.Interaction;
|
||||||
|
using NightclubArcadia.UI;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.EventSystems;
|
||||||
|
|
||||||
|
namespace NightclubArcadia.Player
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts pointer clicks into click-to-move destinations or interactable
|
||||||
|
/// approach targets.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PointerNavigationInput : MonoBehaviour
|
||||||
|
{
|
||||||
|
const float DoubleClickWindow = 0.35f;
|
||||||
|
const float DoubleClickPixelThreshold = 40f;
|
||||||
|
|
||||||
|
[SerializeField] Camera brainCamera;
|
||||||
|
[SerializeField] LayerMask clickMask = ~0;
|
||||||
|
[SerializeField] PlayerPointerInput pointerInput;
|
||||||
|
[SerializeField] ClickToMoveController clickToMove;
|
||||||
|
[SerializeField] PlayerMotionArbiter motionArbiter;
|
||||||
|
|
||||||
|
float lastClickTime = -1f;
|
||||||
|
Vector2 lastClickPosition;
|
||||||
|
bool pendingClick;
|
||||||
|
Vector2 pendingClickPosition;
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
if (pointerInput == null)
|
||||||
|
{
|
||||||
|
pointerInput = GetComponent<PlayerPointerInput>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clickToMove == null)
|
||||||
|
{
|
||||||
|
clickToMove = GetComponent<ClickToMoveController>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (motionArbiter == null)
|
||||||
|
{
|
||||||
|
motionArbiter = GetComponent<PlayerMotionArbiter>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (brainCamera == null)
|
||||||
|
{
|
||||||
|
brainCamera = Camera.main;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clickMask.value == ~0)
|
||||||
|
{
|
||||||
|
clickMask = LayerMask.GetMask("Default", "Ground", "Interactable");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnEnable()
|
||||||
|
{
|
||||||
|
if (pointerInput != null)
|
||||||
|
{
|
||||||
|
pointerInput.Clicked += OnClicked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnDisable()
|
||||||
|
{
|
||||||
|
if (pointerInput != null)
|
||||||
|
{
|
||||||
|
pointerInput.Clicked -= OnClicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingClick = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input System callbacks fire during event processing, where
|
||||||
|
// IsPointerOverGameObject() is invalid (queries last frame's UI).
|
||||||
|
// Queue the click and resolve it in Update instead.
|
||||||
|
void OnClicked()
|
||||||
|
{
|
||||||
|
if (pointerInput == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingClick = true;
|
||||||
|
pendingClickPosition = pointerInput.ScreenPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
if (!pendingClick)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingClick = false;
|
||||||
|
ProcessClick(pendingClickPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProcessClick(Vector2 screenPos)
|
||||||
|
{
|
||||||
|
if (!CanNavigate())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (brainCamera == null)
|
||||||
|
{
|
||||||
|
brainCamera = Camera.main;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (brainCamera == null || clickToMove == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ray = brainCamera.ScreenPointToRay(screenPos);
|
||||||
|
var hits = Physics.RaycastAll(ray, 200f, clickMask, QueryTriggerInteraction.Collide);
|
||||||
|
Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance));
|
||||||
|
|
||||||
|
var isDoubleClick = IsDoubleClick(screenPos);
|
||||||
|
lastClickTime = Time.unscaledTime;
|
||||||
|
lastClickPosition = screenPos;
|
||||||
|
|
||||||
|
foreach (var hit in hits)
|
||||||
|
{
|
||||||
|
var interactable = FindInteractable(hit.collider);
|
||||||
|
if (interactable != null)
|
||||||
|
{
|
||||||
|
if (isDoubleClick)
|
||||||
|
{
|
||||||
|
clickToMove.RequestRun();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clickToMove.SetInteractionTarget(interactable))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var hit in hits)
|
||||||
|
{
|
||||||
|
if (isDoubleClick)
|
||||||
|
{
|
||||||
|
clickToMove.RequestRun();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clickToMove.SetDestination(hit.point))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CanNavigate()
|
||||||
|
{
|
||||||
|
if (GetComponent<PlayerControlLock>() is { IsLocked: true })
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UIStateController.Instance == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !UIStateController.Instance.IsDialogueActive
|
||||||
|
&& !UIStateController.Instance.IsMenuOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsDoubleClick(Vector2 screenPos)
|
||||||
|
{
|
||||||
|
if (lastClickTime < 0f)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var withinTime = Time.unscaledTime - lastClickTime <= DoubleClickWindow;
|
||||||
|
var withinDistance = Vector2.Distance(screenPos, lastClickPosition) <= DoubleClickPixelThreshold;
|
||||||
|
return withinTime && withinDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
static IClickInteractable FindInteractable(Collider collider)
|
||||||
|
{
|
||||||
|
var behaviours = collider.GetComponentsInParent<MonoBehaviour>();
|
||||||
|
foreach (var behaviour in behaviours)
|
||||||
|
{
|
||||||
|
if (behaviour is IClickInteractable interactable)
|
||||||
|
{
|
||||||
|
return interactable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5f62460e35fda4829bd6ffeffe633a7e
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4392b20735bef4228be2723270edbb9b
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using NightclubArcadia.Player.Math;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace NightclubArcadia.Locomotion.Math.Tests
|
||||||
|
{
|
||||||
|
public class MotionInputMathTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void WorldToCameraRelativeInput_ZeroVelocity_ReturnsZero()
|
||||||
|
{
|
||||||
|
var result = MotionInputMath.WorldToCameraRelativeInput(Vector3.zero, 2f, 0f);
|
||||||
|
Assert.AreEqual(Vector2.zero, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WorldToCameraRelativeInput_YawZero_PreservesForward()
|
||||||
|
{
|
||||||
|
var world = new Vector3(0f, 0f, 2f);
|
||||||
|
var result = MotionInputMath.WorldToCameraRelativeInput(world, 2f, 0f);
|
||||||
|
Assert.AreEqual(new Vector2(0f, 1f), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WorldToCameraRelativeInput_Yaw90_RotatesCorrectly()
|
||||||
|
{
|
||||||
|
var world = new Vector3(0f, 0f, 2f);
|
||||||
|
var result = MotionInputMath.WorldToCameraRelativeInput(world, 2f, 90f);
|
||||||
|
Assert.That(result.x, Is.EqualTo(1f).Within(0.001f));
|
||||||
|
Assert.That(result.y, Is.EqualTo(0f).Within(0.001f));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WorldToCameraRelativeInput_ClampFractionToOne()
|
||||||
|
{
|
||||||
|
var world = new Vector3(0f, 0f, 10f);
|
||||||
|
var result = MotionInputMath.WorldToCameraRelativeInput(world, 2f, 0f);
|
||||||
|
Assert.That(result.magnitude, Is.EqualTo(1f).Within(0.001f));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WorldToCameraRelativeInput_RoundTripsThroughAtan2()
|
||||||
|
{
|
||||||
|
var world = new Vector3(1f, 0f, 2f).normalized;
|
||||||
|
var cameraYaw = 15f;
|
||||||
|
var input = MotionInputMath.WorldToCameraRelativeInput(world, 1f, cameraYaw);
|
||||||
|
var targetRotation = Mathf.Atan2(input.x, input.y) * Mathf.Rad2Deg + cameraYaw;
|
||||||
|
var reconstructed = Quaternion.Euler(0f, targetRotation, 0f) * Vector3.forward;
|
||||||
|
var flatWorld = new Vector3(world.x, 0f, world.z).normalized;
|
||||||
|
|
||||||
|
Assert.That(Vector3.Dot(reconstructed, flatWorld), Is.EqualTo(1f).Within(0.001f));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ScreenDeltaToWorldPan_UsesCameraBasis()
|
||||||
|
{
|
||||||
|
var right = Vector3.right;
|
||||||
|
var forward = Vector3.forward;
|
||||||
|
var delta = new Vector2(10f, 0f);
|
||||||
|
var result = MotionInputMath.ScreenDeltaToWorldPan(delta, right, forward, 0.02f);
|
||||||
|
Assert.AreEqual(new Vector3(-0.2f, 0f, 0f), result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f3e10736152304d63ab3fc46cfdb2873
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "NightclubArcadia.Locomotion.Math.Tests",
|
||||||
|
"rootNamespace": "NightclubArcadia.Locomotion.Math.Tests",
|
||||||
|
"references": [
|
||||||
|
"NightclubArcadia.Locomotion.Math",
|
||||||
|
"UnityEngine.TestRunner",
|
||||||
|
"UnityEditor.TestRunner"
|
||||||
|
],
|
||||||
|
"includePlatforms": [
|
||||||
|
"Editor"
|
||||||
|
],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"overrideReferences": true,
|
||||||
|
"precompiledReferences": [
|
||||||
|
"nunit.framework.dll"
|
||||||
|
],
|
||||||
|
"autoReferenced": false,
|
||||||
|
"defineConstraints": [
|
||||||
|
"UNITY_INCLUDE_TESTS"
|
||||||
|
],
|
||||||
|
"versionDefines": [],
|
||||||
|
"noEngineReferences": false
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: caa98c93c07f34ac6aedf23ee4f45c2a
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"com.unity.ai.navigation": "2.0.14",
|
"com.unity.ai.navigation": "2.0.14",
|
||||||
|
"com.unity.cinemachine": "3.1.7",
|
||||||
"com.unity.collab-proxy": "2.13.5",
|
"com.unity.collab-proxy": "2.13.5",
|
||||||
"com.unity.ide.rider": "3.0.38",
|
"com.unity.ide.rider": "3.0.38",
|
||||||
"com.unity.ide.visualstudio": "2.0.26",
|
"com.unity.ide.visualstudio": "2.0.26",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
},
|
},
|
||||||
"com.unity.cinemachine": {
|
"com.unity.cinemachine": {
|
||||||
"version": "3.1.7",
|
"version": "3.1.7",
|
||||||
"depth": 1,
|
"depth": 0,
|
||||||
"source": "registry",
|
"source": "registry",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"com.unity.splines": "2.0.0",
|
"com.unity.splines": "2.0.0",
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
},
|
},
|
||||||
"com.unity.mathematics": {
|
"com.unity.mathematics": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"depth": 3,
|
"depth": 2,
|
||||||
"source": "builtin",
|
"source": "builtin",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
},
|
},
|
||||||
@@ -168,7 +168,7 @@
|
|||||||
},
|
},
|
||||||
"com.unity.splines": {
|
"com.unity.splines": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"depth": 2,
|
"depth": 1,
|
||||||
"source": "registry",
|
"source": "registry",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"com.unity.mathematics": "1.2.1",
|
"com.unity.mathematics": "1.2.1",
|
||||||
|
|||||||
@@ -73,10 +73,10 @@ NavMeshProjectSettings:
|
|||||||
m_Settings:
|
m_Settings:
|
||||||
- serializedVersion: 2
|
- serializedVersion: 2
|
||||||
agentTypeID: 0
|
agentTypeID: 0
|
||||||
agentRadius: 0.5
|
agentRadius: 0.3
|
||||||
agentHeight: 2
|
agentHeight: 1.8
|
||||||
agentSlope: 45
|
agentSlope: 45
|
||||||
agentClimb: 0.75
|
agentClimb: 0.25
|
||||||
ledgeDropHeight: 0
|
ledgeDropHeight: 0
|
||||||
maxJumpAcrossDistance: 0
|
maxJumpAcrossDistance: 0
|
||||||
minRegionArea: 2
|
minRegionArea: 2
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ TagManager:
|
|||||||
-
|
-
|
||||||
- Water
|
- Water
|
||||||
- UI
|
- UI
|
||||||
-
|
- Interactable
|
||||||
-
|
- Ground
|
||||||
-
|
-
|
||||||
-
|
-
|
||||||
-
|
-
|
||||||
|
|||||||
Reference in New Issue
Block a user