앞 서 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();
}
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를 만들어서 전환을 매끄럽게 할 생각이다