using UnityEngine; using UnityEngine.InputSystem; using Unity.Netcode; using System; using Unity.Cinemachine; using System.Collections; [RequireComponent(typeof(NetworkObject))] public class PlayerNetworkController : NetworkBehaviour { // ... 기존 변수들 유지 ... [Header("Movement Settings")] public float moveSpeed = 5f; public float rotationSpeed = 10f; public float jumpHeight = 1.5f; public float gravity = -19.62f; [Header("Interaction Settings")] [SerializeField] private float interactRange = 3f; [SerializeField] private LayerMask interactableLayer; [SerializeField] private LayerMask constructionLayer; [SerializeField] private float buildSpeedMultiplier = 2f; [SerializeField] private LayerMask itemLayer; // DroppedItem 레이어 설정 [Header("Mining Settings")] [SerializeField] private float attackRange = 3.5f; [SerializeField] private float aimRadius = 0.5f; [SerializeField] private int miningDamage = 50; [SerializeField] private LayerMask mineableLayer; [SerializeField] private LayerMask ignoreDuringAim; // 반드시 'Player' 레이어를 포함하세요! [Header("Visual Feedback")] [SerializeField] private float crosshairScreenRadius = 200f; [SerializeField] private UnityEngine.UI.Image crosshairUI; [SerializeField] private Sprite idleCrosshair; [SerializeField] private Sprite targetCrosshair; [Header("Fog of War Settings")] [SerializeField] private float visionRadius = 5f; // 시야 반경 private float _lastRevealTime; [Header("Inventory & Action")] private PlayerInventory _inventory; // 인벤토리 참조 추가 private PlayerActionHandler _actionHandler; private RectTransform _crosshairRect; private MineableBlock _currentTargetBlock; // 현재 강조 중인 블록 저장 private MineableBlock _lastHighlightedBlock; private CharacterController _controller; private PlayerInputActions _inputActions; private Animator _animator; private TunnelTraveler _traveler; private Vector2 _moveInput; private Vector3 _velocity; private Vector3 _currentMoveDir; private bool _isGrounded; private bool _isHoldingInteract = false; // 현재 플레이어가 어떤 행동을 하고 있는지 나타내는 상태 public enum ActionState { Idle, Busy } private ActionState _currentState = ActionState.Idle; public bool IsBusy => _currentState == ActionState.Busy; // 디버그 변수 private Vector3 _debugOrigin; private Vector3 _debugDir; private bool _debugHit; private float _debugDist; // NGO 초기화 public override void OnNetworkSpawn() { if (!IsOwner) return; // 1. 씬에 있는 가상 카메라를 찾습니다. // Unity 6에서는 CinemachineVirtualCamera 대신 CinemachineCamera를 주로 사용합니다. var vcam = GameObject.FindAnyObjectByType(); if (vcam != null) { // 2. 카메라의 Follow와 LookAt 대상을 '나'로 설정합니다. vcam.Follow = transform; vcam.LookAt = transform; Debug.Log("[Camera] 로컬 플레이어에게 카메라가 연결되었습니다."); } // 씬의 Canvas 안에 있는 "Crosshair"라는 이름의 오브젝트를 찾습니다. GameObject crosshairObj = GameObject.Find("Crosshair"); if (crosshairObj != null) { _crosshairRect = crosshairObj.GetComponent(); crosshairUI = crosshairObj.GetComponent(); // 초기 스프라이트 설정 crosshairUI.sprite = idleCrosshair; } _inputActions = new PlayerInputActions(); _inputActions.Player.Jump.performed += ctx => OnJump(); _inputActions.Player.Action.performed += ctx => OnActionInput(); _inputActions.Player.Interact.performed += ctx => OnInteractTap(); // 탭 상호작용 _inputActions.Player.Interact.started += ctx => _isHoldingInteract = true; _inputActions.Player.Interact.canceled += ctx => _isHoldingInteract = false; _inputActions.Player.Select1.performed += ctx => _inventory.ChangeSelectedSlotRpc(0); _inputActions.Player.Select2.performed += ctx => _inventory.ChangeSelectedSlotRpc(1); _inputActions.Player.Select3.performed += ctx => _inventory.ChangeSelectedSlotRpc(2); _inputActions.Player.Select4.performed += ctx => _inventory.ChangeSelectedSlotRpc(3); _inputActions.Player.Select5.performed += ctx => _inventory.ChangeSelectedSlotRpc(4); _inputActions.Player.Select6.performed += ctx => _inventory.ChangeSelectedSlotRpc(5); _inputActions.Enable(); } void Awake() { _controller = GetComponent(); _animator = GetComponent(); _traveler = GetComponent(); // --- 참조 초기화 추가 --- _inventory = GetComponent(); _actionHandler = GetComponent(); if (_inventory == null) Debug.LogError("PlayerInventory 컴포넌트를 찾을 수 없습니다!"); } void Update() { if (!IsOwner) return; if (_traveler != null && _traveler.IsTraveling) return; HandleGravity(); HandleMovement(); if (_isHoldingInteract) PerformConstructionSupport(); UpdateCrosshairPosition(); // 이 안에서 움직여야 합니다. UpdateTargetFeedback(); // 매 프레임 실행하면 무거우므로 0.2초마다 주변 시야 갱신 if (Time.time - _lastRevealTime > 0.2f) { _lastRevealTime = Time.time; RevealSurroundings(); } } // --- 이동 관련 로직 (기존 유지) --- private void HandleMovement() { _isGrounded = _controller.isGrounded; _animator.SetBool("isGrounded", _isGrounded); bool isAttacking = _animator.GetCurrentAnimatorStateInfo(0).IsTag("Attack"); _moveInput = _inputActions.Player.Move.ReadValue(); Vector3 move = new Vector3(_moveInput.x, 0, _moveInput.y).normalized; if (isAttacking) move = _isGrounded ? Vector3.zero : _currentMoveDir; else if (move.magnitude > 0.1f) _currentMoveDir = move; if (move.magnitude >= 0.1f) { if (!isAttacking) { Quaternion targetRotation = Quaternion.LookRotation(move); transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime); } _controller.Move(move * moveSpeed * Time.deltaTime); } _animator.SetFloat("MoveSpeed", isAttacking && _isGrounded ? 0 : move.magnitude); } private void HandleGravity() { if (_isGrounded && _velocity.y < 0) _velocity.y = -2f; _velocity.y += gravity * Time.deltaTime; _controller.Move(_velocity * Time.deltaTime); } private void OnJump() { if (_isGrounded) _velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity); } // 1. 액션 (좌클릭) - 대상이 없어도 나감 // PlayerNetworkController.cs 중 일부 private void OnActionInput() { if (!IsOwner || _actionHandler.IsBusy) return; ItemData selectedItem = _inventory.GetSelectedItemData(); // 로그 1: 아이템 확인 if (selectedItem == null) { Debug.Log("선택된 아이템이 없음"); return; } // 로그 2: 도구 여부 및 액션 데이터 확인 Debug.Log($"현재 아이템: {selectedItem.itemName}, 도구여부: {selectedItem.isTool}, 액션데이터: {selectedItem.toolAction != null}"); if (selectedItem.isTool && selectedItem.toolAction != null) { if (_lastHighlightedBlock != null) { Debug.Log($"채광 시작: {_lastHighlightedBlock.name}"); _actionHandler.PerformAction(selectedItem.toolAction, _lastHighlightedBlock.gameObject); } else { Debug.Log("조준된 블록이 없음 (하이라이트 확인 필요)"); } } } // 2. 인터랙션 (F키) - 대상이 없으면 아예 시작 안 함 private void OnInteractTap() { if (!IsOwner || _actionHandler.IsBusy) return; IInteractable target = GetClosestInteractable(); if (target != null) { _actionHandler.PerformInteraction(target); } } [Rpc(SendTo.Server)] private void ApplyMiningDamageServerRpc(ulong targetId) { if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(targetId, out var target)) { if (target.TryGetComponent(out var block)) { // 서버에서 최종 거리 검증 후 대미지 적용 if (Vector3.Distance(transform.position, target.transform.position) <= attackRange + 1.0f) { block.TakeDamageRpc(miningDamage); block.PlayHitEffectClientRpc(); } } } } [ClientRpc] private void OnAttackClientRpc() => _animator.SetTrigger("Attack"); // 건설 지원 로직 (범위 내 지속 작업이므로 OverlapSphere 유지 또는 Raycast로 변경 가능) private void PerformConstructionSupport() { Collider[] targets = Physics.OverlapSphere(transform.position, interactRange, constructionLayer); foreach (var col in targets) { ConstructionSite site = col.GetComponentInParent(); if (site != null) { site.AdvanceConstruction(Time.deltaTime * buildSpeedMultiplier); } } } public override void OnNetworkDespawn() { if (IsOwner && _inputActions != null) _inputActions.Disable(); } private void UpdateCrosshairPosition() { // 1. 변수 할당 확인 (할당이 안 되어 있으면 여기서 찾음) if (_crosshairRect == null) { GameObject go = GameObject.Find("Crosshair"); if (go != null) _crosshairRect = go.GetComponent(); else return; // 여전히 없으면 중단 } // 2. 마우스 입력 읽기 (New Input System) Vector2 mousePos = Mouse.current.position.ReadValue(); Vector2 screenCenter = new Vector2(Screen.width / 2f, Screen.height / 2f); // 3. 중앙으로부터의 거리 계산 Vector2 offset = mousePos - screenCenter; // 4. [중요] 반지름 제한 확인 (crosshairScreenRadius가 0이면 이동하지 않음) if (crosshairScreenRadius > 0) { // 내적과 크기 계산을 통해 원형 제한 적용 if (offset.magnitude > crosshairScreenRadius) { offset = offset.normalized * crosshairScreenRadius; } } // 5. UI 좌표 적용 (Screen Space - Overlay 기준) _crosshairRect.position = screenCenter + offset; } private void UpdateTargetFeedback() { if (!IsOwner || _crosshairRect == null) return; // 1. 카메라 레이로 조준점 계산 (플레이어 몸통 무시) Ray aimRay = Camera.main.ScreenPointToRay(_crosshairRect.position); Vector3 worldAimPoint; if (Physics.Raycast(aimRay, out RaycastHit mouseHit, 100f, ~ignoreDuringAim)) worldAimPoint = mouseHit.point; else worldAimPoint = aimRay.GetPoint(50f); // 2. 캐릭터 가슴에서 조준점을 향하는 방향 계산 Vector3 origin = transform.position + Vector3.up * 1.2f; Vector3 direction = (worldAimPoint - origin).normalized; // 자기 자신 충돌 방지용 오프셋 Vector3 rayStart = origin + direction * 0.4f; // 3. [중요] 실제 공격과 동일한 SphereCast 실행 RaycastHit blockHit; bool hasTarget = Physics.SphereCast(rayStart, aimRadius, direction, out blockHit, attackRange - 0.4f, mineableLayer); // 4. 하이라이트 대상 업데이트 MineableBlock currentTarget = null; if (hasTarget) { currentTarget = blockHit.collider.GetComponentInParent(); } // 대상이 바뀌었을 때만 아웃라인 갱신 (최적화) if (_lastHighlightedBlock != currentTarget) { if (_lastHighlightedBlock != null) _lastHighlightedBlock.SetHighlight(false); if (currentTarget != null) currentTarget.SetHighlight(true); _lastHighlightedBlock = currentTarget; } // 기즈모 디버그 데이터 동기화 _debugOrigin = rayStart; _debugDir = direction; _debugHit = hasTarget; _debugDist = hasTarget ? blockHit.distance : (attackRange - 0.4f); // 크로스헤어 이미지 교체 if (crosshairUI != null) { crosshairUI.sprite = hasTarget ? targetCrosshair : idleCrosshair; crosshairUI.color = hasTarget ? Color.green : Color.white; } } private void UpdateBlockVisuals(bool hasTarget, RaycastHit hit) { MineableBlock newTarget = null; if (hasTarget) { // 부모나 자신에게서 MineableBlock 컴포넌트를 찾습니다. newTarget = hit.collider.GetComponentInParent(); } // 대상이 바뀌었을 때만 실행 (최적화) if (_currentTargetBlock != newTarget) { // 1. 이전 타겟 하이라이트 해제 if (_currentTargetBlock != null) { _currentTargetBlock.SetHighlight(false); } // 2. 새로운 타겟 하이라이트 적용 if (newTarget != null) { newTarget.SetHighlight(true); } _currentTargetBlock = newTarget; } // 3. 크로스헤어 상태 업데이트 if (crosshairUI != null) { crosshairUI.sprite = hasTarget ? targetCrosshair : idleCrosshair; crosshairUI.color = hasTarget ? Color.green : Color.white; } } private void RevealSurroundings() { // 시야 반경 내의 블록 감지 Collider[] hitBlocks = Physics.OverlapSphere(transform.position, visionRadius, mineableLayer); foreach (var col in hitBlocks) { if (col.TryGetComponent(out var block)) { // 1. [로컬] 내 화면에서 이 블록을 보이게 함 (실시간 시야) block.UpdateLocalVisibility(); // 2. [서버] 아직 발견 안 된 블록이라면 서버에 알림 (공유 안개 해제) if (IsOwner) { RequestRevealServerRpc(block.GetComponent().NetworkObjectId); } } } } [ServerRpc] private void RequestRevealServerRpc(ulong blockNetId) { if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(blockNetId, out var netObj)) { if (netObj.TryGetComponent(out var block)) { block.RevealBlock(); // 서버가 블록 상태를 true로 변경 } } } private IEnumerator ActionRoutine(float duration, string animTrigger, Action actionLogic) { // 1. 상태 잠금 _currentState = ActionState.Busy; // 2. 애니메이션 실행 if (!string.IsNullOrEmpty(animTrigger)) { _animator.SetTrigger(animTrigger); } // 3. 실제 로직 실행 (상호작용, 아이템 사용 등) actionLogic?.Invoke(); // 4. 지정된 시간만큼 대기 yield return new WaitForSeconds(duration); // 5. 상태 해제 _currentState = ActionState.Idle; } private IInteractable GetClosestInteractable() { // 1. 지정된 레이어 내의 콜라이더 탐색 Collider[] colliders = Physics.OverlapSphere(transform.position, interactRange, interactableLayer); IInteractable closest = null; float minDistance = float.MaxValue; foreach (var col in colliders) { // 2. IInteractable 인터페이스를 가지고 있는지 확인 (부모 포함) IInteractable interactable = col.GetComponentInParent(); if (interactable != null) { float dist = Vector3.Distance(transform.position, col.transform.position); if (dist < minDistance) { minDistance = dist; closest = interactable; } } } return closest; } private void OnDrawGizmos() { if (!Application.isPlaying || !IsOwner) return; // 실제 채굴 탐색 궤적을 씬 뷰에 표시 Gizmos.color = _debugHit ? Color.red : Color.green; // 광선 표시 Gizmos.DrawLine(_debugOrigin, _debugOrigin + _debugDir * _debugDist); // 탐색 영역(구체) 표시 Gizmos.DrawWireSphere(_debugOrigin + _debugDir * _debugDist, aimRadius); } }