PlayerController, EnemyController 를 State Pattern 으로 리팩토링
기존 코드는 필드변수, 입력, 이동 및 점프 로직, 애니메이션 전환 등
하나의 스크립트에 전부 포함되어있는데, 이 구조를 State Pattern 으로 변경했다.
public abstract class BaseState
{
public bool HasPhysics;
public abstract void Enter();
public abstract void Update();
public virtual void FixedUpdate() { }
public abstract void Exit();
}
public enum EState
{
Idle, Walk, Jump, Patrol,
}
모든 상태 클래스에 상속할 기본 클래스다.
Enter() : 상태가 변경될 때 실행되는 함수
Update() : 매 프레임마다 실행되는 함수
FixedUpdate() : 물리처리가 필요할 때 실행되는 함수로 사용하지 않는 상태도 있으니 가상함수로 선언
Exit() : 상태가 종료될 때 실행되는 함수
public class StateMachine
{
public Dictionary<EState, BaseState> stateDic;
public BaseState CurState;
public StateMachine()
{
stateDic = new Dictionary<EState, BaseState>();
}
public void ChangeState(BaseState changedState)
{
if (CurState == changedState)
return;
CurState.Exit();
CurState = changedState;
CurState.Enter();
}
public void Update()
{
CurState.Update();
}
public void FixedUpdate()
{
if (CurState.HasPhysics)
CurState.FixedUpdate();
}
}
BaseState 를 상속한 클래스에서 구현한 추상함수들을 관리하고 실행하는 클래스다.
플레이어나 몬스터 오브젝트에 컴포넌트를 추가하고 StateMachine 클래스를
필드변수로 추가하여 관리하는 방식이다.
기존 PlayerController 스크립트에 있던 필드변수들을 Player 스크립트로 옮겼다.
그리고 PlayerState 필드변수로 protected Player player; 를 추가하고
이 클래스 내부에서 player 의 필드변수에 접근해서 사용하는 방식이다.
public class Player_Idle : PlayerState {}
public class Player_Walk : PlayerState {}
public class Player_Jump : PlayerState {}
또한 BaseState 클래스를 상속받은 PlayerState 를
각 상태 (Idle, Walk, Jump)에 재상속해야한다.
사용할 함수들은 BaseState 에서 선언했기 때문에 각 상태 클래스에서
Enter, Update, FixedUpdate, Exit 함수 안에 상태에 맞는 기능을 추가하면 된다.
// Example
public class Player_Idle : PlayerState
{
public Player_Idle(Player player) : base(player) { HasPhysics = false; }
public override void Enter()
{
player.isWalk = false;
player.rigid.velocity = Vector2.zero;
base.Enter();
}
public override void Update()
{
base.Update();
if (Mathf.Abs(player.inputX) > 0.1f)
{
player.stateMachine.ChangeState(player.stateMachine.stateDic[EState.Walk]);
}
}
public override void FixedUpdate()
{
base.FixedUpdate();
}
}
public StateMachine stateMachine;
void StatMachineInit()
{
stateMachine = new StateMachine();
stateMachine.stateDic.Add(EState.Idle, new Player_Idle(this));
stateMachine.stateDic.Add(EState.Walk, new Player_Walk(this));
stateMachine.stateDic.Add(EState.Jump, new Player_Jump(this));
stateMachine.CurState = stateMachine.stateDic[EState.Idle];
}
각 상태에 따른 애니메이션전환, 이동, 점프의 로직은 PlayerState 클래스에서 처리했기 때문에
Player 클래스에서는 필드변수의 초기화, 입력,
이벤트 함수로 BaseState 의 Update, FixedUpdate 실행만 추가하면 된다.
void Start()
{
// 컴포넌트들 초기화
StatMachineInit();
}
void StatMachineInit()
{
stateMachine = new StateMachine();
stateMachine.stateDic.Add(EState.Idle, new Player_Idle(this));
stateMachine.stateDic.Add(EState.Walk, new Player_Walk(this));
stateMachine.stateDic.Add(EState.Jump, new Player_Jump(this));
stateMachine.CurState = stateMachine.stateDic[EState.Idle];
}
Start() 에서 StateMachine 클래스를 초기화하고, 딕셔너리에 사용할 상태들을 초기화한다.
맨 처음의 상태도 지정해줘야 하기에 CurState 에 Idle 을 할당한다.
void Update()
{
inputX = Input.GetAxis("Horizontal");
isJump = Input.GetKeyDown(KeyCode.Space);
stateMachine.Update();
}
void FixedUpdate()
{
stateMachine.FixedUpdate();
}
그리고 이벤트 함수에 StateMachine 클래스의 Update() 와 FixedUpdate() 를 연결하면 된다.
상태를 관리하는
StateMachine과
BaseState를PlayerState에 상속하고,PlayerState를 각 상태에 상속하는상속의 상속의 상속을 하는 과정이 처음에는 비효율적이란 생각이 들었지만
Enemy를State Pattern으로 리팩토링 할 때,기존의 구조를 사용하여 기능만 추가하면 되니 되게 쉬운 느낌이였다.