유한 상태 머신(FSM) 개요
상태 패턴은 유한 상태 머신(Finite state Machine)을 객체 지향 방식으로 구현하는 디자인 패턴입니다. 그래서 상태 패턴에 대해 이해하기 위해서는 유한 상태 머신에 대해 알 필요가 있습니다.
유한 상태 머신이란 유한한 개수의 상태를 가질 수 있는 도구로서 한 객체가 한 번에 오로지 하나의 상태만을 가지게되며 어떠한 이벤트가 발생했을 때 한 상태에서 다른 상태로 전환할 수 있습니다.
Swich-case 문을 활용하여 FSM 구현하기
Player 객체가 있다고 가정해 봅시다. Player 객체는 Idle(대기), Walk(걷기), Run(뛰기) 상태를 가지고 있습니다.
이 상태들을 나타낼 STATE_TYPE 열거형부터 정의해 보겠습니다.
public enum STATE_TYPE // 어떤 상태인지 구별할 enum 변수
{ IDLE,WALK,RUN}
다음으로는 Player 객체를 만들 클래스를 작성해보겠습니다.
public class Player : MonoBehaviour // Player 객체를 생성할 클래스
{
STATE_TYPE curState; // 현재 상태를 나타내는 enum 타입 변수
private void Update()
{
float h = Input.GetAxisRaw("Horizontal"); // 수평축 입력을 받음
float v = Input.GetAxisRaw("Vertical"); // 수직축 입력을 받음
Vector3 moveVec = new Vector3(h, 0, v).normalized; // 이동할 때 사용될 벡터
// Swich-case 문을 활용한 유한 상태 머신
switch(curState)
{
case STATE_TYPE.IDLE:
Debug.Log("대기");
// 만약, 키 입력이 발생했다면,
if(moveVec != Vector3.zero)
{
// 현재 상태를 WALK 상태로 전환
curState = STATE_TYPE.WALK;
}
break;
case STATE_TYPE.WALK:
Debug.Log("걷기");
// 만약, 키 입력이 중지된다면,
if (moveVec == Vector3.zero)
// 현재 상태를 IDLE 상태로 전환
curState = STATE_TYPE.IDLE;
// 만약, 걷기 상태에서 LeftShift 키를 눌렀다면
if (Input.GetKeyDown(KeyCode.LeftShift))
// 현재 상태를 RUN 상태로 전환
curState = STATE_TYPE.RUN;
break;
case STATE_TYPE.RUN:
Debug.Log("뛰기");
// 만약, 키 입력이 중지된다면,
if (moveVec == Vector3.zero)
// 현재 상태를 IDLE 상태로 전환
curState = STATE_TYPE.IDLE;
// 만약, 걷기 상태에서 LeftShift 키를 뗐다면
if (Input.GetKeyUp(KeyCode.LeftShift))
// 현재 상태를 WALK 상태로 전환
curState = STATE_TYPE.WALK;
break;
}
}
}
Swich-case 문을 사용하면 FSM 을 비교적 쉽게 구현할 수 있다는 장점이 있습니다. 하지만, 객체의 상태가 늘어나거나 그에 따른 기능이 여럿 추가된다면 이 코드는 굉장히 비효율적이겠지요.
따라서, 이런 단점을 보완하고자 열거형 변수로만 관리했던 '상태'를 추상화하여 객체 지향의 방식으로 유한상태머신을 구현하는 패턴을 상태 패턴(State Pattern)이라 합니다.
상태 패턴 클래스 다이어그램

상태 패턴의 클래스 다이어그램입니다. 먼저, 왼쪽부터 보면 다음과 같은 클래스가 눈에 띕니다.
- Context : state 변수를 갖는 객체. state 변수 내의 operation() 메서드를 실행시킴
- State 인터페이스(추상클래스) : State1, State2 의 부모클래스로 추상메서드 operation() 을 가지고있음.
- State1, State2 : State 인터페이스를 상속받는 클래스로 operation() 메서드를 가지고있으며 operation() 메서드 내에는 setState(State state) 메서드가 존재하여 서로 상태를 전환할 수 있음.
상태 패턴으로 상태 머신 구현하기
앞서 진행하였던 Player 클래스를 기반으로 코드를 수정하겠습니다.
가장 먼저, 모든 상태의 부모인 State 클래스부터 작성합니다.
public abstract class BasicState
{
public Player owner; // State 의 소유주
public BasicState(Player owner) // State를 생성할 때 소유주를 엮어줌
{
this.owner = owner;
}
public virtual void Enter() { } // 상태 진입시 한번 호출될 메서드
public abstract void Update(); // 상태 진입 중일 때 매 프레임 호출될 메서드
public virtual void Exit() { } // 다른 상태로 전환시 한번 호출될 메서드
}
이제 이를 상속 받는 PlayerIdleState, PlayerWalkState, PlayerMoveState 클래스를 작성합니다.
using UnityEngine;
public class PlayerIdleState : BasicState
{
public PlayerIdleState(Player owner) : base(owner)
{
}
public override void Update()
{
Debug.Log("대기");
if (owner.moveVec != Vector3.zero)
{
owner.curState = new PlayerWalkState(owner);
}
}
}
using UnityEngine;
public class PlayerWalkState : BasicState
{
public PlayerWalkState(Player owner) : base(owner)
{
}
public override void Update()
{
Debug.Log("걷기");
if (owner.moveVec == Vector3.zero)
owner.curState = new PlayerIdleState(owner);
if (Input.GetKeyDown(KeyCode.LeftShift))
owner.curState = new PlayerRunState(owner);
}
}
using UnityEngine;
public class PlayerRunState : BasicState
{
public PlayerRunState(Player owner) : base(owner)
{
}
public override void Update()
{
Debug.Log("뛰기");
if (owner.moveVec == Vector3.zero)
owner.curState = new PlayerIdleState(owner);
if (Input.GetKeyUp(KeyCode.LeftShift))
owner.curState = new PlayerWalkState(owner);
}
}
세 클래스 내에서 특정 조건이 만족될 때 owner.curState 변수를 호출하여 새로 클래스를 할당하여 상태를 전환시킵니다.
마지막으로 이를 사용하는 Player 클래스를 소개하겠습니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
public BasicState curState;
public Vector3 moveVec;
private void Start()
{
curState = new PlayerIdleState(this);
}
private void Update()
{
float h = Input.GetAxisRaw("Horizontal");
float v = Input.GetAxisRaw("Vertical");
moveVec = new Vector3(h, 0, v).normalized;
curState.Update();
}
}
이전에 Swich-case 문으로 작성했던 부분과 비교해보면 훨씬 단순해졌음을 알 수 있습니다. 추가된 사항은 두줄이네요.
- curState = new PlayerIdleState(this); : 현재 상태를 Idle 상태로 초기화
- curState.Update(); : curState 내의 update 메서드를 매 프레임 마다 호출
다음 포스팅에서 보완할 점
사실, 이렇게만 구현해도 상태패턴이라 이야기하긴 합니다만 여전히 불편한 점이 많습니다.
그중 가장 눈에 띄는 점은 상태를 전환 시킬 때마다 힙 영역에 상태 클래스가 할당된다는 점입니다.
이러한 점을 보완하고자 Player 가 State 를 가지는 것이 아니라 StateMachine 이라는 상태를 총괄적으로 관리할 수 있는 클래스를 만들어서 관리하는 방법을 다음 포스팅에서 소개하겠습니다.
'디자인패턴' 카테고리의 다른 글
| 디자인 패턴 <상태 패턴(State Pattern) #2> (1) | 2023.12.10 |
|---|---|
| 디자인 패턴 < 오브젝트 풀링을 활용한 사운드 매니저> (0) | 2023.11.06 |
| 디자인 패턴 <Component Pattern> (0) | 2023.09.21 |