using UnityEngine; using UnityEngine.InputSystem; using Unity.Netcode; using Colosseum.Network; using Colosseum.Skills; namespace Colosseum.Player { /// /// 3인칭 플레이어 이동 (네트워크 동기화) /// [RequireComponent(typeof(CharacterController))] public class PlayerMovement : NetworkBehaviour { [Header("Movement Settings")] [SerializeField] private float moveSpeed = 5f; [SerializeField] private float rotationSpeed = 10f; [SerializeField] private float gravity = -9.81f; [Header("Jump Settings")] [SerializeField] private float jumpForce = 5f; [Header("References")] [SerializeField] private SkillController skillController; [SerializeField] private Animator animator; private CharacterController characterController; private Vector3 velocity; private Vector2 moveInput; private InputSystem_Actions inputActions; private bool isJumping; private bool wasGrounded; // 적 충돌 차단용 private Vector3 blockedDirection; private readonly Collider[] overlapBuffer = new Collider[8]; public float CurrentMoveSpeed => moveInput.magnitude * moveSpeed; public bool IsGrounded => characterController != null ? characterController.isGrounded : false; public bool IsJumping => isJumping; public override void OnNetworkSpawn() { if (!IsOwner) { enabled = false; return; } characterController = GetComponent(); // 보스 콜라이더가 겹칠 때 Unity 내부 자동 밀어냄 비활성화. // 적과의 분리는 EnemyBase.ResolvePlayerOverlap에서 보스 측이 담당. characterController.enableOverlapRecovery = false; if (skillController == null) skillController = GetComponent(); if (animator == null) animator = GetComponentInChildren(); SetSpawnPosition(); InitializeInputActions(); SetupCamera(); } private void InitializeInputActions() { inputActions = new InputSystem_Actions(); inputActions.Player.Enable(); inputActions.Player.Move.performed += OnMovePerformed; inputActions.Player.Move.canceled += OnMoveCanceled; inputActions.Player.Jump.performed += OnJumpPerformed; } private void CleanupInputActions() { if (inputActions != null) { inputActions.Player.Move.performed -= OnMovePerformed; inputActions.Player.Move.canceled -= OnMoveCanceled; inputActions.Player.Jump.performed -= OnJumpPerformed; inputActions.Player.Disable(); } } private void OnDisable() { CleanupInputActions(); moveInput = Vector2.zero; } private void OnEnable() { if (IsOwner && inputActions != null) { inputActions.Player.Enable(); inputActions.Player.Move.performed += OnMovePerformed; inputActions.Player.Move.canceled += OnMoveCanceled; inputActions.Player.Jump.performed += OnJumpPerformed; } } private void SetSpawnPosition() { Transform spawnPoint = PlayerSpawnPoint.GetRandomSpawnPoint(); if (spawnPoint != null) { characterController.enabled = false; transform.position = spawnPoint.position; transform.rotation = spawnPoint.rotation; characterController.enabled = true; } } public override void OnNetworkDespawn() { CleanupInputActions(); } private void OnMovePerformed(InputAction.CallbackContext context) => moveInput = context.ReadValue(); private void OnMoveCanceled(InputAction.CallbackContext context) => moveInput = Vector2.zero; private void OnJumpPerformed(InputAction.CallbackContext context) { if (!isJumping && characterController.isGrounded) Jump(); } private void SetupCamera() { var cameraController = GetComponent(); if (cameraController == null) cameraController = gameObject.AddComponent(); cameraController.Initialize(transform, inputActions); } public void RefreshCamera() => SetupCamera(); private void Update() { if (!IsOwner) return; ApplyGravity(); UpdateBlockedDirection(); Move(); } /// /// 매 프레임 주변 적을 능동적으로 감지하여 blockedDirection을 설정합니다. /// 콜백 기반이 아니므로 보스가 플레이어 쪽으로 밀고 올 때도 즉시 감지합니다. /// private void UpdateBlockedDirection() { blockedDirection = Vector3.zero; Vector3 center = transform.position + characterController.center; float radius = characterController.radius + 0.15f; float halfHeight = Mathf.Max(0f, characterController.height * 0.5f - characterController.radius); // 레이어 무관하게 NavMeshAgent 유무로 적 식별 int count = Physics.OverlapCapsuleNonAlloc( center + Vector3.up * halfHeight, center - Vector3.up * halfHeight, radius, overlapBuffer); for (int i = 0; i < count; i++) { if (overlapBuffer[i].gameObject == gameObject) continue; if (!overlapBuffer[i].TryGetComponent(out _)) continue; Vector3 toEnemy = overlapBuffer[i].transform.position - transform.position; toEnemy.y = 0f; if (toEnemy.sqrMagnitude > 0.0001f) { blockedDirection = toEnemy.normalized; break; } } } private void ApplyGravity() { if (wasGrounded && velocity.y < 0) velocity.y = -2f; else velocity.y += gravity * Time.deltaTime; } private void Move() { if (characterController == null) return; // 스킬 애니메이션 재생 중에는 이동 불가 (루트 모션은 OnAnimatorMove에서 처리) if (skillController != null && skillController.IsPlayingAnimation) { if (!skillController.UsesRootMotion) characterController.Move(velocity * Time.deltaTime); return; } Vector3 moveDirection = new Vector3(moveInput.x, 0f, moveInput.y); moveDirection = TransformDirectionByCamera(moveDirection); moveDirection.Normalize(); // 적 방향으로 이동 시도 중이면 수평 이동 전체 취소 if (blockedDirection != Vector3.zero && Vector3.Dot(moveDirection, blockedDirection) > 0f) moveDirection = Vector3.zero; characterController.Move((moveDirection * moveSpeed + velocity) * Time.deltaTime); if (moveDirection != Vector3.zero) { Quaternion targetRotation = Quaternion.LookRotation(moveDirection); transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime); } if (!wasGrounded && characterController.isGrounded && isJumping) OnJumpEnd(); wasGrounded = characterController.isGrounded; } private void Jump() { isJumping = true; velocity.y = jumpForce; var animController = GetComponent(); if (animController != null) animController.PlayJump(); } public void OnJumpEnd() => isJumping = false; private Vector3 TransformDirectionByCamera(Vector3 direction) { if (Camera.main == null) return direction; Transform cam = Camera.main.transform; Vector3 forward = new Vector3(cam.forward.x, 0f, cam.forward.z).normalized; Vector3 right = new Vector3(cam.right.x, 0f, cam.right.z).normalized; return right * direction.x + forward * direction.z; } /// /// 루트 모션 처리. 스킬 애니메이션 중 애니메이션의 이동/회전 데이터를 적용합니다. /// 적 방향으로의 이동은 취소합니다. /// private void OnAnimatorMove() { if (!IsOwner) return; if (animator == null || characterController == null) return; if (skillController == null || !skillController.IsPlayingAnimation) return; if (!skillController.UsesRootMotion) return; Vector3 deltaPosition = animator.deltaPosition; // 적 방향으로 루트 모션이 향하면 수평 이동 취소 if (blockedDirection != Vector3.zero) { Vector3 deltaXZ = new Vector3(deltaPosition.x, 0f, deltaPosition.z); if (Vector3.Dot(deltaXZ, blockedDirection) > 0f) { deltaPosition.x = 0f; deltaPosition.z = 0f; } } if (skillController.IgnoreRootMotionY) { deltaPosition.y = 0f; characterController.Move(deltaPosition + velocity * Time.deltaTime); } else { characterController.Move(deltaPosition); } if (animator.deltaRotation != Quaternion.identity) transform.rotation *= animator.deltaRotation; if (!wasGrounded && characterController.isGrounded && isJumping) OnJumpEnd(); wasGrounded = characterController.isGrounded; } } }