using Unity.Netcode; using UnityEngine; using UnityEngine.InputSystem; namespace Northbound { /// /// 플레이어가 월드의 오브젝트와 상호작용하는 시스템 /// public class PlayerInteraction : NetworkBehaviour { [Header("Interaction Settings")] public float interactionRange = 3f; public LayerMask interactableLayer = ~0; [Header("Detection")] public Transform rayOrigin; public bool useForwardDirection = true; [Header("Animation")] public bool playAnimations = true; public bool useAnimationEvents = true; public bool blockDuringAnimation = true; [Header("Equipment")] public bool useEquipment = true; [Header("Debug")] public bool showDebugRay = true; private PlayerInputActions _inputActions; private IInteractable _currentInteractable; private Camera _mainCamera; private Animator _animator; private EquipmentSocket _equipmentSocket; private InteractionEquipmentData _pendingEquipmentData; private string _currentEquipmentSocket; private bool _isInteracting = false; public override void OnNetworkSpawn() { if (!IsOwner) return; _mainCamera = Camera.main; _animator = GetComponent(); _equipmentSocket = GetComponent(); if (rayOrigin == null) rayOrigin = transform; _inputActions = new PlayerInputActions(); _inputActions.Player.Interact.performed += OnInteract; _inputActions.Enable(); } public override void OnNetworkDespawn() { if (IsOwner && _inputActions != null) { _inputActions.Player.Interact.performed -= OnInteract; _inputActions.Disable(); _inputActions.Dispose(); } } private void Update() { if (!IsOwner) return; DetectInteractable(); } private void DetectInteractable() { Vector3 origin = rayOrigin.position; Vector3 direction = useForwardDirection ? transform.forward : _mainCamera.transform.forward; Ray ray = new Ray(origin, direction); if (showDebugRay) { Debug.DrawRay(ray.origin, ray.direction * interactionRange, _currentInteractable != null ? Color.green : Color.yellow); } if (Physics.Raycast(ray, out RaycastHit hit, interactionRange, interactableLayer)) { IInteractable interactable = hit.collider.GetComponent(); if (interactable == null) interactable = hit.collider.GetComponentInParent(); if (interactable != null && interactable.CanInteract(OwnerClientId)) { _currentInteractable = interactable; return; } } _currentInteractable = null; } private void OnInteract(InputAction.CallbackContext context) { if (blockDuringAnimation && _isInteracting) return; if (_currentInteractable != null) { _isInteracting = true; _pendingEquipmentData = _currentInteractable.GetEquipmentData(); string animTrigger = _currentInteractable.GetInteractionAnimation(); bool hasAnimation = !string.IsNullOrEmpty(animTrigger); // 장비 장착 (애니메이션 이벤트 사용 안 할 경우) if (!useAnimationEvents && useEquipment && _equipmentSocket != null && _pendingEquipmentData != null) { if (_pendingEquipmentData.attachOnStart && _pendingEquipmentData.equipmentPrefab != null) { AttachEquipment(); if (_pendingEquipmentData.detachOnEnd) { StartCoroutine(DetachEquipmentAfterDelay(2f)); } } } // 애니메이션 재생 if (playAnimations && _animator != null && hasAnimation) { _animator.SetTrigger(animTrigger); } else { // 애니메이션이 없으면 즉시 상호작용 완료 _isInteracting = false; } // 상호작용 실행 (서버에서 처리) _currentInteractable.Interact(OwnerClientId); } } // ======================================== // Animation Event 함수들 // ======================================== public void OnEquipTool() { if (!useAnimationEvents || !useEquipment) return; AttachEquipment(); } public void OnEquipTool(string socketName) { if (!useAnimationEvents || !useEquipment) return; AttachEquipment(socketName); } public void OnUnequipTool() { if (!useAnimationEvents || !useEquipment) return; DetachEquipment(); } public void OnUnequipTool(string socketName) { if (!useAnimationEvents || !useEquipment) return; DetachEquipment(socketName); } public void OnInteractionComplete() { _isInteracting = false; Debug.Log("[PlayerInteraction] 상호작용 완료"); } // ======================================== // 내부 헬퍼 함수들 // ======================================== private void AttachEquipment(string socketName = null) { if (_equipmentSocket == null || _pendingEquipmentData == null) return; if (_pendingEquipmentData.equipmentPrefab == null) return; string socket = socketName ?? _pendingEquipmentData.socketName; _equipmentSocket.AttachToSocket(socket, _pendingEquipmentData.equipmentPrefab); _currentEquipmentSocket = socket; } private void DetachEquipment(string socketName = null) { if (_equipmentSocket == null) return; string socket = socketName ?? _currentEquipmentSocket; if (!string.IsNullOrEmpty(socket)) { _equipmentSocket.DetachFromSocket(socket); if (socket == _currentEquipmentSocket) _currentEquipmentSocket = null; } } private System.Collections.IEnumerator DetachEquipmentAfterDelay(float delay) { yield return new WaitForSeconds(delay); DetachEquipment(); if (!useAnimationEvents) { _isInteracting = false; } } private void OnGUI() { if (!IsOwner || _currentInteractable == null) return; GUIStyle style = new GUIStyle(GUI.skin.label) { fontSize = 24, alignment = TextAnchor.MiddleCenter }; style.normal.textColor = Color.white; string prompt = _currentInteractable.GetInteractionPrompt(); if (_isInteracting) { prompt += " (진행 중...)"; style.normal.textColor = Color.yellow; } GUI.Label(new Rect(Screen.width / 2 - 200, Screen.height - 100, 400, 50), prompt, style); } override public void OnDestroy() { if (_inputActions != null) { _inputActions.Dispose(); } base.OnDestroy(); } } }