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,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
|
||||
Reference in New Issue
Block a user