Files
Emberwild/Assets/GAME/Script/Player/PlayerInteractor.cs
T
2026-06-22 16:18:34 +02:00

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
}
}