Files
Emberwild/Assets/GAME/Script/Player/PlayerInteractor.cs
Mathew 78bfdf2828 Merge remote-tracking branch 'origin/feat/inport-props' into feat/craft-discovery
# Conflicts:
#	Assets/External/Animated PBR Chest Demo/Materials/WoodChest.mat
#	Packages/com.distantlands.cozy.core/Content/Integration/Import for BiRP.unitypackage.meta
#	Packages/com.distantlands.cozy.core/Content/Integration/Import for HDRP.unitypackage.meta
#	Packages/com.distantlands.cozy.core/Content/Integration/Import for URP.unitypackage.meta
2026-07-25 19:42:26 +02:00

211 lines
7.6 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 = RaycastInteractable(out _);
// 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) return;
IInteractable interactable = RaycastInteractable(out _);
if (interactable != null) interactable.Interact();
}
#endregion
#region Interactable Detection
/// <summary>
/// One raycast forward for an interaction target. Returns the first IInteractable under the aim
/// ray that actually offers an interaction — a component whose InteractionPrompt is null/empty is
/// treated as inert and skipped, so IInteractables that opt out (e.g. a Harvestable with no
/// bare-hand gathering) never register as a hover or a valid interact. Triggers are included so
/// walkable interactables (bushes with a trigger collider) still respond.
///
/// The lookup walks up from the collider (as the tool swing does) rather than reading the collider
/// alone: an object whose colliders sit on child meshes is perfectly valid, and matching only the
/// exact hit object made such an object silently unusable — no prompt, no interaction, no error.
/// </summary>
private IInteractable RaycastInteractable(out RaycastHit hit)
{
hit = default;
if (raycastOrigin == null) return null;
if (Physics.Raycast(raycastOrigin.position, raycastOrigin.forward, out hit,
interactRange, interactMask, QueryTriggerInteraction.Collide))
{
IInteractable interactable = hit.collider.GetComponentInParent<IInteractable>();
if (interactable != null && !string.IsNullOrEmpty(interactable.InteractionPrompt))
return interactable;
}
return null;
}
#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 = RaycastInteractable(out RaycastHit hit) != null;
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
}
}