Game Development, 게임개발/개발

FSM,유한 상태 기계, Finite State Machines [Unity]

게임이 더 좋아 2021. 3. 25. 04:57
반응형
728x170

 

 

무슨 기계공학과에서 배우는 단어 같이 생겼다.

아니면 무슨 고체전자물리처럼 Solid State Physics 같이 무슨 State가 들어간다.

 

맞다.. 회로 꾸밀 때 나는 swtich문 많이 썼던 것 같기도 하고..

Mealy, Moore 했었는데 다 까먹은듯 데헿

 

 

하지만 여기서는 프로그래밍 스타일, 패턴 중에 하나로 알아볼 만한 가치가 있다.

그래서 알아보기로 했다.

 


 

FSM의 간단하게 정의를 하고 가자.

 

각 State가 유한하게 있고 State를 전이시키기 위한 조건이 있다.

그것을 표현하는 방법이라고 말할 수 있다.

 

 

그럼 게임 개발에서 FSM의 가지는 의미는 무엇이냐?

 

“finite State Machine is a structure that allows to define complex behaviors

유한 개의 상태를 가지고 주어지는 입력에 따라 어떤 상태에서 다른 상태로 전이되거나 또다른 출력 같은 것들이 일어나게 하는 장치를 나타낸 모델이라고 한다.

 

간단하게 사용자와 게임 안의 플레이어의 관계를 생각하면 되겠다.

 

이렇게 설명하면 어렵다.

예를 들어서 설명하자

 

상태

하는 일

Walk

달리는 버튼을 누르면 Run 상태로 이동한다.

현재 방향을 유지하며 걷는다.

버튼을 누르지 않으면 Idle 상태로 이동한다.

Run

버튼을 누르지 않으면 Idle 상태로 이동한다.

현재 방향을 유지하며 뛴다.

걷는 버튼을 누르면 Walk 상태로 이동한다.

Idle

걷는 버튼을 누르면 Walk 상태로 이동한다.

뛰는 버튼을 누르면 Run 상태로 이동한다.

 

음.. 그니까 각 상태에서 기능을 작동시키거나 다른 상태로 변화하고 싶을 때 if문, Switch 등 같은 Logic을 사용해서 작동시킬 수 있다.

그렇지만 만약 복잡해진다면 다른 상태로 변화하거나 기능을 작동시키는 것이 빠르게 제대로 작동하지 않을 수 있다.

 

즉, 게임이 방대해질수록 이렇게 단순 if문 같은 Logic으로 구성되어 있으면

코드의 가독성과 이해를 떨어뜨릴 수 있다는 말이다.

 

 

 

**그래서 FSM을 구성해주는 좋은 그래픽기반 툴이 있다. 

그래서 각 상태와 형태가 코드가 아닌 그래프로 전반적인 상황을 이해시킬 수 있다. 

또한 각각의 상태가 나누어져있어서 새로운 상태를 추가시키거나 수정, 삭제하는 것이 자유롭다..

 

 

 

**마치 UML 의 State Diagram과 느낌이 비슷하다. -> 역시 목적 또한 "이해하기 쉽고 개발도 용이하게 해준다"는 면에서 같다.

 

 

**일반적으로 자동화된 반응을 이끌어내기 위한 몹(Mob)에 적용되곤 한다.

[Idle] - 주변 돌아다님

*플레이어가 주변에 있으면

[Alert] - 플레이어 주변에 오면 위협함

*플레이어가 때리면

[Attack] - 때림

*싸움 시작 후 3초 지나면

[Call colleagues] - 다굴

 

이런식으로 상태가 전이될 수 있다.

 


 

그래서 어떻게 만드는건데...?

 

우리가 Unity에서 선언하는 is~~같은 조건들이 모두~ 다 상태 전이의 조건이 되는 것이다.

?? 우리도 모르게 상태를 변화시킨 것이다. 다만 모르고 있었을 뿐.

 

Public Button
{
   Bool isButtonPressed = false.
     
   While (true)
  {
     If (isButtonPressed) {
     //버튼이 눌려져있으면?
     }
      else {
        //안 눌러져있으면?
     }
  }
}

 

그렇다고 해서 상태가 늘어났을 때도 그저 bool로 구분을 한다???

그렇게 되면 코드는 If-Else 문의 놀이터가 되어버린다.

가독성이 엄청 떨어지고 버그가 생길 확률이 높으며, 어디서 버그가 나온지도 모른다.

또한 만약 2가지가 한꺼번에 True가 나올 경우 어떻게 하겠는가?? 의 문제도 생긴다.

 

 

위와 같은 방식은 Simple Finite State라고 한다.

++simple인지 아닌지는 중요하지 않다.

 

그래서 응용하면...

다르게도 사용할 수 있다.

 

예를 들어 살펴보면

아래 그림처럼 4가지 상태가 존재한다

만들 수 있겠지?

 

 

상태의 수가 늘어 복잡할 것 같다.

하지만 복잡할 때 빛을 발하는 것이 바로 이 FSM이다.

 


 

그래서 조금 더 복잡해지면 어떻게 되느냐??

그렇다면 이렇게 if문의 반복대신 이렇게 enum과 switch,case문을 사용하여 표현할 수 있다.

 

Public PlayerBheavior {
     
     //플레이어의 상태들
    Enum PlayerState {
        Idle,
        Jump,
        Run,
        Attack
        }
}
 
PlayerState actualPlayerState = PlayerState.Idle;
 
Void Update() {
	//현 상태를 업데이트함.
    Switch (actualPlayerState) {
    case PlayerState.Idle:
        //초기상태를~
    break;
    case PlayerState.Jump:
        //뛰는 ~
    break;
    case PlayerState.Run:
        //달리는~
    break;
    case PlayerState.Attack:
        //공격을 ~
    break;      
        }
}

 

if-else문의 놀이터도 아니며 우리는 2가지 이상 값이 True가 나와 오류가 생기는 걱정을 하지 않아도 된다.

 

하지만 문제가 생기는 것이 있다.

그림을 다시 보면

Idle에서 Run을 양방향으로 상태 전이가 가능하지만

Run에서 Jump같은 경우 바로 전이가 가능하지 않게 설정했다.

 

이 경우에는 어떻게 해야할까??

 

제어해줘야 한다. 당연히 밑에 3개의 스크립트를 보며 이해해보자

이해하기에 나는 약간 더 시간이 필요하다.

 

public class Program
    {
        static void Main(string[] args)
        {
            ChildFSM fsm = new ChildFSM();
            Console.WriteLine("Current State = " + fsm.currentState);
            //MoveNext used to change the state
            Console.WriteLine("Command.Begin: Current State = " + fsm.MoveNext(PlayerState.Run));
            //CanReachNext used to check if the next state it's reachable
            Console.WriteLine("Invalid transition: " + fsm.CanReachNext(PlayerState.Idle));
            Console.WriteLine("Previous State = " + fsm.previous);
        }
    }

1. Main.cs

using System;
using System.Collections.Generic;


public class BaseFSM <T> where T : struct, IConvertible
{
	#region State Transition
	//Basic class that denote the transition between one state and another
    
	public class StateTransition
	{
		public  T currentState { get; set; }
		public  T nextState { get; set; }			
		
		//StateTransition Constructor
        // 상태를 전이
		public StateTransition(T currentState, T nextState)
		{
			this.currentState = currentState;
			this.nextState = nextState;
		}
		
		public override int GetHashCode()
		{
			return 17 + 31 * this.currentState.GetHashCode() + 31 * this.nextState.GetHashCode();
		}
		
		public override bool Equals(object obj)
		{
			StateTransition other = obj as StateTransition;
			return other != null && this.currentState.Equals(other.currentState) && this.nextState.Equals(other.nextState);
		}
	}
	#endregion

	#region BaseFsm Implementation
	protected Dictionary<StateTransition, T> transitions; //Will contain all the transitions inside the FSM
	public T currentState;
	public T previusState;
	
	protected BaseFSM() {
		// Throw Exception on static initialization if the given type isn't an enum.
		if(!typeof (T).IsEnum) 
			throw new Exception(typeof(T).FullName + " is not an enum type.");
	}
	
	private T GetNext(T next)
	{
		StateTransition transition = new StateTransition(currentState, next);
		T nextState;
		if (!transitions.TryGetValue(transition, out nextState))
			throw new Exception("Invalid transition: " + currentState + " -> " + next);
		Console.WriteLine("Next state " + nextState);
		return nextState;
	}
	
	//Used to check if the next state is reachable
	public bool CanReachNext(T next) {
		StateTransition transition = new StateTransition(currentState, next);
		T nextState;
		if (!transitions.TryGetValue(transition, out nextState)){
			Console.WriteLine("Invalid transition: " + currentState + " -> " + next);
			return false;
		}
		else {
			return true;				
		}			
	}

	public T MoveNext(T next)
	{
		previusState = currentState;
		currentState = GetNext(next);
		Console.WriteLine("Change state from " + previusState + " to " + currentState);
		return currentState;
	}
	#endregion
}

2.BaseFSM

using System;
using System.Collections.Generic;

namespace FSM
{
	//State를 정의한다. state machine이니까.
	public enum PlayerState
	{
		Idle,
		Run,
		Jump
	}
	
    
	public class ChildFSM : BaseFSM<PlayerState> // Inherit 받는다.
	{
		public ChildFSM () : base()
		{			
			this.currentState = PlayerState.Idle;
			//Define the state machine transitions
			this.transitions = new Dictionary<StateTransition, PlayerState>
			{
				{ new StateTransition(PlayerState.Idle, PlayerState.Run), PlayerState.Run }, //From Idle state to Run state 
				{ new StateTransition(PlayerState.Run, PlayerState.Jump), PlayerState.Jump }, 
			};	
		}
		
	}
}

3.ChildFSM

이렇게 하면 상태 전이를 제어할 수가 있다. 

 


 

 

그리고 더 복잡해진다면 우리는 이러한 메서드를 따로 쓸 수도 있을 것이다.

 

Void OnStateEnter(PlayerState state);
Void OnStateExecution(PlayerState state);
Void OnStateExit(PlayerState state);

?? 어디서 많이 봤다??

State에 진입할 때, State에 있을 때, State애서 벗어날 때

여느 Collision 과 비슷하다. 바로 그거다.

 

 

위처럼 정의한다고 해도

만약 하나의 클래스 안에 모든 각 상태,state에 대하여 써있다고 생각해보자

읽을 수 없게 어마어마하게 복잡해질 수 있다.

 

그래서 생각해냈다.

interface라는 것을 이용하기로

 

public interface IState // 인터페이스로 선언
{
 
    void OnStateExecution();
    void OnStateEnter ();
    void OnStateExit ();
    
}

 

오 있어보이는데?

 

public class AttackState: IState {
     
    Void OnStateEnter() {
        //상태에 들어왔을 때 할 행동 정의
    }
 
    Void OnStateExecution() {
    //Define your execution behavior here
    } 중 행동 정의
 
    Void OnStateExit() {
    //벗어날 때 행동 정의
    }
}

 

이렇게 정의하게 된다면

우리는 그저 Player에 이렇게 선언하면 된다.

public class Player {
    AttackState attackState = New AttackState();
    IdleState IdleState = New IdleState();  
    Istate currentState = Idle;
}

 

 

그리고 만약 상태가 바뀔 때 작동하도록 메서드 하나 정의하고

Private void ChangeStateExampleMethos(Istate nextState)
{
    currentState.OnStateExit();
    nextState.OnStateEnter();
    currentState = nextState;
} 

 

 

이제는 실행만 하면 된다.

Void Update() {
    currentState.OnStateExecution();
}

 


 

근데 여기서 문제가 생길 수 있다.

만약 점프할 때 공격이 되게한다면..?

뛰면서 점프가 가능하게 한다면..??

 

 

그렇다면 Jump and Attack이라는 상태를 또 만들어야 할까???

단지 공격에 점프만 추가된 것 뿐인데..??

 

비슷한 상태, Similar State 는 상속을 받아서 이용한다.

 

그래서 우리는 Inheritance, 상속을 쓴다.

Void JumpAttackState : AttackState {
//이렇게
}

 

 

 

일반적으로 FSM을 쓸 수 있는 경우가 3가지 있다.

 

  1. 동작들이 유한한 상태들의 집합으로 정의되는 경우
  2. 상태들이 외부나 내부적인 액션이나 트리거에 의해 바뀔 경우
  3. 동작들이 너무 복잡하진 않을 때

 

 

FSM을 나중에 다시 한 번 이해시킬 수 있도록 실력을 쌓아 오겠다.

 


참고링크

 

github.com/MarcoMig/Finite-State-Machine-FSM/blob/master/FSM/Main.cs

 

728x90
반응형
그리드형