409 lines
14 KiB
C#
409 lines
14 KiB
C#
using Unity.Netcode;
|
|
using Unity.Netcode.Components;
|
|
using UnityEngine;
|
|
using UnityEngine.InputSystem;
|
|
|
|
namespace Northbound
|
|
{
|
|
/// <summary>
|
|
/// 플레이어가 월드의 오브젝트와 상호작용하는 시스템
|
|
/// </summary>
|
|
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;
|
|
|
|
[Header("Worker")]
|
|
public Worker assignedWorker;
|
|
|
|
private IInteractable _currentInteractable;
|
|
private IInteractable _unavailableInteractable;
|
|
private Camera _mainCamera;
|
|
private Animator _animator;
|
|
private NetworkAnimator _networkAnimator;
|
|
private EquipmentSocket _equipmentSocket;
|
|
|
|
private EquipmentData _pendingEquipmentData;
|
|
private string _currentEquipmentSocket;
|
|
private bool _isInteracting = false;
|
|
private Coroutine _interactionTimeoutCoroutine;
|
|
|
|
private NetworkPlayerController _networkPlayerController;
|
|
private PlayerStats _playerStats;
|
|
private bool _isInputInitialized = false;
|
|
|
|
public bool IsInteracting => _isInteracting;
|
|
public float WorkPower => _playerStats?.GetManpower() ?? 10f;
|
|
public IInteractable CurrentUnavailableInteractable => _unavailableInteractable;
|
|
|
|
// 로컬 플레이어인지 확인
|
|
private bool IsLocalPlayer => _networkPlayerController != null && _networkPlayerController.IsLocalPlayer;
|
|
private ulong LocalPlayerId => _networkPlayerController != null ? _networkPlayerController.OwnerPlayerId : OwnerClientId;
|
|
|
|
private void Awake()
|
|
{
|
|
_networkPlayerController = GetComponent<NetworkPlayerController>();
|
|
_networkAnimator = GetComponent<NetworkAnimator>();
|
|
_playerStats = GetComponent<PlayerStats>();
|
|
|
|
}
|
|
|
|
public override void OnNetworkSpawn()
|
|
{
|
|
_animator = GetComponent<Animator>();
|
|
_equipmentSocket = GetComponent<EquipmentSocket>();
|
|
|
|
if (rayOrigin == null)
|
|
rayOrigin = transform;
|
|
|
|
// _ownerPlayerId 변경 이벤트 구독
|
|
if (_networkPlayerController != null)
|
|
{
|
|
_networkPlayerController.OnOwnerChanged += OnOwnerPlayerIdChanged;
|
|
_networkPlayerController.OnInputInitialized += TryInitializeInput;
|
|
}
|
|
|
|
// 이미 로컬 플레이어면 입력 초기화
|
|
TryInitializeInput();
|
|
}
|
|
|
|
private void OnOwnerPlayerIdChanged(ulong newOwnerId)
|
|
{
|
|
TryInitializeInput();
|
|
}
|
|
|
|
private void TryInitializeInput()
|
|
{
|
|
if (!IsLocalPlayer) return;
|
|
if (_isInputInitialized) return; // 이미 초기화됨
|
|
if (_networkPlayerController.InputActions == null) return; // 아직 InputActions가 없음
|
|
|
|
_isInputInitialized = true;
|
|
_mainCamera = Camera.main;
|
|
_networkPlayerController.InputActions.Player.Interact.performed += OnInteract;
|
|
}
|
|
|
|
public override void OnNetworkDespawn()
|
|
{
|
|
if (_networkPlayerController != null)
|
|
{
|
|
_networkPlayerController.OnOwnerChanged -= OnOwnerPlayerIdChanged;
|
|
_networkPlayerController.OnInputInitialized -= TryInitializeInput;
|
|
|
|
// 입력 이벤트 해제
|
|
if (_networkPlayerController.InputActions != null)
|
|
{
|
|
_networkPlayerController.InputActions.Player.Interact.performed -= OnInteract;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (!IsLocalPlayer) return;
|
|
|
|
if (_isInteracting && _currentInteractable != null)
|
|
{
|
|
if (!_currentInteractable.CanInteract(LocalPlayerId))
|
|
{
|
|
EndInteraction();
|
|
}
|
|
}
|
|
|
|
DetectInteractable();
|
|
}
|
|
|
|
private void DetectInteractable()
|
|
{
|
|
// 카메라가 없으면 다시 시도
|
|
if (_mainCamera == null)
|
|
{
|
|
_mainCamera = Camera.main;
|
|
if (_mainCamera == null) return;
|
|
}
|
|
|
|
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<IInteractable>();
|
|
|
|
if (interactable == null)
|
|
interactable = hit.collider.GetComponentInParent<IInteractable>();
|
|
|
|
if (interactable != null)
|
|
{
|
|
if (interactable.CanInteract(LocalPlayerId))
|
|
{
|
|
_currentInteractable = interactable;
|
|
_unavailableInteractable = null;
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
_currentInteractable = null;
|
|
_unavailableInteractable = interactable;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
_currentInteractable = null;
|
|
_unavailableInteractable = null;
|
|
}
|
|
|
|
private void OnInteract(InputAction.CallbackContext context)
|
|
{
|
|
// 플레이어가 죽어있으면 상호작용 불가
|
|
if (_networkPlayerController != null && _networkPlayerController.IsDead()) return;
|
|
|
|
if (blockDuringAnimation && _isInteracting)
|
|
return;
|
|
|
|
if (_currentInteractable != null)
|
|
{
|
|
// 상호작용 가능 여부 다시 확인 (NetworkVariable 동기화 지연 대응)
|
|
if (!_currentInteractable.CanInteract(LocalPlayerId))
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool isResource = _currentInteractable is Resource;
|
|
bool hasWorker = assignedWorker != null && assignedWorker.OwnerPlayerId == LocalPlayerId;
|
|
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 && hasAnimation && shouldPlayAnimation)
|
|
{
|
|
// 서버에서 애니메이션 실행 (동기화를 위해)
|
|
PlayAnimationServerRpc(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(LocalPlayerId);
|
|
}
|
|
}
|
|
|
|
[Rpc(SendTo.Server)]
|
|
private void PlayAnimationServerRpc(string animTrigger)
|
|
{
|
|
if (_networkAnimator != null && !string.IsNullOrEmpty(animTrigger))
|
|
{
|
|
_networkAnimator.SetTrigger(animTrigger);
|
|
}
|
|
}
|
|
|
|
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 (!IsLocalPlayer) 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 (!IsLocalPlayer) return;
|
|
|
|
// 디버그: _mainCamera 상태 확인
|
|
if (_mainCamera == null)
|
|
{
|
|
_mainCamera = Camera.main;
|
|
}
|
|
|
|
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 == LocalPlayerId)
|
|
{
|
|
prompt += " (워커 할당 가능)";
|
|
}
|
|
|
|
if (_isInteracting)
|
|
{
|
|
prompt += " (진행 중...)";
|
|
style.normal.textColor = Color.red;
|
|
}
|
|
}
|
|
|
|
if (assignedWorker != null && assignedWorker.OwnerPlayerId == LocalPlayerId)
|
|
{
|
|
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()
|
|
{
|
|
// 입력 정리는 NetworkPlayerController에서 담당
|
|
}
|
|
}
|
|
} |