- Assets/_Game/ 하위로 게임 에셋 통합 - External/ 패키지 벤더별 분류 (Synty, Animations, UI) - 에셋 네이밍 컨벤션 확립 및 적용 (Data_Skill_, Data_SkillEffect_, Prefab_, Anim_, Model_, BT_ 등) - pre-commit hook으로 네이밍 컨벤션 자동 검사 추가 - RESTRUCTURE_CHECKLIST.md 작성 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
394 lines
12 KiB
C#
394 lines
12 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 int enemyLayerMask;
|
|
|
|
/// <summary>
|
|
/// 현재 이동 속도 (애니메이션용)
|
|
/// </summary>
|
|
public float CurrentMoveSpeed => moveInput.magnitude * moveSpeed;
|
|
|
|
|
|
/// <summary>
|
|
/// 현재 지면 접촉 상태
|
|
/// </summary>
|
|
public bool IsGrounded => characterController != null ? characterController.isGrounded : false;
|
|
|
|
|
|
/// <summary>
|
|
/// 점프 중 상태
|
|
/// </summary>
|
|
public bool IsJumping => isJumping;
|
|
|
|
public override void OnNetworkSpawn()
|
|
{
|
|
// 로컬 플레이어가 아니면 입력 비활성화
|
|
if (!IsOwner)
|
|
{
|
|
enabled = false;
|
|
return;
|
|
}
|
|
|
|
characterController = GetComponent<CharacterController>();
|
|
|
|
// SkillController 참조
|
|
if (skillController == null)
|
|
{
|
|
skillController = GetComponent<SkillController>();
|
|
}
|
|
|
|
// Animator 참조
|
|
if (animator == null)
|
|
{
|
|
animator = GetComponentInChildren<Animator>();
|
|
}
|
|
|
|
// 스폰 포인트에서 위치 설정
|
|
SetSpawnPosition();
|
|
|
|
// Input Actions 초기화
|
|
InitializeInputActions();
|
|
|
|
// 카메라 설정
|
|
SetupCamera();
|
|
|
|
// 적 레이어 마스크 초기화
|
|
enemyLayerMask = LayerMask.GetMask("Enemy");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 입력 액션 초기화
|
|
/// </summary>
|
|
private void InitializeInputActions()
|
|
{
|
|
inputActions = new InputSystem_Actions();
|
|
inputActions.Player.Enable();
|
|
|
|
// Move 액션 콜백 등록
|
|
inputActions.Player.Move.performed += OnMovePerformed;
|
|
inputActions.Player.Move.canceled += OnMoveCanceled;
|
|
|
|
// Jump 액션 콜백 등록
|
|
inputActions.Player.Jump.performed += OnJumpPerformed;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 입력 액션 해제
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 스폰 위치 설정
|
|
/// </summary>
|
|
private void SetSpawnPosition()
|
|
{
|
|
Transform spawnPoint = PlayerSpawnPoint.GetRandomSpawnPoint();
|
|
|
|
if (spawnPoint != null)
|
|
{
|
|
// CharacterController 비활성화 후 위치 설정 (충돌 문제 방지)
|
|
characterController.enabled = false;
|
|
transform.position = spawnPoint.position;
|
|
transform.rotation = spawnPoint.rotation;
|
|
characterController.enabled = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 네트워크 정리
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 카메라 재설정 (씬 로드 후 호출)
|
|
/// </summary>
|
|
public void RefreshCamera()
|
|
{
|
|
SetupCamera();
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (!IsOwner) return;
|
|
|
|
ApplyGravity();
|
|
Move();
|
|
}
|
|
|
|
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)
|
|
{
|
|
float blockedAmount = Vector3.Dot(moveDirection, blockedDirection);
|
|
if (blockedAmount > 0f)
|
|
{
|
|
moveDirection -= blockedDirection * blockedAmount;
|
|
moveDirection.Normalize();
|
|
}
|
|
}
|
|
|
|
// 이동 적용
|
|
Vector3 moveVector = moveDirection * moveSpeed * Time.deltaTime;
|
|
characterController.Move(moveVector + velocity * Time.deltaTime);
|
|
|
|
// 충돌 방향 리셋
|
|
blockedDirection = Vector3.zero;
|
|
|
|
// 회전 (이동 중일 때만)
|
|
if (moveDirection != Vector3.zero)
|
|
{
|
|
Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
|
|
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
|
|
}
|
|
|
|
// 착지 체크 (Move 후에 isGrounded가 업데이트됨)
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 점프 중 상태가 끝나면 IsJumping = false;
|
|
/// </summary>
|
|
public void OnJumpEnd()
|
|
{
|
|
isJumping = false;
|
|
}
|
|
|
|
private Vector3 TransformDirectionByCamera(Vector3 direction)
|
|
{
|
|
if (Camera.main == null) return direction;
|
|
|
|
Transform cameraTransform = Camera.main.transform;
|
|
Vector3 cameraForward = cameraTransform.forward;
|
|
Vector3 cameraRight = cameraTransform.right;
|
|
|
|
// Y축 제거
|
|
cameraForward.y = 0f;
|
|
cameraRight.y = 0f;
|
|
cameraForward.Normalize();
|
|
cameraRight.Normalize();
|
|
|
|
return cameraRight * direction.x + cameraForward * 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;
|
|
|
|
// Y축 무시 설정 시 중력 유지
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// CharacterController 충돌 처리. 적과 충돌 시 해당 방향 이동을 차단합니다.
|
|
/// 충돌 normal을 8방향으로 양자화하여 각진 충돌 느낌을 줍니다.
|
|
/// </summary>
|
|
private void OnControllerColliderHit(ControllerColliderHit hit)
|
|
{
|
|
// 적과의 충돌인지 확인
|
|
if ((enemyLayerMask & (1 << hit.gameObject.layer)) != 0)
|
|
{
|
|
// 충돌 방향 저장 (이동 차단용)
|
|
blockedDirection = hit.normal;
|
|
blockedDirection.y = 0f;
|
|
blockedDirection.Normalize();
|
|
|
|
// 8방향으로 양자화 (45도 간격)
|
|
blockedDirection = QuantizeToOctagon(blockedDirection);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 방향을 8각형(45도 간격) 방향으로 양자화합니다.
|
|
/// </summary>
|
|
private Vector3 QuantizeToOctagon(Vector3 direction)
|
|
{
|
|
if (direction == Vector3.zero)
|
|
return direction;
|
|
|
|
// 각도 계산
|
|
float angle = Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg;
|
|
|
|
// 45도 단위로 반올림
|
|
float snappedAngle = Mathf.Round(angle / 45f) * 45f;
|
|
|
|
// 다시 벡터로 변환
|
|
float radians = snappedAngle * Mathf.Deg2Rad;
|
|
return new Vector3(Mathf.Sin(radians), 0f, Mathf.Cos(radians));
|
|
}
|
|
}
|
|
}
|