이번엔 유니티 디자인 패턴중에 fsm 패턴을 알아보자.
FSM 패턴 유니티사용에선 은근 유명한 패턴 중에 하나라고 생각한다.
일단 무슨 패턴인지 모르는 사람들을 위해서 기본적인 구조를 깔고 가려고 한다.
그냥 비유를 해보자면 하나에 객체에 상태를 가지고 있는 것이다.
예시로 오토로 싸우는 게임을 만들고 있는데 메인 캐릭터는 무슨 상태를 가질 수 있을까
idle 기본 모션
move 이동 모션
die 죽은 모션
attack 공격 모션
등등 엄청 많은 상태와 모션 애니메이션 등등을 가지고 있고 이걸 필요할 때마다 교체해 가면서 사용해야 하는데
이때 캐릭터들이 주변 상황에 따라서 자신의 상태를 바꿔가면서 싸우게 된다.
FSM 패턴은 이러한 자신의 상태 기본상태 이동상태 등등 각자 가지고 있고
필요시 추가 나 제거하고 내현 상태를 수정해 가면서 관리한다.
설명이 어려운데 이미지를 하나 만들어왔습니다.
State 로직 부분에서 실행시킬 state를 지정해서 교체 요청을 하고 IState가 가지고 있는 State에서 맞는 상태를 찾아서
교체를 한다.
그러면 Update() 루프를 돌면서 IState안에 있는 상태를 돌게 하면 내상태를 교체해 가면서 실행할 수 있는 것이다.
보통 유니티 Update() 함수 안에 if를 써서 상태를 분리하는데 그걸 좀 더 상세화 하고 추가 제거 관리를 쉽게 하기 위해서 사용된다고 보면 될 거 같다.
오토바이 경주 게임을 만든다고 할 때
오토바이가 플레이어고 가질 수 있는 상태를 상상을 해보면
기본 상태 : 안 움직임,
이동 상태 : 직진으로 달리는 중,
회전 상태 : 좌나 우로 기울어진 상태
등등 생각해 볼 수 있을 거 같다
그러면
먼저 기본적으로 상태를 구현할 수 있는 것을 보면 기본적으로 3가지는 필요하다
직접 로직을 돌릴 Controller
State를 관리할 IState
각각의 상태 State까지 필요하다.
여기서부턴 프로젝트 상황 / 사람 스타일마다 다르는데 기본적으로 모른다는 가정하에 구조만 보여주려고 하는 것이므로 따라오시면 되고 이후에 보고 원하는 데로 변경해 가면서 사용하시면 될 거 같습니다.
먼저 State들이 공용으로 가질 함수들을 정의할 인터페이스 만든다.
여기서 타입 T 를 모른다면 이해하기 살짝 어렵기 때문에 먼저 공부하고 오면 이해에 더욱 도움이 될꺼같다.
IState.cs
public interface IState<T>
{
void OperateEnter(T sender);
void OperateUpdate(T sender);
void OperateExit(T sender);
}
여기서 보면 3가지 함수를 받아왔는데
void OperateEnter(T sender); - 해당 State 들어왔을때 1회 실행
void OperateUpdate(T sender); - 해당 State 업데이트 할때마다 실행
void OperateExit(T sender); - 해당 State 에서 다른 State 호출로 인해서 현재 State 종료할때 1회 실행
위에서 보면 T로 받아서 모든 형식을 구현했다 그러면 이제 이걸 이용해서 다른 곳에서 이어서 사용해 보자
코드 따라오면 된다.
만들기 전에 이러한 state들은 컨트롤해 주는 스크립트로 돌아가기 때문에
임시적으로 BikeController.cs를 만들어두기만 하자
그러면 이제 이 인터페이스를 통해서 만들 상태 State들을 만들어보자 Idle부터 만들어보자
이 형식으로 나머지 move, turn까지 만들어주자
다 똑같고 클래스 명만 다른 모양으로 나오면 된다.
BikeIdle.cs
public class BikeIdle : MonoBehaviour, IState<BikeController>
{
private BikeController _bikeController;
public void OperateEnter(BikeController sender)
{
_bikeController = sender;
if (_bikeController != null)
{
_bikeController.CurrentSpeed = 0;
}
}
public void OperateExit(BikeController sender)
{
}
public void OperateUpdate(BikeController sender)
{
}
}
BikeMove.cs
public class BikeMove : MonoBehaviour, IState<BikeController>
{
private BikeController _bikeController;
public void OperateEnter(BikeController sender)
{
_bikeController = sender;
_bikeController.CurrentSpeed = _bikeController.maxSpeed;
}
public void OperateExit(BikeController sender)
{
}
public void OperateUpdate(BikeController sender)
{
if (_bikeController)
{
if(_bikeController.CurrentSpeed > 0)
{
_bikeController.transform.Translate(Vector3.up * (_bikeController.CurrentSpeed * Time.deltaTime));
}
}
}
}
BikeTurn.cs
public class BikeTurn : MonoBehaviour, IState<BikeController>
{
private Vector3 _turnDirection;
private BikeController _bikeController;
public void OperateEnter(BikeController sender)
{
if (!_bikeController)
_bikeController = sender;
_turnDirection.x = (float)_bikeController.CurrentTurnDirection;
}
public void OperateExit(BikeController sender)
{
}
public void OperateUpdate(BikeController sender)
{
if (_bikeController.CurrentSpeed > 0)
{
_bikeController.transform.Translate(_turnDirection * _bikeController.turnDistance * Time.deltaTime);
}
}
}
그다음에 이제 컨트롤러에서 state들을 set부터 update까지 요청할 중간에서 핸들링해 주는
코드를 만들자.
StateMachine.cs로 만들자 코드 길이가 좀 있으니 잘 보세요 이해를 돕기 위해 주석도 달아뒀습니다
StateMachine.cs
public class StateMachine<T>
{
private T m_sender;
//현재 상태를 담는 프로퍼티
public IState<T> CurState { get; set; }
//기본 상태를 생성시에 설정하게 생성자 선언
public StateMachine(T sender, IState<T> state)
{
m_sender = sender;
SetState(state);
}
public void SetState(IState<T> state)
{
Debug.Log("SetState : " + state);
// null에러출력
if (m_sender == null)
{
Debug.LogError("m_sender ERROR");
return;
}
if (CurState == state)
{
Debug.LogWarningFormat("Same state : ", state);
return;
}
if (CurState != null)
CurState.OperateExit(m_sender);
//상태 교체.
CurState = state;
//새 상태의 Enter를 호출한다.
if (CurState != null)
CurState.OperateEnter(m_sender);
Debug.Log("SetNextState : " + state);
}
//State용 Update 함수.
public void DoOperateUpdate()
{
if (m_sender == null)
{
Debug.LogError("invalid m_sener");
return;
}
CurState.OperateUpdate(m_sender);
}
}
여기서 로그 찍어둬서 state가 변하는걸 체크할수가있다.
위에서부터 본다면 StateMachine 생성자로 생성하고 초기 세팅해 주고
SetState로 현재 상태 새롭게 설정 요청하고
DoOperateUpdate로 update 사용하는 것처럼 요청해서 현 State에서 돌아가게 해 준다 일단 이렇게만 알고
다음으로 넘어가자
이제 controller에서 지금 까지 작성한 코드를 합쳐서 불러서 사용해 보자.
아까 만들어둔 BikeController.cs를 켜고 코드를 수정하자
BikeController.cs
public class BikeController : MonoBehaviour
{
public enum BikeState
{
Idle,
Move,
Turn,
}
public float maxSpeed = 2.0f;
public float turnDistance = 2.0f;
public float CurrentSpeed { get; set; }
public Direction CurrentTurnDirection { get; private set; }
public enum Direction
{
Left = -1,
Right = 1,
}
private Dictionary<BikeState, IState<BikeController>> dicState = new Dictionary<BikeState, IState<BikeController>>();
private StateMachine<BikeController> sm;
// Start is called before the first frame update
void Start()
{
IState<BikeController> idle = new BikeIdle();
IState<BikeController> move = new BikeMove();
IState<BikeController> turn = new BikeTurn();
dicState.Add(BikeState.Idle, idle);
dicState.Add(BikeState.Move, move);
dicState.Add(BikeState.Turn, turn);
sm = new StateMachine<BikeController>(this, dicState[BikeState.Idle]);
}
void Update()
{
if (Input.GetKeyDown(KeyCode.W))
{
sm.SetState(dicState[BikeState.Move]);
}
if (Input.GetKeyDown(KeyCode.D))
{
CurrentTurnDirection = Direction.Right;
sm.SetState(dicState[BikeState.Turn]);
}
if (Input.GetKeyDown(KeyCode.A))
{
CurrentTurnDirection = Direction.Left;
sm.SetState(dicState[BikeState.Turn]);
}
if (Input.GetKeyDown(KeyCode.Space))
{
sm.SetState(dicState[BikeState.Idle]);
}
sm.DoOperateUpdate();
}
}
상태들은 BikeState enum으로 상태를 두고 그 값을 Dictionary로 관리했다
Start 보면 아까 작성한 State를 값을 상태값이랑 넣고 생성자를 통해 만든 걸 볼 수 있다.
구현을 위해서 if 로만 구별해서 넘어가는 걸 보여주기 위해서 작성했다.
그다음 유니티로 가서 상자 하나 두고 거기에 controller 넣고 실행 후 확인해 보자.
상태를 변해 가면서 동작하는 걸 확인할 수 있다.
만약 분리 안 했으면 한 스크립트 안에 모든 걸 집어넣어야 했다.
유지 및 관리 : 이게 가장 중요한 부분인 거 같은데 바로 긴 조건문이나 점점 커지는 클래스 길이를 줄일 수 있다. 또한 새로운 상태가 생겨도 금방 추가해 줄 수 있다. 만약 스턴만 있는데 빙결 슬로 혼란 등등 새로운 애들 넣어야 한다면 그러한 상태만 추가해 주면 금방 구현해 버린다.
이게 이번에는 State 이동할때 각각에 State를 받을때 클래스는 MonoBehaviour를 같이 상속 받았는데 없이 그냥 핸들 컨트롤러만 받는 방법도 있다. 이거는 각자 사용 스타일에 따라서 사용하면 될꺼같다.
잘 학습하고 갑니다. 감사합니다!!