193 lines
6.7 KiB
C#
193 lines
6.7 KiB
C#
using UnityEngine;
|
|
using Ashwild.Interaction;
|
|
|
|
namespace Ashwild.Player
|
|
{
|
|
/// <summary>
|
|
/// Raycasts forward on interact input and triggers the first IInteractable hit.
|
|
/// Draws a selection gizmo that turns its "can interact" colour and shortens to
|
|
/// the hit point when the aim is currently pointing at an interactable target.
|
|
/// </summary>
|
|
[DisallowMultipleComponent]
|
|
public class PlayerInteractor : MonoBehaviour
|
|
{
|
|
#region Serialized Fields
|
|
|
|
[Header("Raycast")]
|
|
/// <summary>
|
|
/// Maximum distance, in metres, at which an interactable can be triggered.
|
|
/// </summary>
|
|
[SerializeField] private float interactRange = 3f;
|
|
|
|
/// <summary>
|
|
/// Layers the interaction ray is allowed to hit.
|
|
/// </summary>
|
|
[SerializeField] private LayerMask interactMask = ~0;
|
|
|
|
/// <summary>
|
|
/// Origin and forward direction of the interaction ray (usually the camera).
|
|
/// </summary>
|
|
[SerializeField] private Transform raycastOrigin;
|
|
|
|
/// <summary>
|
|
/// How long, in seconds, a new target must stay under the ray before the
|
|
/// hover change is committed. Debounces fast view sweeps so the crosshair
|
|
/// animation never flickers on objects only glanced at for a few frames.
|
|
/// </summary>
|
|
[SerializeField] private float hoverStabilityTime = 0.08f;
|
|
|
|
[Header("Gizmo")]
|
|
/// <summary>
|
|
/// Gizmo colour when the ray is not aiming at any interactable.
|
|
/// </summary>
|
|
[SerializeField] private Color idleGizmoColor = Color.green;
|
|
|
|
/// <summary>
|
|
/// Gizmo colour when the ray is aiming at a valid interactable.
|
|
/// </summary>
|
|
[SerializeField] private Color interactableGizmoColor = Color.cyan;
|
|
|
|
#endregion
|
|
|
|
#region State
|
|
|
|
/// <summary>
|
|
/// The committed hover target broadcast on the bus, reused by interact.
|
|
/// </summary>
|
|
private IInteractable currentHover;
|
|
|
|
/// <summary>
|
|
/// The target the ray currently sees, pending stability before it is
|
|
/// committed as the hover (debounce against fast view sweeps).
|
|
/// </summary>
|
|
private IInteractable candidateHover;
|
|
|
|
/// <summary>
|
|
/// How long the candidate has stayed unchanged, in seconds.
|
|
/// </summary>
|
|
private float candidateTimer;
|
|
|
|
#endregion
|
|
|
|
#region Unity Lifecycle
|
|
|
|
/// <summary>
|
|
/// Subscribes to the interact input event.
|
|
/// </summary>
|
|
private void OnEnable() { PlayerEvents.InteractPressed += OnInteractPressed; }
|
|
|
|
/// <summary>
|
|
/// Unsubscribes — must mirror OnEnable exactly — and clears any hover so
|
|
/// consumers (crosshair) reset when this component is disabled.
|
|
/// </summary>
|
|
private void OnDisable()
|
|
{
|
|
PlayerEvents.InteractPressed -= OnInteractPressed;
|
|
CommitHover(null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detects the interactable under the aim ray each frame and commits hover
|
|
/// changes only once a new target has stayed stable for hoverStabilityTime,
|
|
/// so quick view sweeps never spam the bus. One raycast per frame.
|
|
/// </summary>
|
|
private void Update()
|
|
{
|
|
// While input is locked, drop the hover immediately (no debounce).
|
|
if (PlayerEvents.InputLocked)
|
|
{
|
|
candidateHover = null;
|
|
candidateTimer = 0f;
|
|
CommitHover(null);
|
|
return;
|
|
}
|
|
|
|
IInteractable raw = null;
|
|
if (raycastOrigin != null
|
|
&& Physics.Raycast(raycastOrigin.position, raycastOrigin.forward, out RaycastHit hit,
|
|
interactRange, interactMask, QueryTriggerInteraction.Collide))
|
|
{
|
|
hit.collider.TryGetComponent(out raw);
|
|
}
|
|
|
|
// Restart the stability timer whenever the seen target changes.
|
|
if (!ReferenceEquals(raw, candidateHover))
|
|
{
|
|
candidateHover = raw;
|
|
candidateTimer = 0f;
|
|
}
|
|
else
|
|
{
|
|
candidateTimer += Time.deltaTime;
|
|
}
|
|
|
|
// Commit only once the candidate has been stable long enough.
|
|
if (!ReferenceEquals(candidateHover, currentHover) && candidateTimer >= hoverStabilityTime)
|
|
CommitHover(candidateHover);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Hover Detection
|
|
|
|
/// <summary>
|
|
/// Stores the committed hover target and raises the bus event when it
|
|
/// differs from the previous one.
|
|
/// </summary>
|
|
private void CommitHover(IInteractable hover)
|
|
{
|
|
if (ReferenceEquals(hover, currentHover)) return;
|
|
|
|
currentHover = hover;
|
|
PlayerEvents.RaiseInteractableHoverChanged(hover);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Event Handlers
|
|
|
|
/// <summary>
|
|
/// Interacts with whatever the ray points at right now. Uses a fresh raycast
|
|
/// rather than the debounced hover so a quick aim-and-click never misses.
|
|
/// </summary>
|
|
private void OnInteractPressed()
|
|
{
|
|
if (PlayerEvents.InputLocked || raycastOrigin == null) return;
|
|
|
|
if (Physics.Raycast(raycastOrigin.position, raycastOrigin.forward, out RaycastHit hit,
|
|
interactRange, interactMask, QueryTriggerInteraction.Collide)
|
|
&& hit.collider.TryGetComponent(out IInteractable interactable))
|
|
{
|
|
interactable.Interact();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Gizmos
|
|
|
|
/// <summary>
|
|
/// Draws the interaction ray when selected. The ray turns the "interactable"
|
|
/// colour and stops at the hit point whenever it is aiming at an IInteractable;
|
|
/// otherwise it stays the idle colour and spans the full range.
|
|
/// </summary>
|
|
private void OnDrawGizmosSelected()
|
|
{
|
|
Transform o = raycastOrigin != null ? raycastOrigin : transform;
|
|
|
|
bool hitInteractable = Physics.Raycast(o.position, o.forward, out RaycastHit hit,
|
|
interactRange, interactMask, QueryTriggerInteraction.Collide)
|
|
&& hit.collider.TryGetComponent(out IInteractable _);
|
|
|
|
float length = hitInteractable ? hit.distance : interactRange;
|
|
Vector3 endPoint = o.position + o.forward * length;
|
|
|
|
Gizmos.color = hitInteractable ? interactableGizmoColor : idleGizmoColor;
|
|
Gizmos.DrawRay(o.position, o.forward * length);
|
|
Gizmos.DrawWireSphere(endPoint, 0.1f);
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|