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; // 현재 강조 중인 블록 저장 (legacy) private MineableBlock _lastHighlightedBlock; // legacy block targeting // Chunk-based targeting (new system) private MineableChunk _lastHighlightedChunk; private int _lastHighlightedChunkBlockIndex = -1; private ChunkInteractionHandler.ChunkHitResult _currentChunkTarget; 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; private bool _isHoldingAction = false; private bool _hasExecutedOnce = 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(); // 탭 상호작용 // started: 버튼을 누르는 순간 즉시 첫 번째 시도 _inputActions.Player.Action.started += ctx => { _isHoldingAction = true; _hasExecutedOnce = false; // 누르기 시작할 때 초기화 TryExecuteAction(); }; // canceled: 버튼을 떼는 순간 _inputActions.Player.Action.canceled += ctx => { _isHoldingAction = 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(); } // 버튼을 꾹 누르고 있고, 아직 액션이 진행 중이 아닐 때만 반복 체크 if (_isHoldingAction && !_actionHandler.IsBusy) { HandleContinuousAction(); } } // --- 이동 관련 로직 (기존 유지) --- 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. Action (Left Click) - executes even without target private void OnActionInput() { if (!IsOwner || _actionHandler.IsBusy) return; // Don't perform actions when in build mode if (BuildManager.Instance != null && BuildManager.Instance.IsBuildMode) return; ItemData selectedItem = _inventory.GetSelectedItemData(); if (selectedItem == null) return; // Check if item has behavior (new system) if (selectedItem.behavior != null) { // Get target - prioritize chunk system over legacy blocks GameObject target = GetCurrentMiningTarget(); // Use the new behavior system if (selectedItem.CanUse(gameObject, target)) { // Get action descriptor and perform action var actionDesc = selectedItem.GetUseAction(); if (actionDesc != null) { _actionHandler.PerformAction( CreateActionDataFromDescriptor(actionDesc, selectedItem.behavior), target ); } } } } // Helper to bridge between new ActionDescriptor and legacy PlayerActionData private PlayerActionData CreateActionDataFromDescriptor(ActionDescriptor desc, ItemBehavior behavior) { // Create a temporary runtime action data var actionData = ScriptableObject.CreateInstance(); actionData.actionName = desc.ActionName; actionData.duration = desc.Duration; actionData.animTrigger = desc.AnimTrigger; actionData.impactDelay = desc.ImpactDelay; actionData.baseSpeed = desc.AnimSpeed; actionData.canRepeat = desc.CanRepeat; actionData.behavior = behavior; return actionData; } // 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)) { // Use IDamageable interface instead of MineableBlock directly if (target.TryGetComponent(out var damageable)) { // Server-side distance validation before applying damage if (Vector3.Distance(transform.position, target.transform.position) <= attackRange + 1.0f) { damageable.TakeDamage(new DamageInfo(miningDamage, DamageType.Mining, gameObject)); } } } } [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; // Use direct raycast from camera through crosshair position // Use longer range (100m) from camera to catch all distances Ray aimRay = Camera.main.ScreenPointToRay(_crosshairRect.position); RaycastHit blockHit; bool hasTarget = Physics.SphereCast(aimRay, aimRadius, out blockHit, 100f, mineableLayer); // Filter by actual attack range from player if (hasTarget) { Vector3 playerPos = transform.position + Vector3.up * 1.2f; float distanceFromPlayer = Vector3.Distance(playerPos, blockHit.point); if (distanceFromPlayer > attackRange) { hasTarget = false; // Too far from player to interact with } } // 4. 하이라이트 대상 업데이트 - 청크 시스템과 레거시 블록 모두 지원 MineableBlock currentLegacyTarget = null; MineableChunk currentChunk = null; int currentChunkBlockIndex = -1; if (hasTarget) { // Try chunk first (new system) var chunkHit = ChunkInteractionHandler.GetChunkHit(blockHit); if (chunkHit.hasHit) { currentChunk = chunkHit.chunk; currentChunkBlockIndex = chunkHit.blockIndex; _currentChunkTarget = chunkHit; } else { // Fallback to legacy MineableBlock currentLegacyTarget = blockHit.collider.GetComponentInParent(); _currentChunkTarget = ChunkInteractionHandler.ChunkHitResult.None; } } else { _currentChunkTarget = ChunkInteractionHandler.ChunkHitResult.None; } // Update chunk highlight bool chunkTargetChanged = (currentChunk != _lastHighlightedChunk) || (currentChunkBlockIndex != _lastHighlightedChunkBlockIndex); if (chunkTargetChanged) { if (_lastHighlightedChunk != null) _lastHighlightedChunk.SetHighlight(false); if (currentChunk != null) currentChunk.SetHighlight(true, currentChunkBlockIndex); _lastHighlightedChunk = currentChunk; _lastHighlightedChunkBlockIndex = currentChunkBlockIndex; } // Update legacy block highlight if (_lastHighlightedBlock != currentLegacyTarget) { if (_lastHighlightedBlock != null) _lastHighlightedBlock.SetHighlight(false); if (currentLegacyTarget != null) currentLegacyTarget.SetHighlight(true); _lastHighlightedBlock = currentLegacyTarget; } // 기즈모 디버그 데이터 동기화 Ray debugRay = Camera.main.ScreenPointToRay(_crosshairRect.position); _debugOrigin = debugRay.origin; _debugDir = debugRay.direction; _debugHit = hasTarget && (currentChunk != null || currentLegacyTarget != null); _debugDist = hasTarget ? blockHit.distance : attackRange; // 크로스헤어 이미지 교체 bool hasValidTarget = currentChunk != null || currentLegacyTarget != null; if (crosshairUI != null) { crosshairUI.sprite = hasValidTarget ? targetCrosshair : idleCrosshair; crosshairUI.color = hasValidTarget ? 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() { // Use FogOfWarManager's revealRadius if available, fallback to visionRadius float currentRevealRadius = visionRadius; if (FogOfWarManager.Instance != null) { currentRevealRadius = FogOfWarManager.Instance.revealRadius; } // 시야 반경 내의 블록 감지 Collider[] hitBlocks = Physics.OverlapSphere(transform.position, currentRevealRadius, mineableLayer); foreach (var col in hitBlocks) { // Try chunk-based reveal first (new system) if (col.TryGetComponent(out var chunk)) { // Update local visibility (for fog of war visual states) chunk.UpdateLocalVisibility(transform.position, currentRevealRadius); // Request server to mark blocks as discovered (permanent) if (IsOwner) { RequestChunkRevealServerRpc(chunk.GetComponent().NetworkObjectId, transform.position, currentRevealRadius); } continue; } // Fallback to legacy MineableBlock 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로 변경 } } } [ServerRpc] private void RequestChunkRevealServerRpc(ulong chunkNetId, Vector3 playerPos, float radius) { if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(chunkNetId, out var netObj)) { if (netObj.TryGetComponent(out var chunk)) { chunk.RevealBlocksInRadius(playerPos, radius); } } } 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 HandleContinuousAction() { ItemData selectedItem = _inventory.GetSelectedItemData(); if (selectedItem == null || selectedItem.behavior == null) return; // Only repeat if action supports it var actionDesc = selectedItem.GetUseAction(); if (actionDesc != null && actionDesc.CanRepeat) { TryExecuteAction(); } } private void TryExecuteAction() { if (_actionHandler.IsBusy) return; // Don't perform actions when in build mode if (BuildManager.Instance != null && BuildManager.Instance.IsBuildMode) return; ItemData selectedItem = _inventory.GetSelectedItemData(); if (selectedItem == null || selectedItem.behavior == null) return; var actionDesc = selectedItem.GetUseAction(); if (actionDesc == null) return; // Skip if non-repeatable action already executed once if (!actionDesc.CanRepeat && _hasExecutedOnce) return; // Get target - prioritize chunk system over legacy blocks GameObject target = GetCurrentMiningTarget(); if (selectedItem.CanUse(gameObject, target)) { _actionHandler.PerformAction( CreateActionDataFromDescriptor(actionDesc, selectedItem.behavior), target ); _hasExecutedOnce = true; } } /// /// Get the current mining target (chunk or legacy block) /// private GameObject GetCurrentMiningTarget() { // Prioritize chunk target if (_currentChunkTarget.hasHit && _currentChunkTarget.chunk != null) { return _currentChunkTarget.chunk.gameObject; } // Fallback to legacy block return _lastHighlightedBlock != null ? _lastHighlightedBlock.gameObject : null; } /// /// Get the current chunk target info (for MiningBehavior) /// public ChunkInteractionHandler.ChunkHitResult GetCurrentChunkTarget() { return _currentChunkTarget; } 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); } }