본문 바로가기
스파르타 내배캠

스파르타 내배캠 Unity 3기 40일차

by LemongO 2024. 2. 19.

뇌절금지 뇌절금지

뇌절금지 뇌절금지

 

 

슬슬 끝이 다가와서 그런 걸까? 스스로를 통제 못해가고 있는 느낌이 든다...

마음이 급한건지 이게 맞는 건지 잘 모르겠네...

 

 


StateMachine & IState - 2

 

어제 큰 틀로 작성한 것들을 세부적으로 봐보자.

 

  • InputSystem (InputAction)

이전에 했던 것들과 똑같지만 하나 다른 게 있다면 우측 인스펙터 창에서 C# Class File을 Generate 해야 한다는 것이다.

 

 

Apply를 눌러 생성하면 다음과 같이 C# Class가 생성된다.

 

 

 

 

  • PlayerInput (C# Class) : Monobehavior

Player 오브젝트에 부착되는 PlayerInput 클래스는 방금 만들어준 InputAction의 C# 클래스 정보를 가지고 있다.

 

 

public class PlayerInput : MonoBehaviour
{
    public PlayerInputActions InputActions { get; private set; }
    public PlayerInputActions.PlayerActions PlayerActions { get; private set; }

    private void Awake()
    {
        InputActions = new PlayerInputActions();
        PlayerActions = InputActions.Player;
    }

    private void OnEnable()
    {
        InputActions.Enable();
    }

    private void OnDisable()
    {
        InputActions.Disable();
    }
}

 

Awake 시 새로운 PlayerInputActions를 생성하고

실제 Action들이 담겨있는 PlayerActions 구조체 PlayerActions에  방금 생성한 InputActions.Player를 담는다.

 

@Player 이다.

 

오브젝트 활성화 여부에 따라 InputAction 도 OnOff 가능하도록 OnEnable과 OnDisable을 이용해 해당 기능을 넣는다.

 

 

  • Player (C# Class) : MonoBehavior

플레이어의 동작을 수행할 Update 및 FixedUpdate 가 있고,
동작에 필요한 로직을 제어할 PlayerStateMachine을 필드로 가지고 있다.

 

private PlayerStateMachine stateMachine;
private void Start()
{
    Cursor.lockState = CursorLockMode.Locked;
    stateMachine.ChangeState(stateMachine.IdleState);
}

private void Update()
{
    stateMachine.HandleInput();
    stateMachine.Update();
}

private void FixedUpdate()
{
    stateMachine.PhysicsUpdate();
}

 

이 외에도 Player 클래스에는 플레이어에 대한 정보들이 담겨있다.

 

[field: Header("Reference")]
[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 PlayerInput Input { get; private set; }
public CharacterController Controller { get; private set; }
public ForceReceiver ForceReceiver { get; private set; }

private PlayerStateMachine stateMachine;    

private void Awake()
{
    AnimationData.Initialize();
    
    Animator = GetComponentInChildren<Animator>();
    Input = GetComponent<PlayerInput>();
    Controller = GetComponent<CharacterController>();
    ForceReceiver = GetComponent<ForceReceiver>();

    stateMachine = new PlayerStateMachine(this);
}

 

여기서부터 좀 중요했는데,

자세히 보면 Update 메서드에선 stateMachine에 별다른 매개변수 없이 HandleInput 및 Update 메서드만 호출하고 있다.

플레이어의 정보 없이 그저 stateMahcine의 메서드만 호출해주면 어떻게 움직이고 애니메이션이 변할까?

 

그 이유는 PlayerStateMachine에서 Player를 역참조 하고 있기 때문이다.

해당 부분은 조금 있다 보도록 하자.

 

  • PlayerSO (ScriptableObject)

플레이어가 사용할 파라미터들을 가지고 있는 ScriptableObject.

[CreateAssetMenu(fileName = "Player", menuName = "Characters/Player")]

public class PlayerSO : ScriptableObject
{
    [field: SerializeField] public PlayerGroundData GroundedData { get; private set; }
    [field: SerializeField] public PlayerAirData AirData { get; private set; }
}

지상(Ground)과, 공중(Air)에 대한 파라미터를 두 개로 분리시켰다.

또한 ScriptableObject 내에서만 수치를 조정하게 하고 인스펙터 상에 상태에 따른 파라미터들이 보이도록

[field: SerializeField] 어트리뷰트와 get; private set; 프로퍼티를 사용했다.

 

 

  • PlayerAnimationData (C# Class)

Animator 들에 등록된 파라미터들을 Hash로 변환해주는 클래스이다.

Player.Awake 에서 Initialize 메서드를 호출해 사용할 파라미터들을 Hash 로 변환해 준다.

public class PlayerAnimationData
{
    [SerializeField] private string groundParameterName = "@Ground";
    [SerializeField] private string idleParameterName = "Idle";
    [SerializeField] private string walkParameterName = "Walk";
    [SerializeField] private string runParameterName = "Run";

    [SerializeField] private string airParameterName = "@Air";
    [SerializeField] private string jumpParameterName = "Jump";
    [SerializeField] private string fallParameterName = "Fall";

    [SerializeField] private string attackParameterName = "@Attack";
    [SerializeField] private string comboAttackParameterName = "ComboAttack";


    public int GroundParameterHash { get; private set; }
    public int IdleParameterHash { get; private set; }
    public int WalkParameterHash { get; private set; }
    public int RunParameterHash { get; private set; }

    public int AirParameterHash { get; private set; }
    public int JumpParameterHash { get; private set; }
    public int fallParameterHash { get; private set; }

    public int AttackParameterHash { get; private set; }
    public int ComboAttackParameterHash { get; private set; }

    public void Initialize()
    {
        GroundParameterHash = Animator.StringToHash(groundParameterName);
        IdleParameterHash = Animator.StringToHash(idleParameterName);
        WalkParameterHash = Animator.StringToHash(walkParameterName);
        RunParameterHash = Animator.StringToHash(runParameterName);

        AirParameterHash = Animator.StringToHash(airParameterName);
        JumpParameterHash = Animator.StringToHash(jumpParameterName);
        fallParameterHash = Animator.StringToHash(fallParameterName);

        AttackParameterHash = Animator.StringToHash(attackParameterName);
        ComboAttackParameterHash = Animator.StringToHash(comboAttackParameterName);
    }

 

 


지금부터 각 상태에 대해 접근하니 많이 세부적으로 알아봐야 한다.

 

  • StateMachine (C# Class)

강의에 따르면 StateMachine은 그 자체로 사용하는 것이 아니라 상속을 받아 사용하고

생성자를 만들지 못하게 하기 위해 추상클래스로 사용한다고 한다.

 

생성자 부분을 정확히 알지 못해 확인한 결과, 생성자를 가질 순 있지만

인스턴스를 생성하지는 못한다는 것을 알게 되었다.

 

public abstract class StateMachine
{
    protected IState currentState;

    public void ChangeState(IState state)
    {
        currentState?.Exit();

        currentState = state;

        currentState?.Enter();
    }

    public void HandleInput()
    {
        currentState?.HandleInput();
    }

    public void Update()
    {
        currentState?.Update();
    }

    public void PhysicsUpdate()
    {
        currentState?.PhysicsUpdate();
    }
}

 

 

Player 클래스에서도 보았듯, 현재 State의 HandleInput과 Update 메서드들을 실행시켜 주는 메서드들이 이곳에 담겨있다.

 

하지만 여전히 플레이어의 정보는 보이지 않는다.

바로 다음 클래스를 보도록 하자.

 

  • PlayerStateMachine

StateMachine을 상속받고 Player의 State에 따라 행동을 해줄 각 State 정보를 가지고 있으며

실제 로직이 담긴 State에 넘겨줄 플레이어의 이동, 카메라 등을 제어할 파라미터들을 가지고 있다.

 

public Player Player { get; }

앞서 말했듯 PlayerStateMachine에서 Player를 역참조 하고 있기 때문에
매개변수를 넘기지 않고 StateMachine의 메서드를 호출해도 파라미터 값을 전달할 수 있다.

 

public PlayerIdleState IdleState { get; }
public PlayerWalkState WalkState { get; }
public PlayerRunState RunState { get; }

플레이어의 각 상태에 따른 로직을 담당하는 IState를 상속받은 State 들이다.

한 마디로 플레이어의 상태이다.

 

public Vector2 MovementInput { get; set; }
public float MovementSpeed { get; private set; }
public float RotationDamping { get; private set; }
public float MovementSpeedModifier { get; set; } = 1f;

public float JumpForce { get; set; }

public Transform MainCameraTransform { get; set; }

 

이동, 카메라 제어 시 필요한 파라미터들이다.

 

여기까지만 보면 get 프로퍼티들이 있어 초기화도 못하고 무언가를 return 하지도 않는다.

또한 파라미터들의 값 또한 Modifier를 제외하면 초기화가 되어있지도 않다.

 

그래서 PlayerStateMachine의 생성자를 활용해 이를 할당한다.

 

public PlayerStateMachine(Player player)
{
    Player = player;

    IdleState = new PlayerIdleState(this);
    WalkState = new PlayerWalkState(this);
    RunState = new PlayerRunState(this);

    MainCameraTransform = Camera.main.transform;

    MovementSpeed = player.Data.GroundedData.BaseSpeed;
    RotationDamping = player.Data.GroundedData.BaseRotationDamping;
}

생성 시, Player를 매개변수로 받아 역참조하며,

 

각 State 들은 인스턴스를 생성해 매개변수 this를 넘겨준다. 이 부분은 또 나중에 보도록 하자.

MainCameraTransform은 Camera.main.transform을 사용하고

Speed와 Damping은 역참조한 Player의 SO Data를 이용해 초기화하자.

 

MovementInput은 따로 초기화하지 않는다. 그 이유는 플레이어의 입력에 따라 계속해서 변하는 값이기 때문이다.

 

 

  • IState (interface)

IState를 상속받은 각 상태들이 호출해야 할 메서드를 지정해 놓았다.

 

현재 상태로 변경될 때 호출하는 Enter입력에 따라 취할 행동이 바뀌는 HandleInput, Update, PhysicsUpdate현재 상태가 끝났을 때 호출하는 Exit

public interface IState
{
    public void Enter();
    public void Exit();
    public void HandleInput();
    public void Update();
    public void PhysicsUpdate();
}

 

 

  • PlayerBaseState (C# Class) - IState를 상속받은 BaseState. 이를 상속받은 Ground, AirState 와 또 이걸 상속받은 각종 State가 있다.  이부분은 내일 적도록 해야겠다.