Unity | 2D | State Pattern

Clean·2025년 5월 23일

2D

목록 보기
3/4

오늘 배운 것

  • 기존에 구현한 PlayerController, EnemyControllerState Pattern 으로 리팩토링

PlayerController 리팩토링

기존 코드는 필드변수, 입력, 이동 및 점프 로직, 애니메이션 전환 등

하나의 스크립트에 전부 포함되어있는데, 이 구조를 State Pattern 으로 변경했다.


BaseState

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() : 상태가 종료될 때 실행되는 함수


StateMachine

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 클래스를

필드변수로 추가하여 관리하는 방식이다.


PlayerState : BaseState

기존 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();
	}
}

Player : MonoBehaviour

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 클래스에서는 필드변수의 초기화, 입력,

이벤트 함수로 BaseStateUpdate, 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 클래스를 초기화하고, 딕셔너리에 사용할 상태들을 초기화한다.

맨 처음의 상태도 지정해줘야 하기에 CurStateIdle 을 할당한다.


void Update()
{
	inputX = Input.GetAxis("Horizontal");
	isJump = Input.GetKeyDown(KeyCode.Space);
	stateMachine.Update();
}

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

그리고 이벤트 함수에 StateMachine 클래스의 Update()FixedUpdate() 를 연결하면 된다.


상태를 관리하는 StateMachine

BaseStatePlayerState 에 상속하고, PlayerState 를 각 상태에 상속하는

상속의 상속의 상속을 하는 과정이 처음에는 비효율적이란 생각이 들었지만

EnemyState Pattern 으로 리팩토링 할 때,

기존의 구조를 사용하여 기능만 추가하면 되니 되게 쉬운 느낌이였다.

0개의 댓글