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; public float workPower = 10f; [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; [Header("Worker")] public Worker assignedWorker; private PlayerInputActions _inputActions; private IInteractable _currentInteractable; private Camera _mainCamera; private Animator _animator; private EquipmentSocket _equipmentSocket; private EquipmentData _pendingEquipmentData; private string _currentEquipmentSocket; private bool _isInteracting = false; private Coroutine _interactionTimeoutCoroutine; public bool IsInteracting => _isInteracting; public float WorkPower => workPower; 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; // Check if current interactable is no longer valid (e.g., building completed) if (_isInteracting && _currentInteractable != null) { if (!_currentInteractable.CanInteract(OwnerClientId)) { EndInteraction(); } } 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) { bool isResource = _currentInteractable is Resource; bool hasWorker = assignedWorker != null && assignedWorker.OwnerPlayerId == OwnerClientId; bool isWorkerFollowing = hasWorker && (int)assignedWorker.CurrentState == 1; bool shouldPlayAnimation = !isResource || !hasWorker || !isWorkerFollowing; _isInteracting = true; _pendingEquipmentData = _currentInteractable.GetEquipmentData(); string animTrigger = _currentInteractable.GetInteractionAnimation(); bool hasAnimation = !string.IsNullOrEmpty(animTrigger); if (playAnimations && _animator != null && hasAnimation && shouldPlayAnimation) { _animator.SetTrigger(animTrigger); _interactionTimeoutCoroutine = StartCoroutine(InteractionTimeout(3f)); } else { _isInteracting = false; } if (shouldPlayAnimation && !useAnimationEvents && useEquipment && _equipmentSocket != null && _pendingEquipmentData != null) { if (_pendingEquipmentData.attachOnStart && _pendingEquipmentData.equipmentPrefab != null) { AttachEquipment(); if (_pendingEquipmentData.detachOnEnd) { StartCoroutine(DetachEquipmentAfterDelay(2f)); } } } _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() { if (!IsOwner) return; if (_interactionTimeoutCoroutine != null) { StopCoroutine(_interactionTimeoutCoroutine); _interactionTimeoutCoroutine = null; } _isInteracting = false; } // ======================================== // 내부 헬퍼 함수들 // ======================================== 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 System.Collections.IEnumerator InteractionTimeout(float timeout) { yield return new WaitForSeconds(timeout); if (_isInteracting) { _isInteracting = false; _interactionTimeoutCoroutine = null; } } private void EndInteraction() { _isInteracting = false; if (_interactionTimeoutCoroutine != null) { StopCoroutine(_interactionTimeoutCoroutine); _interactionTimeoutCoroutine = null; } } private void OnGUI() { if (!IsOwner) return; GUIStyle style = new GUIStyle(GUI.skin.label) { fontSize = 36, alignment = TextAnchor.MiddleCenter }; style.normal.textColor = Color.black; string prompt = ""; if (_currentInteractable != null) { prompt = _currentInteractable.GetInteractionPrompt(); bool isResource = _currentInteractable is Resource; if (isResource && assignedWorker != null && assignedWorker.OwnerPlayerId == OwnerClientId) { prompt += " (워커 할당 가능)"; } if (_isInteracting) { prompt += " (진행 중...)"; style.normal.textColor = Color.red; } } if (assignedWorker != null && assignedWorker.OwnerPlayerId == OwnerClientId) { string workerStatus = assignedWorker.CurrentState switch { WorkerState.Following => "따라가는 중", WorkerState.Mining => "채굴 중", WorkerState.Returning => "반환 중", _ => "대기 중" }; GUI.Label(new Rect(Screen.width / 2 - 200, Screen.height - 150, 800, 100), $"워커: {workerStatus} (가방: {assignedWorker.CurrentBagResources}/{assignedWorker.maxBagCapacity})", style); } else { GUI.Label(new Rect(Screen.width / 2 - 200, Screen.height - 150, 800, 100), $"워커: 미할당", style); } if (_currentInteractable != null) { GUI.Label(new Rect(Screen.width / 2 - 200, Screen.height - 100, 800, 100), prompt, style); } } public override void OnDestroy() { if (_inputActions != null) { _inputActions.Dispose(); } } } }