

원래는 이렇게 enum으로 행동을 결정하고 gameobject를 결정하는 형태로 개발을 했는데, 이를 FSM에 맞게 수정하려 한다
using UnityEngine;
public class EnemyState : MonoBehaviour
{
private enum ActionState
{
attack,
defence,
effect
}
private Enemy _enemy;
[SerializeField]
private ActionState _actionState = new ActionState();
[SerializeField]
private int _actionValue = 3;
void Awake()
{
_enemy = transform.parent.GetComponent<Enemy>();
gameObject.SetActive(false);
}
public void Action()
{
switch (_actionState)
{
case ActionState.attack:
Debug.Log($"{gameObject.name} Movement: attack Player {_actionValue}");
BattleManager.Instance.ApplyDamage(BattleManager.Instance.GetPlayer(), _actionValue);
break;
case ActionState.defence:
break;
case ActionState.effect:
break;
default:
Debug.LogError($"{gameObject.name} = Movement : use undefined ActionState");
break;
}
gameObject.SetActive(false);
}
}
이러한 형태를 취하고 있는데 이를 수정한다
GPT의 의견
- 상태의 단일 원천이 없습니다. Enemy는 현재 상태를 나타내는 명시적 변수나 상태 객체가 없고, 단지 _acting 플래그와 _moveIndex 순환으로 흐름을 제어합니다.
- 전이는 정의돼 있지 않습니다. 입력(이벤트/조건)에 따라 “현재 상태 → 다음 상태”를 결정하는 전이 표나 규칙이 없습니다.
- Movement.ActionState는 “행동의 종류”일 뿐 “엔티티의 상태”가 아닙니다. 상태 간 전이를 기술하지 못합니다.
- 각 Movement는 일회성 실행 노드입니다. Enter/Update/Exit 같은 상태 수명주기와 가드 조건이 없습니다.
FSM로 개편하려면
- “적의 현재 상태”를 단일 변수나 상태 객체로 명시하십시오(예: Idle, SelectAction, Acting, WaitingEndTurn).
- 전이 규칙을 이벤트로 표현하십시오(턴 시작, 애니메이션 종료, 피격, 플레이어 거리 조건 등).
- 각 상태에 Enter/Update/Exit 책임을 분리하고, 전이는 가드 조건으로 관리하십시오.
- Movement는 “행동 데이터”로 두고, 상태 로직이 이를 선택·실행하도록 연결하십시오.
- 필요하면 턴 매니저 이벤트를 전이 트리거로 사용하고, 전이와 실행을 분리하십시오.
요약하면, 지금 구조는 “순차 행동 실행기”에 가깝고, 전이와 상태가 명시된 형태로 재구성하면 FSM에 가까워질 것입니다.
일단 state를 변경하는 주체는 enemy로 지정하고 설계를 구상한다
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
public class Enemy : Character
{
private bool _acting = false;
[SerializeField]
private List<EnemyState> _states = new List<EnemyState>();
[Header("현재 상태")]
[SerializeField]
private EnemyState _currentState = null;
void Awake()
{
_states = GetComponentsInChildren<EnemyState>(true).ToList();
}
void Start()
{
if (TurnManager.Instance != null)
TurnManager.Instance.OnTurnChanged += TurnChanged;
}
/// <summary>
/// 현재 턴이 적의 턴일시 state의 Action()함수를 통해 행동 시행
/// </summary>
/// <param name="owner">현재 턴의 주체</param>
private void TurnChanged(TurnManager.TurnOwner owner)
{
if (owner == TurnManager.TurnOwner.Enemy)
{
Debug.Log("Enemy's Turn Start");
if (_acting) return;
else _acting = true;
_currentState.Action();
}
}
/// <summary>
/// 현재 state를 다른 state로 변경, 무조건 state의 action()이 끝난 후 호출하도록 설계할 것
/// </summary>
/// <param name="state">바꾸고 싶은 상태</param>
protected void ChangeState(EnemyState state)
{
_currentState = state;
}
}
일단 index로 관리하던 상태같은 무언가를 currentstate로 관리하는 형태로 변경한다
using UnityEngine;
public class EnemyState : MonoBehaviour
{
private Enemy _enemy;
[Header("전환하고 싶은 상태")]
[SerializeField]
private EnemyState _changeState;
void Awake()
{
_enemy = transform.parent.GetComponent<Enemy>();
}
public virtual void Action()
{
}
}
state는 단순하게 행동할 영역과 전환하고 싶은 상태를 저장하도록 한다
다시 GPT의 점검을 받는다
충분히 가지만, FSM로 보기엔 몇 가지 핵이 비어 있습니다. 권장 수정만 짚겠습니다.
- 전환 소유권
- ChangeState 실행은 Enemy가 가져가십시오. State는 “전환 제안”만 합니다.
- 방법: EnemyState가 RequestChange(nextState) 이벤트를 쏘고, Enemy가 이를 받아 ChangeState 호출.
- 상태 수명주기 정립
- 각 State에 Enter/Exit/Action을 두고, 전환 시 Enemy가 current.Exit → current=next → next.Enter 순서로 호출하십시오.
- GameObject 활성/비활성은 Enter/Exit에서만 처리하십시오. 지금처럼 외부에서 임의 활성화하면 꼬이기 쉽습니다.
- 전환 판단과 턴 결합
- Update에서 매 프레임 CheckChangeStateCondition()을 호출하면 적 턴이 아닐 때도 전환됩니다.
- 가드 추가: 적 턴이고 _acting==false인 경우만 전환 판단.
- 액션 종료 신호
- Action()이 끝나는 시점을 Enemy가 모릅니다.
- 각 State가 OnActionDone 이벤트를 보내고 Enemy가 _acting=false로 리셋 후 NextTurn()을 호출하도록 하십시오. 지연이 필요하면 Enemy가 코루틴으로 관리.
- 초기 상태 지정 방식
- _states[0]는 순서 의존이라 취약합니다. [SerializeField] private EnemyState _initialState;를 명시하고 Awake에서 검증하는 편이 안전합니다.
- 전환 정의의 위치
- EnemyState 안의 _changeState 필드는 상태들 간 직접 참조를 만들며 결합도를 높입니다.
- 대안: 상태가 “다음 상태 ID/enum”을 반환하거나, 이벤트 페이로드로 nextState를 넘기고 Enemy가 룩업.
- 보호 수준
- ChangeState가 protected면 외부(State)에서 호출할 수 없습니다. 설계상 Enemy만 호출하겠다면 private로 내리십시오. State가 직접 호출해야 하는 구조라면 현재 원칙과 충돌합니다.
- 재진입·중복 방지
- _acting을 켜고 끄는 책임을 Enemy로 고정하십시오. Action() 재호출, 전환 중복을 막습니다.
요약:
- State는 실행과 “전환 신호”만.
- Enemy가 전환 결정과 수명주기, 턴 종료를 중앙에서 관리.
- 초기 상태 명시, 전환 가드, 액션 종료 신호를 추가하면 FSM로 안정화될 것입니다.
일단 상태에 enter, action, exit를 정의하고 어떻게 사용할지 생각하자
using UnityEngine;
public class EnemyState : MonoBehaviour
{
private Enemy _enemy;
[Header("전환하고 싶은 상태")]
[SerializeField]
private EnemyState _changeState;
void Awake()
{
_enemy = transform.parent.GetComponent<Enemy>();
}
/// <summary>
/// 상태 진입시 수행되는 함수
/// </summary>
public virtual void Enter()
{
}
/// <summary>
/// 적이 자신의 턴에 할 행동
/// </summary>
public virtual void Action()
{
}
/// <summary>
/// 상태 변경시 수행되는 함수
/// </summary>
public virtual void Exit()
{
}
/// <summary>
/// 상태를 변경하는 조건 정의
/// </summary>
public virtual void CheckChangeStateCondition()
{
}
}
지금 state쪽에서
[Header("전환하고 싶은 상태")]
[SerializeField]
private EnemyState _changeState;
자신이 원하는 전환 state를 지목하고 있는데, 이는 결합도를 높이는 원인이 된다고 한다
----
논리적으로 뭔가 무너진 것 같아서 설계를 멈췄다
using System.Collections;
using UnityEngine;
public class EnemyState : MonoBehaviour
{
private Enemy _enemy;
public event System.Action<int> OnRequestChange;
public event System.Action OnActionDone;
protected void ActionDone() => OnActionDone?.Invoke();
[SerializeField]
protected int _nextStateIndex = 0;
void Awake()
{
_enemy = transform.parent.GetComponent<Enemy>();
}
protected void RequestChange(int stateIndex)
{
OnRequestChange?.Invoke(stateIndex);
}
/// <summary>
/// 상태 진입시 수행되는 함수
/// </summary>
public virtual void Enter()
{
gameObject.SetActive(true);
}
/// <summary>
/// 적이 자신의 턴에 할 행동
/// </summary>
public virtual void Action()
{
RequestChange(_nextStateIndex);
ActionDone();
}
/// <summary>
/// 상태 종료시 수행되는 함수
/// </summary>
public virtual void Exit()
{
gameObject.SetActive(false);
}
/// <summary>
/// 상태 변환 체크용 if조건을 넣어서 조건에 맞을 시 _wantChangeState로 이동시킴
/// </summary>
public virtual void CheckStateChange()
{
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
public class Enemy : Character
{
private bool _acting = false;
private int? _pendingNext = null;
[SerializeField]
private List<EnemyState> _states = new List<EnemyState>();
[Header("현재 상태")]
[SerializeField]
private EnemyState _currentState = null;
void Awake()
{
_states = GetComponentsInChildren<EnemyState>(true).ToList();
foreach (var state in _states)
{
state.gameObject.SetActive(false);
}
}
void Start()
{
if (TurnManager.Instance != null)
TurnManager.Instance.OnTurnChanged += TurnChanged;
_currentState = _states[0];
_currentState.OnRequestChange += ChangeState; // 초기 상태 구독
_currentState.OnActionDone += OnActionDone;
_currentState.Enter();
}
void Update()
{
// 플레이어 턴일때만 체크함
if (TurnManager.Instance != null &&
TurnManager.Instance.CurrentOwner == TurnManager.TurnOwner.Player &&
_currentState != null)
{
_currentState.CheckStateChange();
}
}
void OnDestroy()
{
TurnManager.Instance.OnTurnChanged -= TurnChanged;
if (_currentState != null)
{
_currentState.OnRequestChange -= ChangeState;
_currentState.OnActionDone -= OnActionDone;
}
}
/// <summary>
/// 현재 턴이 적의 턴일시 state의 Action()함수를 통해 행동 시행
/// </summary>
/// <param name="owner">현재 턴의 주체</param>
private void TurnChanged(TurnManager.TurnOwner owner)
{
if (owner == TurnManager.TurnOwner.Enemy)
{
Debug.Log("Enemy's Turn Start");
if (_acting) return;
else _acting = true;
_currentState.Action();
}
}
/// <summary>
/// 현재 state를 다른 state로 변경, 무조건 state의 action()이 끝난 후 호출하도록 설계할 것
/// </summary>
/// <param name="state">바꾸고 싶은 상태</param>
public void ChangeState(int stateIndex)
{
if (stateIndex < 0 || stateIndex >= _states.Count) { Debug.LogError($"idx {stateIndex}"); return; }
if (_currentState == _states[stateIndex]) return;
// 행동 중에는 보류만
if (_acting) { _pendingNext = stateIndex; return; }
ApplyState(stateIndex);
}
private void ApplyState(int stateIndex)
{
var next = _states[stateIndex];
if (_currentState == next) return;
_currentState.OnRequestChange -= ChangeState;
_currentState.OnActionDone -= OnActionDone;
_currentState.Exit();
_currentState = next;
_currentState.OnRequestChange += ChangeState;
_currentState.OnActionDone += OnActionDone;
_currentState.Enter();
}
// state 행동 끝났을시 수행
public void OnActionDone()
{
_acting = false;
// 여기서만 전환 확정
if (_pendingNext.HasValue)
{
ApplyState(_pendingNext.Value);
_pendingNext = null;
}
StartCoroutine(DelayTurnEnd());
}
public IEnumerator DelayTurnEnd()
{
yield return new WaitForSeconds(2f);
Debug.Log("enemy turn end");
TurnManager.Instance.NextTurn();
}
}
event를 통한 구독, 구독 해제를 남발하는 형태로 fsm을 만들었는데, 굉장히 불안정한 구조이다...
다른 구조로 설계를 해야할 것 같다
'개발일지 > 게임개발' 카테고리의 다른 글
| Project_DT - 구글 시트 parsing 기능 만들기 (0) | 2025.09.19 |
|---|---|
| Project_DT - 턴제 전투, 적 상태 구현 (0) | 2025.09.18 |
| Project DT - 적 공격 구현 (0) | 2025.09.10 |
| Project DT - 턴 시스템 구현하기 (1) | 2025.09.06 |
| 유니티 3D - 방입장 카메라 무빙 구현 (3) | 2025.08.28 |