[ Enemy FSM ] Enemy Roaming => FSM StateClass

devKyoun·2024년 10월 18일
0

Unity

목록 보기
11/27
post-thumbnail

앞 서 Enemy Roaming을 만들었는데 그것을 구현한 스크립트는 EnemyAI (FSM Manager)로 할 것이고 그 Enemy Roaming의 실질적인 코드는 EnemyRoamingState.cs 로 재구축 하려고 한다

우선 그러한 여러 상태들의 스크립트를 구현하려면 하나의 파이프라인 역할을 할 클래스가 필요하다

그것이 바로 BaseState 이다

public abstract class BaseState
{
    public abstract void StateEnter();
    public abstract void StateExit();
    public abstract void StateUpdate();
}

상태 전이할때 Enter, Exit 기능은 필수이다

그 뒤 EnemyAI를 재구축했다 ( FSM의 심장이 되는 스크립트 )

 public enum EnemyState
 {
     Roaming,
     Tracking,
     //Attack,
     //Defend,
     //Return,
     //Die,
 }

각 상태들은 이렇게 관리를 해주려고 한다
그리고 각 상태들에 관한 변수

// 현재 상태가 무엇인지 담는 currentState
 private BaseState currentState;
 private Dictionary<EnemyState, int> stateValueDic = new Dictionary<EnemyState, int>();
 private BaseState[] stateArray;

여기서 Dictionary 부분을 따로 저렇게 한게 이유가 있다

private BaseState[] stateArray;

현재 state들을 저장할 배열이 있는데 이 배열이 나중에 state들을 참고로 할때 문제가 생긴다

stateArray[EnemyState.Roaming]

이러한 형태로 접근하면 EnemyState.Roaming은 enum 형태기 때문에 아래와 같이 해줘야 한다

stateArray[(int)EnemyState.Roaming]

Or
// 이 형변환 코드가 문제임 GC 발생이 엄청남
stateArray[Convert.xxx(EnemyState.Roaming)]

GC 발생에 따라 Dictionary를 사용해서 이를 해결해준것이다
Dictionary가 성능면에서도 좋다

이제 초기화를 하면 된다

 private void Awake()
 {
     // 상태가 현재는 총 6개
     stateArray = new BaseState[6];

     // Roaming은 0
     stateValueDic.Add(EnemyState.Roaming, 0);
     // 실질적인 EnemyRoaming 연결
     // EnemyManager도 넘겨줘야한다 전체적인 필요한 컴포넌트와 변수들을 담은 스크립트
     stateArray[stateValueDic[EnemyState.Roaming]] = new EnemyRoamingState(this, enemyManager);
     
     // 현재 미개발
     //stateValueDic.Add(EnemyState.Tracking, 1);
     //stateArray[stateValueDic[EnemyState.Tracking]] = new EnemyTrackingState(this);

 }

여기서 enemyManager는 EnemyRoamingState.cs 뒤에 설명하려고한다
그리고 초기화 뒤 FSM의 본격적인 실행을 다룬다

    private void Start()
    {
    	// 시작은 Roaming 상태
        currentState = stateArray[stateValueDic[EnemyState.Roaming]];
        // 상태에 돌입 했으니 Enter
        currentState.StateEnter();
    }

    private void Update()
    {
        if(currentState != null)
        {
            currentState.StateUpdate();
        }
    }


    public void ChangeState(EnemyState nextState)
    {
        if(currentState != null)
        {
        	// 현재 상태에서 나가고
            currentState.StateExit();
        }
		
        // 현재 상태를 전이 될 상태로 업데이트
        currentState = stateArray[stateValueDic[nextState]];
        currentState.StateEnter();
    }

EnemyRoamingState.cs ( EnemyRoamingState : BaseState )

Enemy Roaming 만들기 포스트에서 구현한 것을 BaseState를 상속받은 상태 스크립트로 재구축

EnemyRoamingState.cs의 지역 변수들

private EnemyAI enemyAI;
private EnemyManager enemyManager;

// 스크립트 안에서만 필요한 것들
private Vector3 roamPosition;
private bool setRoamPos = false;
private float waitCounter;
private NavMeshPath path;

StateEnter

public override void StateEnter()
{
    roamPosition = GetRoamingPosition();
    setRoamPos = true;
    enemyManager.Animator.SetBool(enemyManager.AnimIDWalk, true);
}

상태에 돌입하자마자 Roaming을 다니는 로직
애니메이터 업데이트까지 해주자

StateUpdate

public override void StateUpdate()
{
    if (setRoamPos)
    {
        path = new NavMeshPath();
        enemyManager.EnemyNavAgent.CalculatePath(roamPosition, path);
        enemyManager.EnemyNavAgent.SetPath(path);
        path = null;

        setRoamPos = false;
        enemyManager.Animator.SetBool(enemyManager.AnimIDWalk, true);

    }

    if (enemyManager.EnemyNavAgent.remainingDistance <= enemyManager.EnemyNavAgent.stoppingDistance)
    {
        enemyManager.Animator.SetBool(enemyManager.AnimIDWalk, false);
        // 도착 했을때, Timer 시작
        waitCounter += Time.deltaTime;

        if (waitCounter >= enemyManager.WaitTime)
        {
            setRoamPos = true;
            roamPosition = GetRoamingPosition();
            waitCounter = 0;
        }
    }
}

// StateUpdate는 아니지만 이 Roaming에서만 필요한 함수
private Vector3 GetRoamingPosition()
{
    Vector3 randomDir = new Vector3(Random.Range(-1f, 1f), 0, Random.Range(-1f, 1f)).normalized;
    return enemyManager.StartingPosition + randomDir * Random.Range(enemyManager.MinRoamingDistance, enemyManager.MaxRoamingDistance);
}

여기까지 봤을때 enemyManager의 역할은 다소 티가 난다
바로 enemy들이 공통적으로 필요한 컴포넌트, 변수들을 담는 스크립트이다

enemyManager.cs

public class EnemyManager : MonoBehaviour
{
    private NavMeshAgent enemyNavAgent;
    private Animator animator;
    private int animIDWalk;

    private NavMeshPath path = null;

    private Vector3 startingPosition;

    [SerializeField]
    private float minRoamingDistance = 10;
    [SerializeField]
    private float maxRoamingDistance = 70;
    [SerializeField]
    private float waitTime = 2.5f;


    public Vector3 StartingPosition { get { return startingPosition; } }
    public Animator Animator { get { return animator; } }
    public int AnimIDWalk { get { return animIDWalk; } }
    public NavMeshAgent EnemyNavAgent { get { return enemyNavAgent; } }
    public float MinRoamingDistance { get { return minRoamingDistance; } }
    public float MaxRoamingDistance { get {return maxRoamingDistance; } }
    public float WaitTime { get { return waitTime; } }


    private void Awake()
    {
        enemyNavAgent = GetComponent<NavMeshAgent>();
        animator = GetComponentInChildren<Animator>();
        animIDWalk = Animator.StringToHash("Walk");

        startingPosition = transform.position;
    }
}

이래야지 Enemy의 변수들을 조정할때 더 편리하고 좋다
그냥 EnemyAI에다가 다 선언해도 상관없는데 그러면 독립성 측면에서 좋지않다


결과물

NavMeshAgent의 Auto Brake 기능 꺼줘야지 슬라이딩 안함!!


다음 단계

이제 Tracking, Attacking State.cs를 만들어서 전환을 매끄럽게 할 생각이다

profile
Game Developer

0개의 댓글