현업에서도 많이 쓰이는 중요한 패턴
유한상태기계
상태를 명확하게 나눔으로써 내 행동을 클래스단위로 나눌 수 있고 이를 통해 버그를 방지할 수 있다.
상태를 체크할 때 bool을 사용하지 않아도 된다.
데이터를 제어하려면 스크립터블 오브젝트를 사용하거나 MonoBehaviour를 상속받아서 이를 컴포넌트화 시켜야한다.
(MonoBehaviour는 new로 생성하지 않고 Awake나 Start에서 초기화함
주체가 되는 State Machine
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public enum EMyState
{
IdleMyState,
MoveMyState,
StunMyState
}
public interface IMyState
{
void EnterState();
void ExcuteState();
void ExitState();
}
public class IdleMyState : IMyState
{
public void EnterState()
{
}
public void ExcuteState()
{
}
public void ExitState()
{
}
}
public class MoveMyState : IMyState
{
public void EnterState()
{
}
public void ExcuteState()
{
}
public void ExitState()
{
}
}
public class StunMyState : IMyState
{
public void EnterState()
{
}
public void ExcuteState()
{
}
public void ExitState()
{
}
}
public class StateMachine : MonoBehaviour
{
[SerializeField] private EMyState defaultState;
private IMyState _currentMyState;
private Dictionary<EMyState, IMyState> _states = new();
private void ChangeState_Internal(IMyState newMyState)
{
if (_currentMyState != null)
{
_currentMyState.ExitState();
}
_currentMyState = newMyState;
_currentMyState.EnterState();
}
public void ChangeState(EMyState state)
{
ChangeState_Internal(_states[state]);
}
// Start is called before the first frame update
void Start()
{
_states.Add(EMyState.IdleMyState, new IdleMyState());
_states.Add(EMyState.MoveMyState, new MoveMyState());
_states.Add(EMyState.StunMyState, new StunMyState());
// DefaultState
ChangeState(EMyState.IdleMyState);
}
// Update is called once per frame
void Update()
{
if (_currentMyState != null)
{
_currentMyState.ExcuteState();
}
}
}
구현 FSM 기초코드
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public enum EMyState
{
IdleMyState,
MoveMyState,
StunMyState
}
public interface IMyState
{
void EnterState();
void ExcuteState();
void ExitState();
}
public abstract class VMyState : IMyState
{
public StateMachine StateMachine;
public abstract void EnterState();
public abstract void ExcuteState();
public abstract void ExitState();
}
public class IdleMyState : VMyState
{
public override void EnterState()
{
}
public override void ExcuteState()
{
if (Input.GetKey(KeyCode.W))
{
StateMachine.ChangeState(EMyState.MoveMyState);
}
else if (Input.GetKey(KeyCode.F))
{
StateMachine.ChangeState(EMyState.StunMyState);
}
}
public override void ExitState()
{
}
}
public class MoveMyState : VMyState
{
public override void EnterState()
{
}
public override void ExcuteState()
{
if (Input.GetKey(KeyCode.W))
{
StateMachine.transform.position += StateMachine.transform.forward * (Time.deltaTime * 10);
}
else if (Input.GetKey(KeyCode.S))
{
StateMachine.transform.position -= StateMachine.transform.forward * (Time.deltaTime * 10);
}
else if (Input.GetKey(KeyCode.F))
{
StateMachine.ChangeState(EMyState.StunMyState);
}
else
{
StateMachine.ChangeState(EMyState.IdleMyState);
}
}
public override void ExitState()
{
}
}
public class StunMyState : VMyState
{
IEnumerator Stun()
{
yield return new WaitForSeconds(3.0f);
StateMachine.ChangeState(EMyState.IdleMyState);
}
public override void EnterState()
{
StateMachine.StartCoroutine(Stun());
}
public override void ExcuteState()
{
}
public override void ExitState()
{
}
}
public class StateMachine : MonoBehaviour
{
[SerializeField] private EMyState defaultState;
private IMyState _currentMyState;
private Dictionary<EMyState, IMyState> _states = new();
private void ChangeState_Internal(IMyState newMyState)
{
if (_currentMyState != null)
{
_currentMyState.ExitState();
}
_currentMyState = newMyState;
_currentMyState.EnterState();
}
public void ChangeState(EMyState state)
{
ChangeState_Internal(_states[state]);
}
// Start is called before the first frame update
void Start()
{
_states.Add(EMyState.IdleMyState, new IdleMyState() {StateMachine = this});
_states.Add(EMyState.MoveMyState, new MoveMyState() {StateMachine = this});
_states.Add(EMyState.StunMyState, new StunMyState() {StateMachine = this});
// DefaultState
ChangeState(EMyState.IdleMyState);
}
// Update is called once per frame
void Update()
{
if (_currentMyState != null)
{
_currentMyState.ExcuteState();
}
}
}
유니티식 FSM
StateMachine.cs
using System;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public enum EMyState
{
IdleMyState,
MoveMyState,
StunMyState
}
public interface IMyState
{
void EnterState();
void ExcuteState();
void ExitState();
}
public abstract class VMyState : MonoBehaviour, IMyState
{
public StateMachine StateMachine;
public abstract void EnterState();
public abstract void ExcuteState();
public abstract void ExitState();
}
public class StateMachine : MonoBehaviour
{
[SerializeField] private EMyState defaultState;
private IMyState _currentMyState;
private Dictionary<EMyState, IMyState> _states = new();
private void ChangeState_Internal(IMyState newMyState)
{
if (_currentMyState != null)
{
_currentMyState.ExitState();
}
_currentMyState = newMyState;
_currentMyState.EnterState();
}
public void ChangeState(EMyState state)
{
ChangeState_Internal(_states[state]);
}
// Start is called before the first frame update
void Start()
{
_states.Add(EMyState.IdleMyState, new IdleMyState() {StateMachine = this});
_states.Add(EMyState.MoveMyState, new MoveMyState() {StateMachine = this});
_states.Add(EMyState.StunMyState, new StunMyState() {StateMachine = this});
// DefaultState
ChangeState(EMyState.IdleMyState);
}
// Update is called once per frame
void Update()
{
if (_currentMyState != null)
{
_currentMyState.ExcuteState();
}
}
}
IdleMyState.cs
using UnityEngine;
public class IdleMyState : VMyState
{
public override void EnterState()
{
}
public override void ExcuteState()
{
if (Input.GetKey(KeyCode.W))
{
StateMachine.ChangeState(EMyState.MoveMyState);
}
else if (Input.GetKey(KeyCode.F))
{
StateMachine.ChangeState(EMyState.StunMyState);
}
}
public override void ExitState()
{
}
}
MoveMyState.cs
using UnityEngine;
public class MoveMyState : VMyState
{
public override void EnterState()
{
}
public override void ExcuteState()
{
if (Input.GetKey(KeyCode.W))
{
StateMachine.transform.position += StateMachine.transform.forward * (Time.deltaTime * 10);
}
else if (Input.GetKey(KeyCode.S))
{
StateMachine.transform.position -= StateMachine.transform.forward * (Time.deltaTime * 10);
}
else if (Input.GetKey(KeyCode.F))
{
StateMachine.ChangeState(EMyState.StunMyState);
}
else
{
StateMachine.ChangeState(EMyState.IdleMyState);
}
}
public override void ExitState()
{
}
}
StunMyState.cs
using System.Collections;
using UnityEngine;
public class StunMyState : VMyState
{
IEnumerator Stun()
{
yield return new WaitForSeconds(3.0f);
StateMachine.ChangeState(EMyState.IdleMyState);
}
public override void EnterState()
{
StateMachine.StartCoroutine(Stun());
}
public override void ExcuteState()
{
}
public override void ExitState()
{
}
}
유니티에서 스테이트를 컴포넌트 화 시킨다음에 자동 등록 시키는 방법 코드
using System;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public enum EMyState
{
IdleMyState,
MoveMyState,
StunMyState
}
public interface IMyState
{
void EnterState();
void ExcuteState();
void ExitState();
}
public abstract class VMyState : MonoBehaviour, IMyState
{
[NonSerialized]public StateMachine StateMachine;
public abstract void EnterState();
public abstract void ExcuteState();
public abstract void ExitState();
}
public class StateMachine : MonoBehaviour
{
[SerializeField] private EMyState defaultState;
private IMyState _currentMyState;
private Dictionary<EMyState, IMyState> _states = new();
private void ChangeState_Internal(IMyState newMyState)
{
if (_currentMyState != null)
{
_currentMyState.ExitState();
}
_currentMyState = newMyState;
_currentMyState.EnterState();
}
public void ChangeState(EMyState state)
{
ChangeState_Internal(_states[state]);
}
void Start()
{
// 1번 이거는 성능이 직접 컴포넌트 가져오는 방식 대비 비싸다.
VMyState[] stateArray = GetComponents<VMyState>();
foreach (var state in stateArray)
{
state.StateMachine = this;
EMyState outEnum;
if (EMyState.TryParse(state.GetType().ToString(), out outEnum))
{
_states.Add(outEnum, state);
}
}
// 2번 아래의 방식이 좀 더 비용적으로 저렴하다.
_states.Add(EMyState.IdleMyState, GetComponent<IdleMyState>());
_states.Add(EMyState.MoveMyState, GetComponent<MoveMyState>());
_states.Add(EMyState.StunMyState, GetComponent<StunMyState>());
// DefaultState
ChangeState(EMyState.IdleMyState);
}
// Update is called once per frame
void Update()
{
if (_currentMyState != null)
{
_currentMyState.ExcuteState();
}
}
}
컴포넌트 방식으로 바뀐 뒤에는 데이터를 가공하기가 편해진다.

상태 안에 상태가 존재하는 경우 HFSM(Hierarchy FSM)을 사용 수 있다.
public abstract class VMyState : MonoBehaviour, IMyState
{
[NonSerialized]public StateMachine OwnerStateMachine;
// HSFM 이용 할 시
public StateMachine HSFM_StateMachine;
public abstract void EnterState();
public abstract void ExcuteState();
public abstract void ExitState();
}

이렇게 해서 하위 객체에 StateMachine을 달고 State를 달면
무한 HFSM 된다.




HFSM 상위상태 변경 시 하위상태 정리 안되는문제 해결 버전 코드
GetSuperOwnerStateMachine
using System;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public enum EMyState
{
NoneMyState,
IdleMyState,
MoveMyState,
StunMyState,
}
public abstract class VMyState : MonoBehaviour
{
[NonSerialized]public StateMachine OwnerStateMachine;
// HSFM 이용 할 시
public StateMachine HSFM_StateMachine;
public void EnterStateWrapper()
{
EnterState();
}
public void ExcuteStateWrapper()
{
ExcuteState();
}
public void ExitStateWrapper()
{
ExitState();
if (HSFM_StateMachine)
{
HSFM_StateMachine.ChangeState(EMyState.NoneMyState);
}
}
public abstract void EnterState();
public abstract void ExcuteState();
public abstract void ExitState();
}
public class StateMachine : MonoBehaviour
{
[SerializeField] private EMyState defaultState;
private VMyState _currentMyState;
private Dictionary<EMyState, VMyState> _states = new();
StateMachine GetSuperOwnerStateMachile()
{
StateMachine stateMachine = GetComponentInParent<StateMachine>();
if (stateMachine)
{
return stateMachine.GetSuperOwnerStateMachile();
}
return this;
}
private void ChangeState_Internal(VMyState newMyState)
{
if (_currentMyState != null)
{
_currentMyState.ExitStateWrapper();
}
if (newMyState == null)
{
_currentMyState = null;
return;
}
_currentMyState = newMyState;
_currentMyState.EnterStateWrapper();
}
public void ChangeState(EMyState state)
{
if (state == EMyState.NoneMyState)
{
ChangeState_Internal(null);
return;
}
ChangeState_Internal(_states[state]);
}
// Start is called before the first frame update
void Start()
{
// 이거는 성능이 직접 컴포넌트 가져오는 방식 대비 비싸다.
VMyState[] stateArray = GetComponents<VMyState>();
foreach (var state in stateArray)
{
state.OwnerStateMachine = this;
EMyState outEnum;
if (EMyState.TryParse(state.GetType().ToString(), out outEnum))
{
_states.Add(outEnum, state);
}
}
// DefaultState
ChangeState(defaultState);
}
// Update is called once per frame
void Update()
{
if (_currentMyState != null)
{
_currentMyState.ExcuteStateWrapper();
}
}
}'STUDY > 디자인패턴' 카테고리의 다른 글
| Strategy 전략 패턴 (0) | 2024.07.19 |
|---|---|
| Observer 옵저버 패턴 ★★★ (0) | 2024.07.19 |
| Command 커맨드 패턴 ★★★ (0) | 2024.07.16 |
| chain of responsibility 책임연쇄 패턴 (0) | 2024.07.16 |
| Proxy 프록시 패턴 (0) | 2024.07.15 |