using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 플레이어의 모든 시스템을 초기화하고, 매 프레임 상태 머신을 실행
// Awake에서 상태 머신을 만들고 Idle 상태로 시작 → Update와 FixedUpdate로 상태를 계속 갱신
// 플레이어의 모든 하위 시스템 (입력, 애니메이션, 이동 등)을 관리하는 중심 클래스 (입력뿐 아니라 플레이어에게 필요한 여러 컴포넌트들을 관리)
public class Player : MonoBehaviour
{
[field: SerializeField] public PlayerSO Data { get; private set; } // 플레이어 데이터 (속도, 점프력 등 설정)
[field:Header("Animations")]
[field:SerializeField] public PlayerAnimationData AnimationData { get; private set; } // 애니메이션 데이터
public Animator Animator { get; private set; } // 애니메이션 제어
public PlayerController Input { get; private set; } // 입력 처리
public CharacterController Controller { get; private set; } // 물리 이동
public ForceReceiver ForceReceiver { get; private set; }
private PlayerStateMachine stateMachine; // FSM의 핵심 컨트롤러
private void Awake()
{
AnimationData.Initialize(); // 애니메이션 파라미터 해시값 초기화
Animator = GetComponentInChildren<Animator>(); // 애니메이터 연결
Input = GetComponent<PlayerController>(); // 입력 시스템 연결
Controller = GetComponent<CharacterController>(); // 캐릭터 컨트롤러(물리) 연결
ForceReceiver = GetComponent<ForceReceiver>();
stateMachine = new PlayerStateMachine(this); // 상태 머신 생성
}
void Start()
{
Cursor.lockState = CursorLockMode.Locked; // 마우스 잠금
stateMachine.ChangeState(stateMachine.IdleState); // 초기 상태를 Idle로 설정
}
private void Update()
{
stateMachine.HandleInput(); // 입력 처리 (키보드, 마우스 입력 감지)
stateMachine.Update(); // 논리 업데이트 (상태 전환 등)
}
private void FixedUpdate()
{
stateMachine.PhysicsUpdate(); // 물리 업데이트 (이동, 회전 등)
}
}
Player.csusing System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 생성 시 모든 상태를 준비하고, ChangeState로 현재 상태를 전환
public class PlayerStateMachine : StateMachine
{
public Player Player { get; } // 플레이어 객체 참조
public Vector2 MovementInput { get; set; } // 이동 입력값 (WASD 등)
public float MovementSpeed { get; private set; } // 기본 이동 속도
public float RotationDamping { get; private set; }
public float MovementSpeedModifier { get; set; } = 1f; // 속도 배율 (Walk, Run 등)
public float JumpForce { get; set; }
public Transform MainCameraTransform { get; set; } // 카메라 방향 참조
public PlayerIdleState IdleState { get; private set; } // 대기 상태
public PlayerWalkState WalkState { get; private set;} // 걷기 상태
public PlayerRunState RunState { get; private set;} // 달리기 상태
public PlayerJumpState JumpState { get; private set;} // 점프 상태
public PlayerFallState FallState { get; private set;} // 추락 상태
public PlayerStateMachine(Player player)
{
this.Player = player;
MainCameraTransform = Camera.main.transform; // 카메라 연결
IdleState = new PlayerIdleState(this);
WalkState = new PlayerWalkState(this);
RunState = new PlayerRunState(this);
JumpState = new PlayerJumpState(this);
FallState = new PlayerFallState(this);
MovementSpeed = player.Data.GroundData.BaseSpeed; // 기본 속도 설정
RotationDamping = player.Data.GroundData.BaseRotationDamping;
}
}
PlayerStateMachine.csusing System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 플레이어의 키보드, 마우스 입력을 받아내고 FSM이나 이동 로직이 이걸 참조해서 행동함
public class PlayerController : MonoBehaviour
{
public PlayerInputs playerInputs { get; private set; } // 인풋 시스템에서 전체 입력을 다루는 객체
public PlayerInputs.PlayerActions playerActions { get; private set; } // Player 액션 맵 (이동, 점프 등 액션)
private void Awake()
{
playerInputs = new PlayerInputs(); // InputActions 생성
playerActions = playerInputs.Player; // 플레이어 인풋 안의 액션 맵, 그 옆 액션을 표시
}
private void OnEnable() // 켜질 때
{
playerInputs.Enable(); // 이 오브젝트가 활성화되면 인풋 활성화
}
private void OnDisable() // 꺼질 때
{
playerInputs.Disable(); // 비활성화되면 인풋 비활성화
}
}
PlayerController.csusing System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
// 모든 상태가 공통으로 사용하는 기능(입력 읽기, 이동, 회전)을 제공
// 상태가 시작되면 입력을 감지하고, 매 프레임 이동 로직을 처리
public class PlayerBaseState : IState
{
protected PlayerStateMachine stateMachine; // 상태 머신 참조 (상태 전환, 데이터 공유용)
protected readonly PlayerGroundData groundData; // 지상 상태에서 사용할 데이터 (속도, 회전 감쇠 등)
// 상태 머신 초기화
public PlayerBaseState(PlayerStateMachine stateMachine)
{
this.stateMachine = stateMachine; // 상태 머신 연결
groundData = stateMachine.Player.Data.GroundData; // PlayerSO에서 지상 데이터 가져오기
}
/// <summary>
/// 상태에 진입할 때 호출 (상태 초기화)
/// </summary>
public virtual void Enter()
{
AddInputActionsCallbacks(); // 입력 이벤트(이동, 달리기 등) 감지하도록 등록
}
/// <summary>
/// 상태에서 나갈 때 호출 (정리 작업)
/// </summary>
public virtual void Exit()
{
RemoveInputActionsCallbacks(); // 입력 이벤트 해제
}
/// <summary>
/// 입력 이벤트를 등록하는 메서드
/// </summary>
protected virtual void AddInputActionsCallbacks()
{
PlayerController input = stateMachine.Player.Input; // 플레이어의 입력 컨트롤러 가져오기
input.playerActions.Movement.canceled += OnMovementCanceled; // 이동 입력이 끝날 때 호출
input.playerActions.Run.started += OnRunStarted; // 달리기 시작 시 호출
input.playerActions.Jump.started += OnJumpStarted;
}
/// <summary>
/// 입력 이벤트를 해제하는 메서드 (메모리 누수 방지)
/// </summary>
protected virtual void RemoveInputActionsCallbacks()
{
PlayerController input = stateMachine.Player.Input;
input.playerActions.Movement.canceled -= OnMovementCanceled; // 이동 이벤트 해제
input.playerActions.Run.started -= OnRunStarted; // 달리기 이벤트 해제
input.playerActions.Jump.started -= OnJumpStarted;
}
/// <summary>
/// 매 프레임 입력 처리
/// </summary>
public virtual void HandleInput()
{
ReadMovementInput(); // WASD 등 입력 값을 읽어서 stateMachine에 저장
}
/// <summary>
/// 물리 업데이트
/// </summary>
public virtual void PhysicsUpdate()
{
}
/// <summary>
/// 논리 업데이트
/// </summary>
public virtual void Update()
{
Move(); // 이동 및 회전 실행
}
/// <summary>
/// 이동 입력이 취소될 때 호출 (e.g. WASD 키 뗐을 때)
/// </summary>
/// <param name="context"></param>
protected virtual void OnMovementCanceled(InputAction.CallbackContext context)
{
}
/// <summary>
/// 달리기 입력이 시작될 때 호출 (e.g. Shift 키 눌렀을 때)
/// </summary>
/// <param name="context"></param>
protected virtual void OnRunStarted(InputAction.CallbackContext context)
{
}
protected virtual void OnJumpStarted(InputAction.CallbackContext context)
{
}
// 애니메이션 시작
protected void StartAnimation(int animationHash)
{
stateMachine.Player.Animator.SetBool(animationHash, true); // Animator의 bool 파라미터 true로 설정
}
// 애니메이션 종료
protected void StopAnimation(int animationHash)
{
stateMachine.Player.Animator.SetBool(animationHash, false);
}
// WASD 입력을 읽어서 상태 머신에 저장
private void ReadMovementInput()
{
stateMachine.MovementInput = stateMachine.Player.Input.playerActions.Movement.ReadValue<Vector2>();
}
// 이동과 회전을 총괄하는 메서드
private void Move()
{
Vector3 movementDirection = GetMovementDirection(); // 카메라 기준 이동 방향 계산
Move(movementDirection); // 플레이어를 해당 방향으로 이동
Rotate(movementDirection); // 이동 방향으로 플레이어 회전
}
// 카메라를 기준으로 WASD 입력을 3D 방향으로 변환
private Vector3 GetMovementDirection()
{
Vector3 forward = stateMachine.MainCameraTransform.forward; // 카메라의 앞 방향
Vector3 right = stateMachine.MainCameraTransform.right; // 카메라의 우측 방향
forward.y = 0; // 수평 이동만 하도록 y를 0으로
right.y = 0;
forward.Normalize(); // 방향 벡터 크기를 1로 맞춤
right.Normalize();
// 입력값을 카메라 방향에 맞춰 합성
return forward * stateMachine.MovementInput.y + right * stateMachine.MovementInput.x;
}
// 플레이어를 실제로 이동시킴 (CharacterController 사용)
private void Move(Vector3 direction)
{
float movementSpeed = GetMovementSpeed(); // 현재 속도 계산
stateMachine.Player.Controller.Move(((direction * movementSpeed) + stateMachine.Player.ForceReceiver.Movement ) * Time.deltaTime);
}
// 이동 속도 계산
private float GetMovementSpeed()
{
float moveSpeed = stateMachine.MovementSpeed * stateMachine.MovementSpeedModifier;
return moveSpeed;
}
// 플레이어를 이동 방향으로 부드럽게 회전
private void Rotate(Vector3 direction)
{
if(direction != Vector3.zero) // 방향이 있을 때만 회전
{
Transform playerTransform = stateMachine.Player.transform;
Quaternion targetRotation = Quaternion.LookRotation(direction); // 목표 회전각 계산
playerTransform.rotation = Quaternion.Slerp(playerTransform.rotation, targetRotation, stateMachine.RotationDamping * Time.deltaTime); // 현재 회전에서 목표 회전으로 보간
}
}
}
PlayerBaseState.csusing System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerGroundState : PlayerBaseState
{
public PlayerGroundState(PlayerStateMachine stateMachine) : base(stateMachine)
{
}
// 바닥에 있을 때 호출
public override void Enter()
{
base.Enter();
StartAnimation(stateMachine.Player.AnimationData.GroundParameterHash);
}
// 바닥에서 떨어질 때 호출
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Player.AnimationData.GroundParameterHash);
}
// 논리 업데이트
public override void Update()
{
base.Update();
}
// 물리 업데이트
public override void PhysicsUpdate()
{
base.PhysicsUpdate();
// 플레이어가 바닥에 있지 않고 아래로 떨어지는 속도가 중력보다 크면 (공중에 떠 있는 상태)
if (!stateMachine.Player.Controller.isGrounded
&& stateMachine.Player.Controller.velocity.y < Physics.gravity.y * Time.fixedDeltaTime)
{
stateMachine.ChangeState(stateMachine.FallState); // 추락 상태로 전환
return;
}
}
// 이동 입력이 취소될 때
protected override void OnMovementCanceled(InputAction.CallbackContext context)
{
if(stateMachine.MovementInput == Vector2.zero) // 입력이 완전히 없으면
{
return;
}
stateMachine.ChangeState(stateMachine.IdleState); // 대기 상태로 전환
base.OnMovementCanceled(context);
}
// 점프 입력이 시작될 때
protected override void OnJumpStarted(InputAction.CallbackContext context)
{
base.OnJumpStarted(context);
stateMachine.ChangeState(stateMachine.JumpState); // 점프 상태로 전환
}
}
PlayerGroundState.csusing System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerAirState : PlayerBaseState
{
public PlayerAirState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
{
}
// 공중 상태에 진입할 때 호출
public override void Enter()
{
base.Enter();
StartAnimation(stateMachine.Player.AnimationData.AirParameterHash); // Air 시작
}
// 공중 상태에서 나갈 때 호출
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Player.AnimationData.AirParameterHash); // Air 종료
}
}
PlayerAirState.csPlayerJumpState.csPlayerFallState.csusing System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ForceReceiver : MonoBehaviour
{
[SerializeField] private CharacterController _controller; // 캐릭터 컨트롤러 참조
private float _verticalVelocity; // 세로 방향의 속도 (중력, 점프 등 적용)
public Vector3 Movement => Vector3.up * _verticalVelocity; // 현재 이동 벡터 (y축만 사용)
void Start()
{
_controller = GetComponent<CharacterController>(); // 컨트롤러 초기화
}
void Update()
{
if (_controller.isGrounded) // 비닥에 닿아 있다면
{
_verticalVelocity = Physics.gravity.y * Time.deltaTime; // 바닥에 붙어 있도록 유지
}
else // 바닥에 닿아 있지 않다면
{
_verticalVelocity += Physics.gravity.y * Time.deltaTime; // 중력의 영향을 받도록 더해줌
}
}
// 점프 요청 시 호출 (점프 힘을 세로 속도에 추가)
public void Jump(float jumpForce)
{
_verticalVelocity += jumpForce; // 순간적으로 위로 속도 증가
}
}
ForceReceiver.csPlayer.cs
PlayerAnimationData.cs
PlayerController.cs
ForceReceiver.cs
PlayerAirState.cs
PlayerBaseState.cs
PlayerFallState.cs
PlayerGroundState.cs
PlayerIdleState.cs
PlayerJumpState.cs
PlayerRunState.cs
PlayerStateMachine.cs
PlayerWalkState.cs
StateMachine.cs
PlayerSO.cs (ScriptableObject)
Player.Awake:PlayerStateMachine 생성자:Player.Start:PlayerIdleState.Enter:Player.Update:WASD 입력:Shift 입력 (달리기):Space 입력 (점프):PlayerWalkState.Update:Player.FixedUpdate:ForceReceiver.Update:PlayerJumpState:PlayerFallState:Idle → Walk: WASD 입력.Walk → Run: Shift 입력.Walk/Run → Idle: OnMovementCanceled에서 입력 0 확인.Ground → Jump: OnJumpStarted.Jump → Fall: Y 속도 <= 0.Fall → Idle: 착지.Ground → Fall: 땅에서 떨어짐 (!isGrounded && velocity.y < gravity).Player.Update: 입력 → 논리 업데이트 → 상태 전환.Player.FixedUpdate: 물리 체크 → 상태 전환.ForceReceiver.Update: 중력/점프 적용 → Controller.Move.