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 gameplayInputEnabled = true;
private bool isJumping;
private bool wasGrounded;
private Vector3 forcedMovementVelocity;
private float forcedMovementRemaining;
// 클라이언트가 기록, 서버가 소비하는 월드 스페이스 이동 방향
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 bool IsForcedMoving => forcedMovementRemaining > 0f && forcedMovementVelocity.sqrMagnitude > 0.0001f;
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.Move.performed += OnMovePerformed;
inputActions.Player.Move.canceled += OnMoveCanceled;
inputActions.Player.Jump.performed += OnJumpPerformed;
SetGameplayInputEnabled(true);
}
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;
ClearForcedMovement();
}
private void OnEnable()
{
if (IsOwner && inputActions != null)
{
SetGameplayInputEnabled(gameplayInputEnabled);
}
}
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();
ClearForcedMovement();
}
private void OnMovePerformed(InputAction.CallbackContext context) => moveInput = context.ReadValue();
private void OnMoveCanceled(InputAction.CallbackContext context) => moveInput = Vector2.zero;
///
/// 로컬 플레이어의 전투 입력을 일시적으로 차단하거나 복구합니다.
///
public void SetGameplayInputEnabled(bool enabled)
{
gameplayInputEnabled = enabled;
if (!IsOwner || inputActions == null)
return;
if (enabled)
{
inputActions.Player.Enable();
return;
}
moveInput = Vector2.zero;
if (netMoveInput.Value != Vector2.zero)
netMoveInput.Value = Vector2.zero;
inputActions.Player.Disable();
}
private void OnJumpPerformed(InputAction.CallbackContext context)
{
if (!IsOwner) return;
if (!gameplayInputEnabled) 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;
Vector3 forcedDelta = ConsumeForcedMovementDelta(Time.deltaTime);
if (skillController != null && skillController.IsPlayingAnimation)
{
if (!skillController.UsesRootMotion)
characterController.Move(velocity * Time.deltaTime + forcedDelta);
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 + forcedDelta);
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;
}
///
/// 넉백처럼 입력과 무관한 강제 이동을 적용합니다.
///
public void ApplyForcedMovement(Vector3 worldVelocity, float duration)
{
if (!IsServer)
return;
if (duration <= 0f || worldVelocity.sqrMagnitude <= 0.0001f)
{
ClearForcedMovement();
return;
}
forcedMovementVelocity = worldVelocity;
forcedMovementRemaining = duration;
}
///
/// 강제 이동 상태를 즉시 초기화합니다.
///
public void ClearForcedMovement()
{
forcedMovementVelocity = Vector3.zero;
forcedMovementRemaining = 0f;
}
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;
Vector3 forcedDelta = ConsumeForcedMovementDelta(Time.deltaTime);
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 + forcedDelta);
}
else
{
characterController.Move(deltaPosition + forcedDelta);
}
if (animator.deltaRotation != Quaternion.identity)
transform.rotation *= animator.deltaRotation;
if (!wasGrounded && characterController.isGrounded && isJumping)
OnJumpEnd();
wasGrounded = characterController.isGrounded;
}
private Vector3 ConsumeForcedMovementDelta(float deltaTime)
{
if (forcedMovementRemaining <= 0f || forcedMovementVelocity.sqrMagnitude <= 0.0001f)
return Vector3.zero;
float appliedDeltaTime = Mathf.Min(deltaTime, forcedMovementRemaining);
forcedMovementRemaining = Mathf.Max(0f, forcedMovementRemaining - appliedDeltaTime);
Vector3 delta = forcedMovementVelocity * appliedDeltaTime;
if (forcedMovementRemaining <= 0f)
{
forcedMovementVelocity = Vector3.zero;
}
return delta;
}
}
}