카테고리 없음

20250417 TIL enemy 전략패턴 및 상태머신 적용

note4973 2025. 4. 17. 21:23
using UnityEngine;
using DG.Tweening;
using System;
using System.Collections;

public enum EnemyState
{
    Idle,
    Chase,
    Attack
}

public class EnemyController2 : MonoBehaviour
{
    public float moveSpeed = 3f;
    public float DetectionRange = 10f;
    public EnemyState _currentState = EnemyState.Idle;
    public Tile lastCheckedTile = null;
    IdleBaseBehaviour idleBehaviour;
    ChaseBaseBehaviour chaseBehaviour;
    AttackBaseBehaviour attackBehaviour;
    IMovementStrategy moveStrategy;
    EnemyAnimatorController animationController;
    PlayerStats _playerStats;
    EnemyStats _enemyStats;
    Tween _currentTween;

    void Awake()
    {
        GameManager.Instance.RegisterEnemy(GetComponent<EnemyStats>());
        _enemyStats = gameObject.GetComponent<EnemyStats>();
        idleBehaviour = GetComponent<IdleBaseBehaviour>();
        chaseBehaviour = GetComponent<ChaseBaseBehaviour>();
        attackBehaviour = GetComponent<AttackBaseBehaviour>();
        moveStrategy = GetComponent<IMovementStrategy>();
        animationController = GetComponent<EnemyAnimatorController>();
    }

    void Start()
    {
        _playerStats = GameManager.Instance.PlayerTransform.GetComponent<PlayerStats>();
    }

    public IEnumerator TakeTurn()
    {
        if (_currentTween != null && _currentTween.IsActive()) _currentTween.Kill();

        switch (_currentState)
        {
            case EnemyState.Idle:
                HandleIdle();
                break;
            case EnemyState.Chase:
                HandleChase();
                break;
            case EnemyState.Attack:
                HandleAttack();
                break;
        }

        yield break;
    }

    public void ChangeState(EnemyState newState)
    {
        _currentState = newState;
        TakeTurn(); // 상태 바뀌자마자 바로 행동 즉 실질 행동시작 전에 상태체크를우선해야함
    }

    private void HandleIdle()
    {
        idleBehaviour?.Excute();
    }

    private void HandleChase()
    {
        chaseBehaviour?.Excute();
    }

    private void HandleAttack()
    {
        attackBehaviour?.Excute();
    }


    public void MoveTo(Vector2Int targetPosition,  Action onComplete = null)
    {
        Vector3 position = new Vector3(targetPosition.x, targetPosition.y, 0);

        if(moveStrategy != null)
            moveStrategy.Move(this.transform , position, animationController, onComplete);
        else
            BasicMove(position, onComplete);
    }

    void BasicMove(Vector3 targetPosition,  Action onComplete = null)
    {
        animationController?.TriggerMove();

        _currentTween?.Kill(); // 기존 움직임 취소
        _currentTween = transform.DOMove(targetPosition, 1f / moveSpeed)
            .SetEase(Ease.Linear)
            .OnComplete(() =>
            {
                animationController?.TriggerIdle();
                onComplete?.Invoke();
                });
    }

    public void Attack( Action onComplete = null)
    {
        animationController?.TriggerAttack();

        onComplete?.Invoke();
    }
}

 

 

enemy controller에 상태머신과 전략패턴을 적용하여 유동성 있는 코드로 작성하였다.

 

    public IEnumerator TakeTurn()
    {
        if (_currentTween != null && _currentTween.IsActive()) _currentTween.Kill();

        switch (_currentState)
        {
            case EnemyState.Idle:
                HandleIdle();
                break;
            case EnemyState.Chase:
                HandleChase();
                break;
            case EnemyState.Attack:
                HandleAttack();
                break;
        }

        yield break;
    }

    public void ChangeState(EnemyState newState)
    {
        _currentState = newState;
        TakeTurn(); // 상태 바뀌자마자 바로 행동 즉 실질 행동시작 전에 상태체크를우선해야함
    }

 

상태머신을 이용하여 상태가 변하면 자동으로 변한 상태의 메서드를 실행한다.

 

현재 idle, chase, attack에는 전략패턴을 적용하여 컴퍼넌츠를 바꾸면 행동패턴을 바꾸도록 구성하였다.

 

public class ChaseBaseBehaviour : BaseBehaviour
{
    public override void Excute()
    {
        if (StateCheck())
            Action();
    }

    public override bool StateCheck()
    {
        foreach (Tile tile in attackRangeTile)//공격 사거리 안에 있으며 시야에 있는지 체크
        {
            if (tile.characterStats is PlayerStats player)
            {
                if (TileUtility.IsTileVisible(level, CurTile, tile))
                {
                    controller.ChangeState(EnemyState.Attack);
                    return false;
                }
                break;
            }
        }

        foreach (Tile tile in vision)//플레이어가 시야에 있다면 lastchecked에 넣기
        {
            if (tile.characterStats is PlayerStats player)
            {
                controller.lastCheckedTile = tile;
                break;
            }
        }

        if (controller.lastCheckedTile == null)//플레이어가 시야에 없다면 마지막으로 보인곳으로 감 마지막으로 보인곳에 도달하면 다시 idle
        {
            controller.ChangeState(EnemyState.Idle);
            return false;
        }

        return true;
    }

    public override void Action()
    {

    }

    protected void MoveTo(Tile target)
    {
        CurTile.characterStats = null;
        controller.MoveTo(target.gridPosition);
        CurTile = target;
        CurTile.characterStats = enemyStats;

        if (target == controller.lastCheckedTile)
            controller.lastCheckedTile = null;
    }
}

 

해당 클래스에서 StateCheck 후 Action을 실행한다.

 

이를통해 각 모듈이 자신의 책임만 가지게 되어 SRP(단일 책임 원칙)을 만족했고코드 의존성이 줄어들어 테스트와 리팩토링이 쉬워졌다

 

또한 이동 메서드 역시 전략패턴을 통해 유동성 있는 구조를 구성하였다.

public interface IMovementStrategy
{
    void Move(Transform enemy, Vector3 targetPos, EnemyAnimatorController animationController = null, Action onComplete = null);
}

public class BasicMovement : IMovementStrategy
{
    public void Move(Transform enemy, Vector3 target, EnemyAnimatorController animationController = null, Action onComplete = null)
    {
        animationController?.TriggerMove();

        enemy.DOMove(target, 0.5f)
            .SetEase(Ease.Linear)
            .OnComplete(() =>
            {
                animationController?.TriggerIdle();
                onComplete?.Invoke();
            });
    }

 

이 역시 move 메서드만 변경해주면 캐릭터의 움직임 제어를 유동적으로 구성할수있게 된다.