Player 상태 구현
Animator Controller


코드 구현
public class CharacterMovement : MonoBehaviour
{
Transform _transform;
Animator _anim;
float _h, _v, _r;
[SerializeField] private float _moveSpeed = 5f;
[SerializeField] private float _turnSpeed = 60f;
_r = Input.GetAxis("Mouse X");
_transform.Rotate(Vector3.up * _r * _turnSpeed * Time.deltaTime);
_h = Input.GetAxis("Horizontal");
_v = Input.GetAxis("Vertical");
if (_v != 0)
{
_transform.Translate(Vector3.forward * _v * _moveSpeed * Time.deltaTime);
}
if (_h != 0)
{
_transform.Translate(Vector3.right * _h * _moveSpeed * Time.deltaTime);
}
_anim.SetFloat("Forward", _v);
_anim.SetFloat("Side", _h);
Monster 상태 구현
Animator Controller

코드 구현
public enum MonsterState
{
IDLE,
PATROL,
CHASE,
COMBAT,
DAMAGED,
DEAD
}
public class MonsterAI : MonoBehaviour
{
[SerializeField] private MonsterState _state = MonsterState.IDLE;
private Animator _anim;
private Collider _collider;
private NavMeshAgent _agent;
private Coroutine _currentCoroutine;
[Header("[Idle State]")]
[SerializeField] private float _idleTime = 0f;
[Header("[Patrol State]")]
[SerializeField] private GameObject _destObj;
[SerializeField] private Vector3 _patrolDest;
[SerializeField] private float _rotateSpeed = 120f;
[SerializeField] private float _patrolAccel = 8f;
[SerializeField] private float _patrolRange = 5f;
[SerializeField] private float _patrolMoveSpeed = 3f;
[SerializeField] private float _patrolAngularSpeed = 350f;
[Header("[Chase State]")]
[SerializeField] private float _chaseSpeed = 8f;
[SerializeField] private float _chaseAccel = 8f;
[SerializeField] private float _chaseStoppingDistance = 2f;
[SerializeField] private float _chaseRange = 8f;
[SerializeField] private float _chaseAngularSpeed = 3f;
private Vector3 _prevPosition;
private GameObject _target;
[Header("[Combat State]")]
[SerializeField] private float _attackRange = 3.5f;
[SerializeField] private float _damage = 5f;
[SerializeField] private float _attackInterval = 2f;
[SerializeField] private float _hp = 20f;
private float _pendingDamage = 0f;
private bool _isAlive = true;
public bool IsAlive { get { return _isAlive; } }
[Header("Damaged State")]
[SerializeField] private float _damageStunTime = 0.5f;
[Header("Dead State")]
[SerializeField] private float _deadTime = 2f;
private int _hashPatrol = Animator.StringToHash("Patrol");
private int _hashChase = Animator.StringToHash("Chase");
private int _hashCombat = Animator.StringToHash("Combat");
private int _hashDamaged = Animator.StringToHash("Damaged");
private int _hashDead = Animator.StringToHash("Dead");
private void Start()
{
_anim = GetComponent<Animator>();
_collider = GetComponent<Collider>();
_agent = GetComponent<NavMeshAgent>();
if (_agent != null)
{
_agent.updateRotation = true;
_agent.angularSpeed = _rotateSpeed;
}
ChangeState(_state);
}
private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("Bullet"))
{
BulletController bullet = collision.gameObject.GetComponent<BulletController>();
TakeDamage(bullet.Damage);
}
}
private void ChangeState(MonsterState monState)
{
if (_currentCoroutine != null)
{
StopCoroutine(_currentCoroutine);
}
_state = monState;
ApplyAgentSetting(_state);
switch (_state)
{
case MonsterState.IDLE: _currentCoroutine = StartCoroutine(Co_Idle()); break;
case MonsterState.PATROL: _currentCoroutine = StartCoroutine(Co_Patrol()); break;
case MonsterState.CHASE: _currentCoroutine = StartCoroutine(Co_Chase()); break;
case MonsterState.COMBAT: _currentCoroutine = StartCoroutine(Co_Combat()); break;
case MonsterState.DAMAGED: _currentCoroutine = StartCoroutine(Co_Damaged()); break;
case MonsterState.DEAD: _currentCoroutine = StartCoroutine(Co_Dead()); break;
}
}
private IEnumerator Co_Idle()
{
SetAnimation();
if (_agent) _agent.isStopped = true;
float waitTime = Random.Range(2f, 4f);
_target = GameObject.FindGameObjectWithTag("Player");
float elapsed = 0f;
while (true)
{
if (IsFindTarget(_chaseRange))
{
ChangeState(MonsterState.CHASE);
yield break;
}
elapsed += Time.deltaTime;
if (elapsed >= waitTime)
{
ChangeState(MonsterState.PATROL);
yield break;
}
yield return null;
}
}
private IEnumerator Co_Patrol()
{
SetAnimation(hashPatrol: true);
_patrolDest = transform.position + new Vector3(
Random.Range(-_patrolRange, _patrolRange),
0f,
Random.Range(-_patrolRange, _patrolRange)
);
if (_destObj) _destObj.transform.position = _patrolDest;
_target = GameObject.FindGameObjectWithTag("Player");
if (_agent)
{
_agent.isStopped = false;
_agent.speed = _patrolMoveSpeed;
_agent.SetDestination(_patrolDest);
}
while (true)
{
if (IsFindTarget(_chaseRange))
{
ChangeState(MonsterState.CHASE);
yield break;
}
if (_agent && !_agent.pathPending && _agent.remainingDistance <= 0.2f)
{
ChangeState(MonsterState.IDLE);
yield break;
}
yield return null;
}
}
private IEnumerator Co_Chase()
{
SetAnimation(hashChase: true);
_prevPosition = transform.position;
_target = GameObject.FindGameObjectWithTag("Player");
if (_agent)
{
_agent.isStopped = false;
_agent.speed = _chaseSpeed;
}
while (true)
{
if (_target == null)
{
ChangeState(MonsterState.IDLE);
yield break;
}
float dist = Vector3.Distance(transform.position, _target.transform.position);
if (_agent) _agent.SetDestination(_target.transform.position);
if (dist <= _attackRange)
{
ChangeState(MonsterState.COMBAT);
yield break;
}
if (dist > _chaseRange)
{
ChangeState(MonsterState.IDLE);
yield break;
}
yield return null;
}
}
private IEnumerator Co_Combat()
{
SetAnimation(hashCombat: true);
_target = GameObject.FindGameObjectWithTag("Player");
if (_agent)
{
_agent.isStopped = true;
_agent.velocity = Vector3.zero;
}
float currentCooldown = _attackInterval;
while (true)
{
if (_target == null || _target.GetComponent<Combat>().IsAlive == false)
{
ChangeState(MonsterState.IDLE);
yield break;
}
Vector3 dirToTarget = (_target.transform.position - transform.position).normalized;
RotatetoDirection(dirToTarget);
currentCooldown -= Time.deltaTime;
if (currentCooldown <= 0)
{
float distTarget = Vector3.Distance(transform.position, _target.transform.position);
if (distTarget > _attackRange)
{
ChangeState(MonsterState.CHASE);
yield break;
}
_target.GetComponent<Combat>().TakeDamage(_damage);
currentCooldown = _attackInterval;
}
yield return null;
}
}
private IEnumerator Co_Damaged()
{
_anim.SetTrigger(_hashDamaged);
_hp -= _pendingDamage;
if (_agent)
{
_agent.isStopped = true;
_agent.velocity = Vector3.zero;
}
if (_hp <= 0)
{
_isAlive = false;
ChangeState(MonsterState.DEAD);
yield break;
}
yield return new WaitForSeconds(_damageStunTime);
if (IsFindTarget(_chaseRange))
{
if (IsFindTarget(_attackRange))
{
ChangeState(MonsterState.COMBAT);
}
else
{
ChangeState(MonsterState.CHASE);
}
}
else
{
ChangeState(MonsterState.IDLE);
}
}
private IEnumerator Co_Dead()
{
_anim.SetTrigger(_hashDead);
_collider.enabled = false;
if (_agent) _agent.enabled = false;
yield return new WaitForSeconds(_deadTime);
Destroy(gameObject);
}
private void RotatetoDirection(Vector3 dir)
{
if (dir == Vector3.zero) return;
Quaternion targetRot = Quaternion.LookRotation(dir);
transform.rotation = Quaternion.Slerp(
transform.rotation, targetRot, _rotateSpeed * Time.deltaTime);
}
private bool IsFindTarget(float range)
{
if (_target)
{
float distTarget = Vector3.Distance(transform.position, _target.transform.position);
if (distTarget < range)
{
return true;
}
}
return false;
}
public void TakeDamage(float damage)
{
_pendingDamage = damage;
ChangeState(MonsterState.DAMAGED);
}
public void SetAnimation(bool hashPatrol = false, bool hashChase = false, bool hashCombat = false)
{
_anim.SetBool(_hashPatrol, hashPatrol);
_anim.SetBool(_hashChase, hashChase);
_anim.SetBool(_hashCombat, hashCombat);
}
문제 해결 방법
- Monster가 Player에게 Chase 할 때 Idle 상태로 쭉 끌려 오는 문제가 있었습니다. Has Exit Time 체크 해제를 하지 않아서 애니메이션이 바뀌지 않는 문제 였습니다.
- Monster가 걸어 다닐 때 시선을 회전 하지 않고 걸어다니는 문제가 있었습니다. Rotate Speed의 값을 설정 하지 않아서 생긴 문제였습니다. Rotate Speed 값을 늘려주어서 해결 하였습니다.
- Monster의 상태 구현에서 Coroutine과 여러 함수들이 있는데 이것은 다음 포스팅들에서...