using Unity.Netcode; using UnityEngine; using UnityEngine.InputSystem; public sealed class PlayerInteractionController : NetworkBehaviour, IPlayerSystem { [SerializeField, Min(0.1f)] private float interactionDistance = 3f; [SerializeField] private LayerMask interactionMask = ~0; private Camera playerCamera; private IInteractable focusedInteractable; private IPlayerInteractionSession activeSession; private readonly RaycastHit[] interactionHits = new RaycastHit[32]; private bool activeSessionReleaseRequested; private bool isInitialized; public void Initialize(PlayerFacade facade) { playerCamera = GetComponentInChildren(true); isInitialized = playerCamera != null; } private void Update() { if (!isInitialized || !IsOwner || Keyboard.current == null) return; if (PlayerInteractionSessionRegistry.TryGetControlledSession( OwnerClientId, out activeSession)) { focusedInteractable = null; if (!Keyboard.current.eKey.isPressed && !activeSessionReleaseRequested) { activeSessionReleaseRequested = true; activeSession.RequestRelease(); } return; } activeSession = null; activeSessionReleaseRequested = false; focusedInteractable = FindFocusedInteractable(); if (Keyboard.current.eKey.wasPressedThisFrame && focusedInteractable != null && focusedInteractable.IsInteractionAvailable) { focusedInteractable.RequestInteraction(); } } private IInteractable FindFocusedInteractable() { Ray ray = new(playerCamera.transform.position, playerCamera.transform.forward); int hitCount = Physics.RaycastNonAlloc( ray, interactionHits, interactionDistance, interactionMask, QueryTriggerInteraction.Collide); if (hitCount <= 0) return null; IInteractable closestAvailable = null; float closestAvailableDistance = float.PositiveInfinity; IInteractable closestUnavailable = null; float closestUnavailableDistance = float.PositiveInfinity; float closestBlockingDistance = float.PositiveInfinity; for (int hitIndex = 0; hitIndex < hitCount; hitIndex++) { RaycastHit hit = interactionHits[hitIndex]; IInteractable interactable = FindInteractable(hit.collider); if (interactable == null) { closestBlockingDistance = Mathf.Min(closestBlockingDistance, hit.distance); continue; } if (interactable.IsInteractionAvailable) { if (hit.distance < closestAvailableDistance) { closestAvailable = interactable; closestAvailableDistance = hit.distance; } } else if (hit.distance < closestUnavailableDistance) { closestUnavailable = interactable; closestUnavailableDistance = hit.distance; } } if (closestAvailable != null && closestAvailableDistance <= closestBlockingDistance) return closestAvailable; return closestUnavailable != null && closestUnavailableDistance <= closestBlockingDistance ? closestUnavailable : null; } private static IInteractable FindInteractable(Collider collider) { foreach (MonoBehaviour component in collider.GetComponentsInParent()) { if (component is IInteractable interactable) return interactable; } return null; } private void OnGUI() { if (!IsOwner) return; if (activeSession != null) { DrawPrompt(activeSession.ReleasePrompt); return; } if (focusedInteractable == null) return; string prompt = focusedInteractable.IsInteractionAvailable ? "[E] " + focusedInteractable.InteractionPrompt : "In use"; DrawPrompt(prompt); } private static void DrawPrompt(string prompt) { GUIStyle style = new(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontSize = 16, normal = { textColor = Color.white } }; Rect area = new(0f, Screen.height * 0.62f, Screen.width, 28f); GUI.Label(area, prompt, style); } }