유니티 유한상태기계 - FSM(Finite State Machine) 2
FSM 1의 기본틀에다가 이것저것 기능들 추가함
Enemy 스크립트 :
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class Enemy : MonoBehaviour
{
[Header("Idle data")]
public float idleTime;
public float aggressionRange;
[Header("Move data")]
public float moveSpeed;
public float chaseSpeed;
private bool manualMovement;
private bool manualRotation;
public Transform[] patrolPoints;
int currentPatrolIndex;
public int trunSpeed;
public Transform player { get; private set; }
public NavMeshAgent agent { get; private set; }
public EnemyStateMachine enemyStateMachine { get; private set; }
public Animator anim { get; private set; }
protected virtual void Awake()
{
enemyStateMachine = new EnemyStateMachine();
agent = GetComponent<NavMeshAgent>();
anim = GetComponentInChildren<Animator>();
//player = GameObject.Find("Player_Character").GetComponent<Transform>();
player = GameObject.FindWithTag("Player").transform;
}
protected virtual void Start()
{
foreach (var p in patrolPoints)
{
p.parent = null;
}
}
protected virtual void Update()
{
}
public bool PlayerInAggressionRange()
{
return Vector3.Distance(player.position, transform.position) < aggressionRange;
}
public void ActivateManualMovement(bool manualMovement)
{
this.manualMovement = manualMovement;
}
public bool ManualMovementActive()
{
return manualMovement;
}
public void ActivateManualRotation(bool manualRotation)
{
this.manualRotation = manualRotation;
}
public bool ManualRotationActive()
{
return manualRotation;
}
public Vector3 GetPatrolDestination()
{
Vector3 destination = patrolPoints[currentPatrolIndex].transform.position;
currentPatrolIndex++;
if (currentPatrolIndex >= patrolPoints.Length)
currentPatrolIndex = 0;
return destination;
}
protected virtual void OnDrawGizmos()
{
Gizmos.DrawWireSphere(transform.position, aggressionRange);
}
public Quaternion FaceTarget(Vector3 target)
{
// 객체가 target을 바라보도록 하는 회전값
Quaternion targetRotation = Quaternion.LookRotation(target - transform.position);
// 현재 회전 각도 가져오기
Vector3 currentEulerAngels = transform.rotation.eulerAngles;
// y축 회전 보간
float yRotation = Mathf.LerpAngle(currentEulerAngels.y, targetRotation.eulerAngles.y, trunSpeed * Time.deltaTime);
return Quaternion.Euler(currentEulerAngels.x, yRotation, currentEulerAngels.z);
}
public void AnimationTrigger()
{
enemyStateMachine.currentState.AnimationTrigger();
}
}
Enemy_Melee 스크립트 :
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public struct AttackData
{
public string attackName;
public float attackRange;
public float moveSpeed;
public float attackIndex;
[Range(1, 2)]
public float animationSpeed;
}
public enum EnemyType
{
Normal,
Patrol
}
public class Enemy_Melee : Enemy
{
[Header("Attack data")]
public AttackData attackData;
public EnemyType enemyType;
public IdleState_Melee idleState { get; private set; }
public MoveState_Melee moveState { get; private set; }
public RecoveryState_Melee recoveryState { get; private set; }
public ChaseState_Melee chaseState { get; private set; }
public AttackState_Melee attackState { get; private set; }
protected override void Awake()
{
base.Awake();
idleState = new IdleState_Melee(this, enemyStateMachine, "Idle");
moveState = new MoveState_Melee(this, enemyStateMachine, "Move");
recoveryState = new RecoveryState_Melee(this, enemyStateMachine, "Recovery");
chaseState = new ChaseState_Melee(this, enemyStateMachine, "Chase");
attackState = new AttackState_Melee(this, enemyStateMachine, "Attack");
}
protected override void Start()
{
base.Start();
enemyStateMachine.Initialize(idleState);
}
protected override void Update()
{
base.Update();
enemyStateMachine.currentState.Update();
}
protected override void OnDrawGizmos()
{
base.OnDrawGizmos();
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, attackData.attackRange);
}
public bool PlayerInAttackRange()
{
return Vector3.Distance(player.position, transform.position) < attackData.attackRange;
}
}
EnemyState 스크립트 :
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyState
{
protected Enemy enemyBase;
protected EnemyStateMachine enemyStateMachine;
protected string animBoolName;
protected float stateTimer;
protected bool triggerCalled;
public EnemyState(Enemy enemyBase, EnemyStateMachine enemyStateMachine, string animBoolName)
{
this.enemyBase = enemyBase;
this.enemyStateMachine = enemyStateMachine;
this.animBoolName = animBoolName;
}
public virtual void Enter()
{
enemyBase.anim.SetBool(animBoolName, true);
triggerCalled = false;
}
public virtual void Update()
{
stateTimer -= Time.deltaTime;
}
public virtual void Exit()
{
enemyBase.anim.SetBool(animBoolName, false);
}
public bool AnimationTrigger()
{
return triggerCalled = true;
}
}
EnemyStateMachine 스크립트 :
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyStateMachine
{
public EnemyState currentState { get; private set; }
// 첫번째 상태를 초기화함
public void Initialize(EnemyState startState)
{
currentState = startState;
currentState.Enter();
}
// 이전상태를 종료하고 바로 다른 상태로 전환
public void ChangeState(EnemyState newState)
{
Debug.Log($"Changing state from {currentState} to {newState}");
currentState.Exit(); // 이전 상태를 종료하고
currentState = newState; // 새로운 상태를 가져와서
currentState.Enter(); // 새로운 상태로 들어감
}
}
EnemyAnimationEvents 스크립트 : 애니메이션 클립에 이벤트 추가 용도.
ex. 애니메이션 동작이 끝나는 부분에 AnimationTrigger() 이벤트를 호출해서 다음 동작을 하게 만듦.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyAnimationEvents : MonoBehaviour
{
Enemy enemy;
private void Awake()
{
enemy = GetComponentInParent<Enemy>();
}
public void AnimationTrigger()
{
enemy.AnimationTrigger();
}
public void StartManualMovement()
{
enemy.ActivateManualMovement(true);
}
public void StopManualMovement()
{
enemy.ActivateManualMovement(false);
}
public void StartManualRotation()
{
enemy.ActivateManualRotation(true);
}
public void StopManualRotation()
{
enemy.ActivateManualRotation(false);
}
}
IdleState_Melee 스크립트 :
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class IdleState_Melee : EnemyState
{
private Enemy_Melee enemy;
public IdleState_Melee(Enemy enemyBase, EnemyStateMachine enemyStateMachine, string animBoolName) : base(enemyBase, enemyStateMachine, animBoolName)
{
// Enemy_Melee에 있는 다른 상태로 접근 및 전환하기 위해 선언.
// ex) enemyStateMachine.ChangeState(enemy.moveState);
enemy = enemyBase as Enemy_Melee; // Enemy_Melee 타입으로 형변환
}
public override void Enter()
{
base.Enter();
stateTimer = enemyBase.idleTime;
}
public override void Exit()
{
base.Exit();
}
public override void Update()
{
base.Update();
if (enemy.PlayerInAggressionRange())
{
enemyStateMachine.ChangeState(enemy.recoveryState);
return;
}
if (stateTimer < 0 && enemy.enemyType == EnemyType.Patrol)
{
enemyStateMachine.ChangeState(enemy.moveState);
}
}
}
MoveState_Melee 스크립트 :
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting.FullSerializer;
using UnityEngine;
public class MoveState_Melee : EnemyState
{
Enemy_Melee enemy;
Vector3 destination;
public MoveState_Melee(Enemy enemyBase, EnemyStateMachine enemyStateMachine, string animBoolName) : base(enemyBase, enemyStateMachine, animBoolName)
{
enemy = enemyBase as Enemy_Melee;
}
public override void Enter()
{
base.Enter();
enemy.agent.speed = enemy.moveSpeed;
destination = enemy.GetPatrolDestination();
enemy.agent.SetDestination(destination);
}
public override void Exit()
{
base.Exit();
}
public override void Update()
{
base.Update();
if(enemy.PlayerInAggressionRange())
{
enemyStateMachine.ChangeState(enemy.recoveryState);
return;
}
if(enemy.agent.remainingDistance <= enemy.agent.stoppingDistance )
{
enemyStateMachine.ChangeState(enemy.idleState);
}
}
}
RecoveryState_Melee 스크립트 :
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RecoveryState_Melee : EnemyState
{
Enemy_Melee enemy;
public RecoveryState_Melee(Enemy enemyBase, EnemyStateMachine enemyStateMachine, string animBoolName) : base(enemyBase, enemyStateMachine, animBoolName)
{
enemy = enemyBase as Enemy_Melee;
}
public override void Enter()
{
base.Enter();
enemy.agent.isStopped = true;
}
public override void Exit()
{
base.Exit();
}
public override void Update()
{
base.Update();
enemy.transform.rotation = enemy.FaceTarget(enemy.player.position);
if (triggerCalled)
{
if (enemy.PlayerInAttackRange())
{
enemyStateMachine.ChangeState(enemy.attackState);
}
else
{
enemyStateMachine.ChangeState(enemy.chaseState);
}
}
}
}
AttackState_Melee 스크립트 :
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AttackState_Melee : EnemyState
{
Enemy_Melee enemy;
public Vector3 attackDirection;
private float attackMoveSpeed;
private const float MAX_ATTACK_DISTANCE = 50;
public AttackState_Melee(Enemy enemyBase, EnemyStateMachine enemyStateMachine, string animBoolName) : base(enemyBase, enemyStateMachine, animBoolName)
{
enemy = enemyBase as Enemy_Melee;
}
public override void Enter()
{
base.Enter();
attackMoveSpeed = enemy.attackData.moveSpeed;
enemy.anim.SetFloat("AttackAnimationSpeed", enemy.attackData.animationSpeed);
enemy.anim.SetFloat("AttackIndex", enemy.attackData.attackIndex);
enemy.agent.isStopped = true;
enemy.agent.velocity = Vector3.zero;
attackDirection = enemy.transform.position + (enemy.transform.forward * MAX_ATTACK_DISTANCE);
}
public override void Exit()
{
base.Exit();
if (enemy.PlayerInAttackRange())
{
enemy.anim.SetFloat("RecoveryIndex", 1);
}
else
{
enemy.anim.SetFloat("RecoveryIndex", 0);
}
}
public override void Update()
{
base.Update();
if (enemy.ManualRotationActive())
{
enemy.transform.rotation = enemy.FaceTarget(enemy.player.position);
}
if (enemy.ManualMovementActive())
{
enemy.transform.position = Vector3.MoveTowards(enemy.transform.position, attackDirection, attackMoveSpeed * Time.deltaTime);
}
if (triggerCalled)
{
if (enemy.PlayerInAttackRange())
{
enemyStateMachine.ChangeState(enemy.recoveryState);
}
else
{
enemyStateMachine.ChangeState(enemy.chaseState);
}
}
}
}