숙련 주차 팀 프로젝트가 시작됐다.
Monster
클래스에 FSM
을 적용하기로 했는데, FSM
을 자동화 하기 위해서 Generic으로 State 열거형을 받아와서 event Dictionary에 key로 등록해보려했다.
where T : Enum
으로 제네릭에 조건을 걸었는데도 명시적 캐스트가 안통한다...
as 연산자도 안통한다...
이것저것 막 바꿔보다가 IDE의 IntelliSense 자동완성에
var enums = Enum.GetValues(typeof(T)) as T[];
이게 나오길래 해봤더니, 잘 동작한다.
최고다 자동완성 ,,
해결을 하고 나서, 또 다른 해결법을 하나 더 찾았다.
두 번째로 틀렸던 방법에서 as연산자를 쓸게 아니라, 명시적 타입 캐스트를 해줘도 됐었다...
위 기능을 이용해서 제네릭으로 enum type을 받아서, 자동으로 callback Dictionary를 만들어주는 상태 머신을 작성해봤다.
IStateMachine<T> interface
using System;
public interface IStateMachine<T> where T : Enum
{
void OnStateEnter();
void OnStateStay();
void OnStateExit();
void StateTransition(T state);
void BindEvent(T state, StateEvent @event, Action action);
}
public enum StateEvent
{
Enter,
Stay,
Exit,
}
StateMachine<T> class
using System.Collections.Generic;
using System;
using UnityEngine;
public class StateMachine<T> : IStateMachine<T> where T : Enum
{
// 현재 상태
private T _state;
public T CurrentState => _state;
// Generic으로 받아온 열거형을 이용해 각 상태에 따라 호출될 callback들을 저장할 Dictionary를 만듭니다.
private Dictionary<StateEvent, Dictionary<T, Action>> _events = new();
// 생성자에서 Dictionary를 초기화합니다.
public StateMachine()
{
for (int i = 0; i < Enum.GetValues(typeof(StateEvent)).Length; i++)
{
_events[(StateEvent)i] = new Dictionary<T, Action>();
var enums = Enum.GetValues(typeof(T)) as T[];
for (int j = 0; j < enums.Length; j++)
_events[(StateEvent)i].Add(enums[j], null);
}
}
// 현재 상태에 알맞는 callback을 Dictionary에서 찾아 호출합니다.
public void OnStateEnter() => _events[StateEvent.Enter][_state]?.Invoke();
public void OnStateExit() => _events[StateEvent.Exit][_state]?.Invoke();
public void OnStateStay() => _events[StateEvent.Stay][_state]?.Invoke();
// 상태를 전이하는 메서드입니다.
public void StateTransition(T state)
{
// 바뀔 상태가 현재 상태와 같다면 실행하지 않습니다.
if (state.Equals(_state))
return;
// 현재 상태의 Exit callback을 호출하고,
// 상태를 바꾼 뒤 바뀐 상태의 Enter callback을 호출합니다.
OnStateExit();
_state = state;
OnStateEnter();
}
// Dictionary에 callback을 등록합니다.
public void BindEvent(T state, StateEvent @event, Action action)
{
_events[@event][state] += action;
}
}
Monster class
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditorInternal;
using UnityEngine;
public class Monster : Creature
{
// FSM에 사용할 상태들을 열거형으로 선언합니다.
public enum MonsterState
{
Idle,
Aggro,
Attack,
Dead,
}
private Player _player;
private float _detectRange;
private StateMachine<MonsterState> _fsm; // 선언한 열거형으로 FSM 선언
public MonsterState CurrentState => _fsm.CurrentState;
public float DetectRange { get => _detectRange; set => _detectRange = value; }
protected virtual void Start()
{
Initialize();
}
protected virtual void Update()
{
// StateStay는 매 프레임 호출합니다.
_fsm.OnStateStay();
}
public void Initialize()
{
// FSM을 생성하고,
// 테스트용으로 Idle 상태일 때 매 프레임 로그를 찍는 callback을 등록했습니다.
_fsm = new();
_fsm.BindEvent(MonsterState.Idle, StateEvent.Stay, () => Debug.Log("Stay.."));
}
}
Monster
클래스에서 MonsterState
로 선언한 기본, 어그로, 공격, 죽음 상태로 바인딩 되는 callback들을 저장할 Dictionary
가 자동으로 만들어진다!
나중에 Player
클래스나, 더 많은 상태를 가지는 또 다른 몬스터 등을 만들 때, 그냥 상태를 정의한 열거형 타입만 넘겨주면 되는 구조로 짜봤다.