- CharacterController.enableOverlapRecovery 비활성화로 자동 밀어냄 제거 - 레이어 마스크 의존 제거, 컴포넌트(NavMeshAgent/CharacterController)로 식별 - EnemyBase LateUpdate에서 velocity 기반 보스 위치 보정 - EnemyBase OnAnimatorMove에서 루트모션의 플레이어 방향 이동 차단 - BossEnemy Update를 OnServerUpdate 패턴으로 리팩터링 - 보스 프리팹 하위 오브젝트 레이어 Enemy로 통일 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
293 lines
10 KiB
C#
293 lines
10 KiB
C#
using UnityEngine;
|
|
using UnityEngine.InputSystem;
|
|
using Unity.Netcode;
|
|
using Colosseum.Network;
|
|
using Colosseum.Skills;
|
|
|
|
namespace Colosseum.Player
|
|
{
|
|
/// <summary>
|
|
/// 3인칭 플레이어 이동 (네트워크 동기화)
|
|
/// </summary>
|
|
[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<CharacterController>();
|
|
// 보스 콜라이더가 겹칠 때 Unity 내부 자동 밀어냄 비활성화.
|
|
// 적과의 분리는 EnemyBase.ResolvePlayerOverlap에서 보스 측이 담당.
|
|
characterController.enableOverlapRecovery = false;
|
|
|
|
if (skillController == null)
|
|
skillController = GetComponent<SkillController>();
|
|
|
|
if (animator == null)
|
|
animator = GetComponentInChildren<Animator>();
|
|
|
|
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<Vector2>();
|
|
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<PlayerCamera>();
|
|
if (cameraController == null)
|
|
cameraController = gameObject.AddComponent<PlayerCamera>();
|
|
cameraController.Initialize(transform, inputActions);
|
|
}
|
|
|
|
public void RefreshCamera() => SetupCamera();
|
|
|
|
private void Update()
|
|
{
|
|
if (!IsOwner) return;
|
|
|
|
ApplyGravity();
|
|
UpdateBlockedDirection();
|
|
Move();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 매 프레임 주변 적을 능동적으로 감지하여 blockedDirection을 설정합니다.
|
|
/// 콜백 기반이 아니므로 보스가 플레이어 쪽으로 밀고 올 때도 즉시 감지합니다.
|
|
/// </summary>
|
|
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<UnityEngine.AI.NavMeshAgent>(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<PlayerAnimationController>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 루트 모션 처리. 스킬 애니메이션 중 애니메이션의 이동/회전 데이터를 적용합니다.
|
|
/// 적 방향으로의 이동은 취소합니다.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|