using UnityEngine; using UnityEngine.InputSystem; using Unity.Netcode; using Colosseum.Network; using Colosseum.Skills; namespace Colosseum.Player { /// /// 서버 권한 이동. /// - 오너(클라이언트/호스트): 입력 수집 → NetworkVariable에 월드 방향 기록 /// - 서버: NetworkVariable을 읽어 CharacterController 구동 → NetworkTransform으로 동기화 /// [RequireComponent(typeof(CharacterController))] [RequireComponent(typeof(PlayerActionState))] 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; [SerializeField] private PlayerActionState actionState; private CharacterController characterController; private Vector3 velocity; private Vector2 moveInput; // 로컬 원시 입력 (IsOwner 전용) private InputSystem_Actions inputActions; private bool isJumping; private bool wasGrounded; // 클라이언트가 기록, 서버가 소비하는 월드 스페이스 이동 방향 private NetworkVariable netMoveInput = new NetworkVariable( Vector2.zero, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); // 적 충돌 차단용 private Vector3 blockedDirection; private readonly Collider[] overlapBuffer = new Collider[8]; public float CurrentMoveSpeed => netMoveInput.Value.magnitude * moveSpeed * GetMoveSpeedMultiplier(); public bool IsGrounded => characterController != null ? characterController.isGrounded : false; public bool IsJumping => isJumping; public override void OnNetworkSpawn() { Debug.Log($"[PlayerMovement] LOCAL OnNetworkSpawn: OwnerClientId={OwnerClientId}, IsOwner={IsOwner}, IsServer={IsServer}, LocalClientId={NetworkManager.LocalClientId}"); ReportSpawnRpc(IsOwner, IsServer, IsLocalPlayer, NetworkManager.LocalClientId); // 서버: 모든 플레이어의 이동 처리 담당 if (IsServer) { characterController = GetComponent(); characterController.enableOverlapRecovery = false; if (skillController == null) skillController = GetComponent(); if (animator == null) animator = GetComponentInChildren(); if (actionState == null) actionState = GetOrCreateActionState(); SetSpawnPosition(); } // 오너: 입력 및 카메라 초기화 if (IsOwner) { if (actionState == null) actionState = GetOrCreateActionState(); InitializeInputActions(); SetupCamera(); } // 서버도 오너도 아닌 클라이언트: 비활성화 if (!IsOwner && !IsServer) enabled = false; } [Rpc(SendTo.Server)] private void ReportSpawnRpc(bool isOwner, bool isServer, bool isLocalPlayer, ulong localClientId) { Debug.Log($"[PlayerMovement] SPAWN REPORT: OwnerClientId={OwnerClientId}, IsOwner={isOwner}, IsServer={isServer}, IsLocalPlayer={isLocalPlayer}, LocalClientId={localClientId}"); } 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 (!IsOwner) return; if (actionState != null && !actionState.CanJump) return; JumpRequestRpc(); } /// /// 클라이언트가 점프 요청 → 서버가 검증 후 실행 /// [Rpc(SendTo.Server)] private void JumpRequestRpc() { if (actionState != null && !actionState.CanJump) return; if (!isJumping && characterController != null && 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() { // 오너: 카메라 기준 이동 방향을 NetworkVariable에 기록 if (IsOwner) UpdateNetworkInput(); // 서버: NetworkVariable을 읽어 실제 이동 처리 if (IsServer) { ApplyGravity(); UpdateBlockedDirection(); Move(); } } /// /// 로컬 입력을 카메라 기준 월드 방향으로 변환해 NetworkVariable에 기록 /// private void UpdateNetworkInput() { if (actionState != null && !actionState.CanMove) { if (netMoveInput.Value != Vector2.zero) netMoveInput.Value = Vector2.zero; return; } Vector3 dir = new Vector3(moveInput.x, 0f, moveInput.y); if (dir.sqrMagnitude > 0.001f) dir = TransformDirectionByCamera(dir).normalized; Vector2 worldDir = new Vector2(dir.x, dir.z); if (netMoveInput.Value != worldDir) netMoveInput.Value = worldDir; } /// /// 매 프레임 주변 적을 감지하여 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); 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; if (skillController != null && skillController.IsPlayingAnimation) { if (!skillController.UsesRootMotion) characterController.Move(velocity * Time.deltaTime); return; } // 클라이언트가 전송한 월드 스페이스 방향 사용 Vector3 moveDirection = new Vector3(netMoveInput.Value.x, 0f, netMoveInput.Value.y); if (moveDirection.sqrMagnitude > 0.001f) moveDirection.Normalize(); if (actionState != null && !actionState.CanMove) moveDirection = Vector3.zero; if (blockedDirection != Vector3.zero && Vector3.Dot(moveDirection, blockedDirection) > 0f) moveDirection = Vector3.zero; float actualMoveSpeed = moveSpeed * GetMoveSpeedMultiplier(); characterController.Move((moveDirection * actualMoveSpeed + 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 float GetMoveSpeedMultiplier() { if (actionState == null) return 1f; return actionState.MoveSpeedMultiplier; } private PlayerActionState GetOrCreateActionState() { var foundState = GetComponent(); if (foundState != null) return foundState; return gameObject.AddComponent(); } /// /// 루트 모션 처리 (서버 전용 — NetworkTransform으로 동기화) /// private void OnAnimatorMove() { if (!IsServer) 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; } } }