[UNITY] 여러 에이전트가 사용 가능한 FSM

Doorbals·2022년 9월 7일
0

UNITY

목록 보기
2/4

여러 에이전트가 사용 가능한 FSM

1. 일반화 프로그래밍을 이용한 State Class

  • 이전의 State Class는 한 클래스 형식의 에이전트만 사용 가능.
  • 따라서 클래스나 메소드 내부의 내용은 같지만 사용되는 데이터 형식이 다를 때 사용하는 일반화 프로그래밍을 활용해 재사용 가능하게 변경.
// T에 올 수 있는 타입을 class로 한정
public abstract class State<T> where T : class 
{
	// 해당 상태 시작 시 1회 호출
	public abstract void Enter(T entity);
    
    // 해당 상태 업데이트 시 매 프레임 호출
    public abstract void Execute(T entity);
    
    // 해당 상태 종료시 1회 호출
    public abstract void Exit(T entity);
}

2. 일반화 프로그래밍을 이용한 Student 클래스 (에이전트 클래스)

  • 맨 아래 부분에 Student가 가지고 있는 상태를 저장하는 states, currentState 변수에 한정 매개변수를 지정해줘야 한다.
public enum StudentStates // 플레이어가 가질 수 있는 상태
{RestAndSleep, StudyHard, TakeAExam, PlayAGame, HitTheBottle}

public class Student
{
	// Student의 속성들
	private int knowledge;				// 지식
    private int stress;					// 스트레스
    private int fatigue;				// 피로
    private int totalScore;				// 총 점수
    private Locations currentLocation;	// 현재 위치
    
    // Student가 가지고 있는 모든 상태, 현재 상태
    private State<Student>[] states;
    // stateMachine 클래스에 현재 상태 관리 위임했으므로 해당 부분 삭제
    // private State<Student> currentState; 
    // 대신에 stateMachine 변수 선언
    private StateMachine<Student> stateMachine;
    ...
    
    public void SetUp(string name)
    {
    	// states에 State 클래스 형식의 객체가 5개 들어가는 배열 대입 
    	states = new State[5]; 
        
        // states의 각 인덱스에 각 클래스 메모리 할당
        states[(int)StudentStates.RestAndSleep] = new StudentOwnedStates.RestAndSleep(); 
        states[(int)StudentState.StudyHard] = new StudentOwendStates.StudyHard();
        states[(int)StudentState.TakeAExam] = new StudentOwendStates.TakeAExam();
        states[(int)StudentState.PlayAGame] = new StudentOwendStates.PlayAGame();
        states[(int)StudentState.HitTheBottle] = new StudentOwendStates.HitTheBottle();
        
        // stateMachine 클래스에 현재 상태 관리 위임했으므로 해당 부분 삭제
        // 현재 상태를 RestAndSleep 상태로 설정
       	//ChangeState(StudentStates.RestAndSleep);
        
        // 상태를 관리하는 StateMachine에 메모리 할당하고, 첫 상태 결정
        stateMachine = new StateMachine<Student>();
        // stateMachine.Setup()에서 ChangeState()를 호출하기 때문에 처음 상태를 매개변수로 전달
        stateMachine.Setup(this, states[(int)StudentStates.RestAndSleep]);
        
        knowledge = 0;				
        stress = 0;					
        fatigue = 0;			
        totalScore = 0;				
        currentLocation = Location.SweetHome;	
    }
    
    public void Updated()
    {
    	// currentState가 비어있지 않으면 현재 상태의 Execute 메서드 실행
    	//if(currentState != null)
        //	currentState.Execute(this); // this는 Student 클래스 객체
        
        stateMachine.Execute(); // stateMachine에 정의된 Execute() 호출
    }
    
   	public void ChangeState(StudentStates newState)
    {
    	// 새로 바꾸려는 상태가 비어있으면 상태를 바꾸지 않고 return
    	// if(states[(int)newState] == null) return;
        
        // 현재 상태를 종료하기 위해 Exit() 메소드 호출
        // if(currentState != null)
        // currentState.Exit(this);
            
        // 새로운 상태로 변경하고, 새로 바뀐 상태의 Enter() 메서드 호출
        // currentState = states[(int)newState]
        // currentState.Enter(this);
        
        stateMachine.ChangeState(states[(int)newState]) // stateMachine에 정의된 ChangeState() 호출
    }
}

3. 일반화 프로그래밍을 이용한 StudentOwnedStates 네임스페이스 (에이전트의 동작 클래스들)

  • State Class를 일반화 시켜주었기 때문에 StudentOwnedStates 네임스페이스 내부에 존재하는 State Class를 상속받는 메서드들에 한정 매개변수를 지정해주어야 한다.
namespace StudentOwnedStates
{
	public class RestAndSleep : State<Student>
    {
    	public override void Enter(Student entity) {...}
        public override void Execute(Student entity) {...}
        public override void Exit(Student entity) {...}
    }
    
    public class StudyHard : State<Student>
    {
    	public override void Enter(Student entity) {...}
        public override void Execute(Student entity) {...}
        public override void Exit(Student entity) {...}
    }
    ...
}

4. StateMachine Class의 필요성

  • 상태 관리에 대한 코드가 에이전트 내부에 있으면 다른 종류의 에이전트를 제작할 때 상태 관리에 대한 코드를 또 작성해야 함.
  • 때문에 상태와 연관된 모든 데이터와 메소드를 하나의 클래스로 캡슐화함으로써 코드 재사용을 줄일 수 있다.
  • 에이전트는 StateMachine Class를 참조 클래스로 사용하여 인스턴스를 소유하고, 현재 상태에 대한 관리, 상태 변경 등에 대한 처리 위임 가능.
  • 전역 상태(Global State) : 에이전트가 어떤 상태를 수행할 때, 모든 상태에서 지속적으로 업데이트 되어야 하는 조건 논리가 있을 때 이 논리를 소유하고 있는 상태
    => 현재 상태와 별개로 작동하는 상태 변수를 만들어 관리
  • 상태 블립(State Blip) : 에이전트의 상태가 변경될 때, 직전 상태로 복귀한다는 조건 하에 다른 상태로 변경하는 것.
    => 이런 형식의 기능성을 FSM에 부여하기 위해 반드시 이전 상태를 기억해두고 상태 블립이 그 상태로 전환시킬 수 있게 해야 한다.
public class StateMachine<T> where T : class
{
	private T ownerEntity; // StateMachine의 소유주
    private State<T> currentState; // 현재 상태
    private State<T> globalState; // 전역 상태
    private State<T> previousState; // 이전 상태
    
    // StateMachine을 참조 클래스로 사용하는 에이전트에서 호출
    public void SetUp(T owner, State<T> entryState)
    {
    	ownerEntity = owner;
        currentState = null;
        globalState = null;
        previousState = null;
        
        // entryState 상태로 상태 변경
        ChangeState(entryState);
    }
    
    public void Execute()
    {
    	if(globalState != null)
        {
        	globalState.Execute(ownerEntity);
        }
    
    	if(currentState != null)
        {
        	currentState.Execute(ownerEntity);
        }
    }
    
    public void ChangeState(State<T> newState)
    {
    	// 새로 바꾸려는 상태가 비어있으면 상태 변경 X
        if(newState == null) return;
        
        // 현재 재생 중인 상태가 있으면 Exit() 메서드 호출
        if(currentState != null)
        {
        	// 상태 변경되면 현재 상태가 이전 상태가 됨.
        	previousState = currentState;
        	currentState.Exit(ownerEntity)
        }
        
        // 새로운 상태로 변경하고, 새로 바뀐 상태의 Enter() 메서드 호출
        currentState = newState;
        currentState.Enter(ownerEntity);
    }
    
    public void SetGlobalState(State<T> newState)
    {
    	globalState = newState;
    }
    
    // 이전 상태로 복귀
    public void RevertToPreviousState()
    {
    	ChangeState(previousState);
    }
}

5. 또 다른 에이전트 클래스인 Unemployed 클래스

  • Student 클래스와 같은 에이전트 클래스
public enum UnemployedStates {RestAndSleep, PlayAGame, HitTheBottle, VisitBathroom, Global}

public class Unemployed 
{
	// Unemployed의 멤버 변수
	private int bored;
    private int stress;
    private int fatigue;
    private Locations currentLocation;
    
    public UnemployedStates CurrentState {private set; get;} // 현재 상태
    
    // Unemployed가 가지고 있는 모든 상태, 상태 관리자
    private State<Unemployed>[] states;
    private StateMachine<Unemployed> stateMachine;
    
    // 각 멤버 변수에 대한 Property 정의
    public int Bored
    {
    	set => bored = Mathf.Max(0, value);
        get => bored
    }
    public int Stress {...}
    public int fatigue {...}
    public Locations CurrentLocation {...}
    
    public void Setup()
    {
    	// Unemployed가 가질 수 있는 상태 개수만큼 메모리 할당, 각 상태에 클래스 메모리 할당
    	states = new State<Unemployed>[5];
        states[(int)UnemployedStates.RestAndSleep] = new UnemployedOwnedStates.RestAndSleep();
        states[(int)UnemployedStates.PlayAGame] = new UnemployedOwnedStates.PlayAGame();
        states[(int)UnemployedStates.HitTheBottle] = new UnemployedOwnedStates.HitTheBottle();
        states[(int)UnemployedStates.VisitBathroom] = new UnemployedOwnedStates.VisitBathroom();
        states[(int)UnemployedStates.Global] = new UnemployedOwnedStates.Global();
    
    	// 상태를 관리하는 StateMachine에 메모리 할당 및 첫 상태 결정
    	stateMachine = new StateMachine<Unemployed>();
        stateMachine.Setup(this, states[(int)UnemployedStates.RestAndSleep]);
        stateMachine.SetGlobalState(states[(int)UnemployedStates.Global]);
        
        bored = 0;
        stress = 0;
        fatigue = 0;
        currentLocation = Location.SweetHome;
	}
    
    public void Updated()
    {
    	stateMachine.Execute();
    }
    
    public void ChangeState(UnemployedStates newState)
    {
    	CurrentState = newState;
    	stateMachine.ChangeState(states[(int)newState]);
    }
    
    public void RevertToPreviousState()
    {
    	stateMachine.RevertToPreviousState();
    }
}

6. UnemployedOwnedStates 네임스페이스 (에이전트의 동작 클래스들)

  • StudentOwnedStates와 동일한 방식
namespace UnemployedOwnedStates
{
	public class RestAndSleep : State<Unemployed>
    {
    	public override void Enter(Unemployed entity) {...}
        public override void Execute(Unemployed entity) {...}
        public override void Exit(Unemployed entity) {...}
    }
    
    public class PlayAGame : State<Unemployed>
    {
    	public override void Enter(Unemployed entity) {...}
        public override void Execute(Unemployed entity) {...}
        public override void Exit(Unemployed entity) {...}
    }
    
    public class VisitBathroom : State<Unemployed>
    {
    	public override void Enter(Unemployed entity) {...}
        public override void Execute(Unemployed entity)
        {
        	// 바로 직전 상태로 돌아감.
        	entity.RevertToPreviousState();
        }
        public override void Exit(Unemployed entity) {...}
    }
    
    // 전역 상태. 현재 상태와 별개로 실행됨.
    public class StateGlobal : State<Unemployed>
    {
    	public override void Enter(Unemployed entity) {...}
        public override void Execute(Unemployed entity)
        {
        	if(entity.CurrentState == UnemployedStates.VisitBathroom)
            {
            	return;
            }
            
            // 10% 확률로 "VisitBathroom" 상태로 변경
            int bathroomState = Random.Range(0, 100);
            if(bathroomState < 10)
            {
            	entity.ChangeState(UnemployedStates.VisitBathroom);
			}
        }
        public override void Exit(Unemployed entity) {...}
    }
}

출처 : 고박사의 유니티 노트 - FSM3 (링크)

profile
게임 클라이언트 개발자 지망생의 TIL

0개의 댓글