initial
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
using Unity.Netcode;
|
||||
|
||||
namespace AfterHours.Interaction
|
||||
{
|
||||
/// <summary>
|
||||
/// Alles, was der Spieler per Blick + Taste bedienen kann.
|
||||
/// WICHTIG: Interact() läuft nur auf dem Server. Clients rufen es niemals
|
||||
/// direkt auf, sondern gehen über PlayerInteractor.RequestInteractRpc.
|
||||
/// </summary>
|
||||
public interface IInteractable
|
||||
{
|
||||
/// <summary>Text für den Blick-Prompt, z. B. "Regal auffüllen".</summary>
|
||||
string GetPrompt(ulong clientId);
|
||||
|
||||
/// <summary>Serverseitige Ausführung. Muss selbst validieren.</summary>
|
||||
void Interact(NetworkObject actor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 994d75674ec8c0e46a8c40f1bee27330
|
||||
@@ -0,0 +1,69 @@
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AfterHours.Interaction
|
||||
{
|
||||
/// <summary>
|
||||
/// Raycast auf dem Owner-Client für den Prompt, Ausführung auf dem Server.
|
||||
/// Der Server vertraut dem Client die Zielauswahl NICHT blind – er prüft
|
||||
/// Distanz und Sichtbarkeit erneut (siehe CLAUDE.md §3).
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(NetworkObject))]
|
||||
public sealed class PlayerInteractor : NetworkBehaviour
|
||||
{
|
||||
[SerializeField] private Transform _eyes;
|
||||
[SerializeField] private float _reach = 2.5f;
|
||||
[SerializeField] private LayerMask _interactableMask = ~0;
|
||||
|
||||
/// <summary>Toleranz, weil Client und Server nie exakt denselben Frame sehen.</summary>
|
||||
private const float ServerReachTolerance = 1.5f;
|
||||
|
||||
private IInteractable _current;
|
||||
public string CurrentPrompt { get; private set; }
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsOwner) return;
|
||||
|
||||
RefreshTarget();
|
||||
|
||||
if (_current != null && Input.GetKeyDown(KeyCode.E))
|
||||
{
|
||||
var target = ((MonoBehaviour)_current).GetComponent<NetworkObject>();
|
||||
if (target != null) RequestInteractRpc(target);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshTarget()
|
||||
{
|
||||
_current = null;
|
||||
CurrentPrompt = null;
|
||||
|
||||
if (!Physics.Raycast(_eyes.position, _eyes.forward, out var hit, _reach, _interactableMask))
|
||||
return;
|
||||
|
||||
if (!hit.collider.TryGetComponent<IInteractable>(out var interactable))
|
||||
return;
|
||||
|
||||
_current = interactable;
|
||||
CurrentPrompt = interactable.GetPrompt(OwnerClientId);
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server)]
|
||||
private void RequestInteractRpc(NetworkObjectReference targetRef)
|
||||
{
|
||||
if (!targetRef.TryGet(out var target)) return;
|
||||
|
||||
// Server-Validierung: ist der Spieler überhaupt in Reichweite?
|
||||
float distance = Vector3.Distance(transform.position, target.transform.position);
|
||||
if (distance > _reach + ServerReachTolerance)
|
||||
{
|
||||
Debug.LogWarning($"[PlayerInteractor] Client {OwnerClientId} zu weit entfernt ({distance:F1} m).");
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.TryGetComponent<IInteractable>(out var interactable))
|
||||
interactable.Interact(NetworkObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f8feeb0d94c7e44b8d05c33089a77ef
|
||||
Reference in New Issue
Block a user