본문 바로가기
개발일지/게임개발

Project_DT - 턴제 전투, 적 상태 구현

by 라이티아 2025. 9. 18.
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();
    }
}
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 UnityEngine;

public class EnemyState : MonoBehaviour
{
    private Enemy _enemy;

    [SerializeField]
    protected int _nextStateIndex = 0;

    void Awake()
    {
        _enemy = transform.parent.GetComponent<Enemy>();
    }

    /// <summary>
    /// 상태 진입시 수행되는 함수
    /// </summary>
    public virtual void Enter()
    {
        gameObject.SetActive(true);
    }
    /// <summary>
    /// 적이 자신의 턴에 할 행동
    /// </summary>
    public virtual void Action()
    {
        
    }

    /// <summary>
    /// 상태 종료시 수행되는 함수
    /// </summary>
    public virtual void Exit()
    {
        gameObject.SetActive(false);
    }

    /// <summary>
    /// 상태 변환 체크용 if조건을 넣어서 조건에 맞을 시 _wantChangeState로 이동시킴
    /// </summary>
    public virtual void CheckStateChange()
    {

    }
}

일단 구독을 유발하는 event를 전면적으로 제거했다

 

이를 delegate로 구현해보려 한다

 

protected Action<int> RequestChange; // 전이 요청
protected Action ReportDone; // 액션 종료 보고
// 주입 지점
public void BindCallbacks(Action<int> requestChange, Action reportDone)
{
RequestChange = requestChange;
ReportDone = reportDone;
}

state쪽에서는 해당 Action을 delegate로 사용한다

 

이것을 적 객체가

// 콜백 묶음
private Action<int> _requestChange;
private Action _reportDone;
void Awake()
{
_states = GetComponentsInChildren<EnemyState>(true).ToList();
foreach (var state in _states)
{
state.gameObject.SetActive(false);
}
_requestChange = ChangeState;
_reportDone = OnActionDone;
// 모든 상태에 주입
foreach (var state in _states)
state.BindCallbacks(_requestChange, _reportDone);
}

모든 상태에 대해서 주입을 해서 저장을 해둔다

 

이렇게 주입을 해둘 시 requestchange, reportdone가 state에서 어떻게든 호출이 될시 enemy쪽으로 함수 호출을 요청하게 되고, 처리는 enemy가 하게 된다

 


private Action<int> _requestChange; private Action _reportDone;

이 2개에

 

_requestChange = ChangeState;

_reportDone = OnActionDone;

내부 함수를 호출하도록 연결하고

 

foreach (var state in _states)

state.BindCallbacks(_requestChange, _reportDone);

이 반복을 통해서 주입을 시키는데

 

public void BindCallbacks(Action<int> requestChange, Action reportDone)

{ RequestChange = requestChange; ReportDone = reportDone; }

주입에서는 이 RequestChange ReportDone가 state에서 invoke되면 연결된 부분을 호출하게 해서 enemy의 내부 함수를 호출하게 하는 구조로 되어 있다