에셋들 적용도 끝났으니, 먼저 가장 중요한 플레이어에게 생명을 불어넣기 위한 작업을 진행하기로 했다.
Idle, Walk, Sprint, Dodge(구르기) 총 네가지로 일단 기본적인 움직임을 구성할 예정이다. 플레이어의 움직임 구조는 FSM으로 짜기로 했다.
Input System을 다운받아 기본적인 인풋 액션들을 구성해줬다.
인풋 세팅을 클래스로 생성한 후, InputReader라는 스크립트를 새로 만들어 인풋 세팅을 상속받고 SO로 만들어 플레이어가 접근하기 편하게 설정해줬다.
public class PlayerController : MonoBehaviour
{
[SerializeField] private InputReader _inputReader;
public InputReader InputReader => _inputReader;
public CharacterController CharacterControllerCompo { get; private set; }
public PlayerAnimator AnimatorCompo { get; private set; }
public StateMachine PlayerStateMachine { get; private set; }
}
플레이어의 베이스가 되는 PlayerController.cs를 만들었다. 여기에 아까 만든 InputReader.cs를 넣고 플레이어의 움직임과 애니메이션에 필요한 CharacterController, PlayerAnimator, StateMachine을 추가해준다.
여기서 PlayerAnimator는 플레이어의 애니메이션을 관리하는 스크립트, StateMachine은 FSM구조를 위한 상태머신이다.
private void OnEnable()
{
if (_inputReader != null)
{
var playerInput = new Controls();
playerInput.Player.SetCallbacks(_inputReader);
playerInput.Player.Enable();
}
}
시작할 때 InputReader가 있다면 인풋시스템을 활성화시켜준다.
그리고 대충 움직임과 카메라 움직임 코드 추가. 카메라는 시네머신 VirtualCamera, 3rd Person Follow를 사용했다.
FSM 구조는 위에 글에 있는거랑 거의 똑같아서 따로 설명을 하진 않겠다.
이번엔 FSM을 리플렉션을 써서 만들진 않았고 그냥 플레이어의 자식 오브젝트에 State들을 다 때려넣었다. 리플렉션이 좀 느리다길래 다른 방법을 시도해보고 싶기도 했고, 저렇게 해두면 각 State마다 필요한 변수값들이 직관적으로 보여서 좋은 것 같다.
PlayerController.cs에서 시작할 때 저 자식오브젝트에 있는 State들을 불러와, StateMachine의 Dictionary에 저장한다.
모든 State들의 부모인 State.cs는 이렇게 되어있다.
플레이어의 기능과 컴포넌트들을 담고 있는 PlayerController.cs와 플레이어의 공격을 관리하는 PlayerAttackController.cs, 그리고 State들을 바꿔다닐 수 있게 하는 StateMachine.cs를 가지고 있다. 모든 State들은 이 변수들에 접근할 수 있다.
현재까지 만든 State들은 IdleState, MoveState, DodgeState, BasicAttackState, GroundedState 이렇게 다섯 종류가 있다.
public class PlayerGroundedState : State
{
public override void EnterState()
{
base.EnterState();
_owner.InputReader.BasicAttackEvent += AttackHandle;
_owner.InputReader.DodgeEvent += DodgeHandle;
}
public override void ExitState()
{
base.ExitState();
_owner.InputReader.BasicAttackEvent -= AttackHandle;
_owner.InputReader.DodgeEvent -= DodgeHandle;
}
private void AttackHandle()
{
if (_owner.CanAttack)
_stateMachine.ChangeState(StateTypeEnum.BasicAttack);
}
private void DodgeHandle()
{
if (_owner.CanAttack)
_stateMachine.ChangeState(StateTypeEnum.Dodge);
}
public override void UpdateState()
{
base.UpdateState();
_owner.Move();
}
}
GroundedState는 IdleState와 MoveState의 부모 State이며, 두 State의 동일한 기능을 관장한다. 또한 InputReader의 Action들을 구독해서 특정 입력이 감지된다면 곧바로 State들을 바꿔줄수 있게 해줬다.
애니메이션은 대충 이렇게 되어있다. (Attack쪽은 추후에 설명) Idle, Walk, Run은 블렌드 트리로 만들었다.
public class PlayerAnimator : MonoBehaviour
{
private Animator _animator;
private PlayerController _player;
private readonly int _speedAnim = Animator.StringToHash("Speed");
private readonly int _motionSpeedAnim = Animator.StringToHash("MotionSpeed");
private readonly int _groundedAnim = Animator.StringToHash("Grounded");
private readonly int _attackAnim = Animator.StringToHash("Attack");
private readonly int _attackCount = Animator.StringToHash("Attack_Count");
private readonly int _dodgeAnim = Animator.StringToHash("Dodge");
private readonly int _dodgeLeftAnim = Animator.StringToHash("Dodge_L");
private readonly int _dodgeRightAnim = Animator.StringToHash("Dodge_R");
private readonly int _dodgeBackAnim = Animator.StringToHash("Dodge_B");
private readonly int _freeFallAnim = Animator.StringToHash("FreeFall");
private float _animationBlend;
private void Awake()
{
_animator = GetComponent<Animator>();
_player = transform.parent.GetComponent<PlayerController>();
}
public void SetMoveAnimation(float targetSpeed, float SpeedChangeRate, float inputMagnitude)
{
if (inputMagnitude <= 0f)
{
_animationBlend = Mathf.Lerp(_animationBlend, 0, Time.deltaTime * SpeedChangeRate);
}
_animationBlend = Mathf.Lerp(_animationBlend, targetSpeed, Time.deltaTime * SpeedChangeRate);
if (_animationBlend < 0.01f) _animationBlend = 0f;
_animator.SetFloat(_speedAnim, _animationBlend);
_animator.SetFloat(_motionSpeedAnim, 1);
}
public void SetGroundedAnimation(bool value)
{
_animator.SetBool(_groundedAnim, value);
}
public void SetFreeFallAnimation(bool value)
{
_animator.SetBool(_freeFallAnim, value);
}
플레이어의 Animation들을 관리하는 PlayerAnimator.cs
플레이어의 State가 실행될 때 얘에 접근해서 필요한 애니메이션을 실행시켜주면 된다.
그리고 특정 동작이 끝났다면, PlayerAnimator에 있는 각 실행에 대한 EndTrigger를 실행시켜주면된다.
각 애니메이션 마지막에 달려있는 Animation Event를 통해 EndTrigger 함수를 연결해놓았기 때문에 자동으로 종료된다.
말이 좀 난잡해졌는데 요약해서 구조에 대해 설명하자면,
InputReader에서 Dodge 키 입력 감지 ->
InputReader에 있는 Dodge Action 실행 ->
만약 IdleState였다면 바로 DodgeState로 넘어감 ->
PlayerController.cs의 Dash 기능 실행해서 앞으로 전진함 ->
PlayerAnimator.cs의 함수 실행해서 구르는 애니메이션 실행함 ->
Dash 기능 끝나고, 애니메이션은 Animation Event으로 애니메이션 마지막에 실행이 종료 ->
다시 Idle 상태로 전환
움직임과 카메라 전환, 애니메이션이 담겨있는 영상
다음엔 플레이어 공격을 개발할 예정이다. (지금도 Attack 애니메이션은 적용되어있다.)
'Unity > 개인프로젝트' 카테고리의 다른 글
[Unity] 갠프(3) - 기본 공격(3콤보), 애니메이션 (0) | 2024.07.08 |
---|---|
[Unity] 갠프(1) - 프로젝트 시작, 에셋 적용 (0) | 2024.07.06 |