StateMachine 클래스 개요
지난 포스팅에서 Player 클래스가 State 변수를 가지고 있도록 하여 상태 패턴을 구현했었습니다.
하지만, 이렇게 구현할 시 상태가 전환 될 때 마다 힙 영역에 클래스가 할당된다는 단점이 있었습니다.
이를 보완하고자 StateMachine 클래스를 새로 만들어 상태 패턴을 재구성 해보겠습니다.
StateMachine 클래스 소개
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class StateMachine
{
Dictionary<string, BasicState> stateDic; // 상태들을 저장할 딕셔너리
public BasicState curState; // 현재 상태 변수
public Player owner; // 소유주
public StateMachine(Player owner) // 생성자에서 소유주 등록, 딕셔너리 할당
{
this.owner = owner;
stateDic = new Dictionary<string, BasicState>();
}
public void AddState(string stateName, BasicState state) // 딕셔너리에 상태 등록하는 메서드
{
if (stateDic.ContainsKey(stateName)) return;
stateDic.Add(stateName, state);
state.sm = this;
}
public void SetState(string stateName) // curState 변수에 상태 연결
{
if (stateDic.ContainsKey(stateName))
{
if (curState != null)
{
curState.Exit();
}
curState = stateDic[stateName];
curState.Enter();
}
}
public void Update() // 소유주 클래스에서 매 프레임 호출할 변수
{
curState.Update();
}
}
기존에 Player 가 curState 를 가지고 있었고 BasicState 클래스에서 소유주 변수를 가지고 있던것과 다르게 그것들을 StateMachine 클래스가 대신 가지고있고 상태들을 추가하고 현 상태에 연결하는 메서드 또한 가지고 있습니다.
이제, 이 StateMachine 을 사용하는 Player 클래스를 소개하겠습니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
StateMachine sm;
public Vector3 moveVec;
private void Start()
{
sm = new StateMachine(this); // StateMachine 할당
// 상태들을 StateMachine에 등록
sm.AddState("Idle", new PlayerIdleState());
sm.AddState("Walk", new PlayerWalkState());
sm.AddState("Run",new PlayerRunState());
// 초기 상태 Idle
sm.SetState("Idle");
}
private void Update()
{
float h = Input.GetAxisRaw("Horizontal");
float v = Input.GetAxisRaw("Vertical");
moveVec = new Vector3(h, 0, v).normalized;
sm.Update(); // StateMachine 클래스 내의 Update 메서드 호출
}
}
Player 는 StateMachine 클래스 변수를 가지고있고 이 변수를 통해 StateMachine 클래스 내의 메서드를 호출합니다.
이제, 마지막으로 BasicState와 이를 상속받는 Idle, Walk, Run 상태 클래스를 소개하겠습니다.
public abstract class BasicState
{
public StateMachine sm; // Player 변수가 아니라 StateMachine 변수를 가짐
public virtual void Enter() { }
public abstract void Update();
public virtual void Exit() { }
}
using UnityEngine;
public class PlayerIdleState : BasicState
{
public override void Update()
{
Debug.Log("대기");
if (sm.owner.moveVec != Vector3.zero)
{
sm.SetState("Walk"); // 상태 전환시 StateMachine 내의 SetState 메서드 호출
}
}
}
using UnityEngine;
public class PlayerWalkState : BasicState
{
public override void Update()
{
Debug.Log("걷기");
if (sm.owner.moveVec == Vector3.zero)
sm.SetState("Idle"); // 상태 전환시 StateMachine 내의 SetState 메서드 호출
if (Input.GetKeyDown(KeyCode.LeftShift))
sm.SetState("Run"); // 상태 전환시 StateMachine 내의 SetState 메서드 호출
}
}
using UnityEngine;
public class PlayerRunState : BasicState
{
public override void Update()
{
Debug.Log("뛰기");
if (sm.owner.moveVec == Vector3.zero)
sm.SetState("Idle"); // 상태 전환시 StateMachine 내의 SetState 메서드 호출
if (Input.GetKeyUp(KeyCode.LeftShift))
sm.SetState("Walk"); // 상태 전환시 StateMachine 내의 SetState 메서드 호출
}
}
세 클래스는 이제 더이상 Player 를 소유주로 직접 가지지 않습니다. 대신, 상태들을 관리하는 StateMachine 클래스 변수를 가지고 클래스 내의 SetState 메서드를 호출하여 상태들을 전환 시킵니다.
이로써, 이전 포스팅에서 구현하였던 상태 패턴의 단점은 어느정도 커버한 셈입니다.
그렇지만 여전히 불편한 점은 남아있습니다.
지금은 Player 클래스 하나만 가지고 상태 패턴을 구현하였지만 만약, Monster, NPC 혹은 GameManager 에서도 특정한 상태를 구현해주어야한다면 그 때마다 소유주가 다른 StateMachine 을 만들어야합니다.
이를 해결하고자 C# 에서는 generic 이란 문법을 사용합니다.
StateMachine 클래스 generic 화 하기
generic이란 여러 타입을 T 라는 문자 하나로 추상화하는 방법을 말합니다.
우선 StateMachine 클래스를 수정합니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class StateMachine<T> where T : class // <T> 를 추가 , where T : class (T 는 최소 class)
{
Dictionary<string, BasicState> stateDic;
public BasicState curState;
public T owner; // 소유주를 T 로 추상화
public StateMachine(T owner)
{
this.owner = owner;
stateDic = new Dictionary<string, BasicState>();
}
public void AddState(string stateName, BasicState state)
{
if (stateDic.ContainsKey(stateName)) return;
stateDic.Add(stateName, state);
state.sm = this;
}
public void SetState(string stateName)
{
if (stateDic.ContainsKey(stateName))
{
if (curState != null)
{
curState.Exit();
}
curState = stateDic[stateName];
curState.Enter();
}
}
public void Update()
{
curState.Update();
}
}
이렇게 두면 이제 T 에는 class 가 올 수 있기에 Player 뿐만아니라 다른 클래스도 StateMachine 을 소유할 수 있게됩니다.
하지만 이렇게 하게되면 한가지 의문이 생깁니다.
기존에 BasicState 클래스를 다시 봅시다.
public abstract class BasicState
{
public StateMachine sm; // StateMachine<T> 로 선언 해주어야 하는가?
public virtual void Enter() { }
public abstract void Update();
public virtual void Exit() { }
}
기존에는 StateMachine 이라고만 되어있기에 당연히 컴파일 에러가 발생합니다. 그렇다고해서 StateMachine<T> 로 선언해준다면 BasicState 또한 BasicState <T> 처럼 추상화 해주어야합니다.
이렇게되면 BasicState 를 상속받는 다른 상태 클래스들도 <T> 를 추가해주어야하는 문제가 발생합니다.
이는 사실, BasicState 클래스와 StateMachine 클래스의 참조 관계가 매우 끈끈한 관계이기에 발생한 문제입니다.
그래서 둘중 하나라도 바뀌게되면 나머지 한 클래스에도 영향을 미치게 되는 것이지요.
그래서 이 끈끈한 관계를 느슨하게 만들기 위해 우리는 StateMachine 클래스를 추상화 할 것입니다.
public interface IStateMachine
{
object GetOwner(); // 소유주를 반환받는 메서드
void SetState(string stateName); // BasicState 에서 상태 전환시 호출될 SetState 메서드
}
이제 StateMachine 클래스는 IStateMachine 인터페이스를 상속받습니다.
public class StateMachine<T> : IStateMachine where T : class // T 타입으로 추상화
{
Dictionary<string, BasicState> stateDic;
public BasicState curState;
public T owner; // T 타입으로 소유주 추상화
public StateMachine(T owner)
{
this.owner = owner;
stateDic = new Dictionary<string, BasicState>();
}
public void AddState(string stateName, BasicState state)
{
if (stateDic.ContainsKey(stateName)) return;
stateDic.Add(stateName, state);
state.Init(this); // 상태 등록시 상태 클래스 내의 Init 메서드 호출
}
public void SetState(string stateName)
{
if (stateDic.ContainsKey(stateName))
{
if (curState != null)
{
curState.Exit();
}
curState = stateDic[stateName];
curState.Enter();
}
}
public void Update()
{
curState.Update();
}
public object GetOwner() // 소유주 반환 메서드
{
return owner;
}
}
이제 BasicState 클래스는 더이상 StateMachine 이 아니라 IStateMachine 을 가지게됩니다.
public abstract class BasicState
{
public IStateMachine sm;
public virtual void Init(IStateMachine sm)
{
this.sm = sm;
}
public virtual void Enter() { }
public abstract void Update();
public virtual void Exit() { }
}
이제 PlayerState 라는 클래스를 만들고 BasicState를 상속받도록 합니다.
public class PlayerState : BasicState
{
protected Player player;
public override void Init(IStateMachine sm)
{
this.sm = sm;
player =(Player)sm.GetOwner();
}
public override void Update()
{
}
}
Init 메서드를 보시면 GetOwner 메서드를 호출하여 소유주를 엮어줍니다.
이제 기존의 IdleState, WalkState, RunState 클래스는 PlayerState 클래스를 상속받습니다.
using UnityEngine;
public class PlayerIdleState : PlayerState
{
public override void Update()
{
Debug.Log("대기");
if (player.moveVec != Vector3.zero)
{
sm.SetState("Walk");
}
}
}
using UnityEngine;
public class PlayerWalkState : PlayerState
{
public override void Update()
{
Debug.Log("걷기");
if (player.moveVec == Vector3.zero)
sm.SetState("Idle");
if (Input.GetKeyDown(KeyCode.LeftShift))
sm.SetState("Run");
}
}
using UnityEngine;
public class PlayerRunState : PlayerState
{
public override void Update()
{
Debug.Log("뛰기");
if (player.moveVec == Vector3.zero)
sm.SetState("Idle");
if (Input.GetKeyUp(KeyCode.LeftShift))
sm.SetState("Walk");
}
}
코드를 실행하면 잘 작동되는 것을 확인할 수 있습니다.
클래스 UML
여러 메서드를 생략했지만 구조만 놓고보면 다음과 같습니다.

'디자인패턴' 카테고리의 다른 글
| 디자인 패턴 <상태 패턴(State Pattern) #1> (0) | 2023.12.09 |
|---|---|
| 디자인 패턴 < 오브젝트 풀링을 활용한 사운드 매니저> (0) | 2023.11.06 |
| 디자인 패턴 <Component Pattern> (0) | 2023.09.21 |