안녕하세요.
오늘은 어제 포스팅했던 '유니티 FSM에 대하여 - 1'의 내용을 이어가도록 하겠습니다.
어제는 FSM이 실제로 작동할 수 있도록 EnemyBrain을 만들고 마무리했었습니다.
먼저 Enemy가 이동할 수 있도록 Movement 스크립트를 붙여주겠습니다. (플레이어의 이동 스크립트를 재활용)
using System;
using System.Net.NetworkInformation;
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
public Rigidbody2D RbCompo { get; private set; }
[SerializeField] private float _moveSpeed = 5f, _jumpPower = 7f;
[SerializeField] private Transform _groundChecker;
[SerializeField] private Vector2 _groundCheckerSize;
[SerializeField] private LayerMask _whatIsGround;
[Header("Extra Setting")]
[SerializeField] private float _extraGravity = 30f;
[SerializeField] private float _gravityDelay = 0.15f;
private float _timeInAir; //공중에 있는 시간
private float _xMove; //이동방향
[field: SerializeField] public bool IsGrounded { get; private set; }
private void Awake()
{
RbCompo = GetComponent<Rigidbody2D>();
}
public void SetXMove(float xMove)
{
_xMove = xMove;
}
private void Update()
{
CalculateAirTime();
}
private void CalculateAirTime()
{
if (!IsGrounded)
_timeInAir += Time.deltaTime;
else
_timeInAir = 0;
}
private void ApplyExtraGravity()
{
if (_timeInAir > _gravityDelay)
{
RbCompo.AddForce(Vector2.down * _extraGravity);
}
}
private void FixedUpdate()
{
IsGrounded = CheckGround();
HorizontalMove();
ApplyExtraGravity();
}
private void HorizontalMove()
{
float xVelocity = _xMove * _moveSpeed;
RbCompo.linearVelocityX = xVelocity;
}
public void Jump()
{
RbCompo.AddForce(Vector2.up * _jumpPower, ForceMode2D.Impulse);
}
public bool CheckGround()
{
Collider2D collider = Physics2D.OverlapBox(_groundChecker.position, _groundCheckerSize, 0, _whatIsGround);
return collider;
}
private void OnDrawGizmos()
{
if (_groundChecker == null) return;
Gizmos.color = Color.red;
Gizmos.DrawWireCube(_groundChecker.position, _groundCheckerSize);
Gizmos.color = Color.white;
}
}
이제 Enemy를 담당할 Enemy스크립트를 만들어주겠습니다.
using System;
using System.Collections;
using UnityEngine;
public class Enemy : MonoBehaviour
{
[Header("Enemy Settings")]
public float chaseRadius;
public float attackRadius;
public LayerMask playerMask;
public PlayerMovement MoveCompo { get; private set; }
public Animator AnimCompo { get; private set; }
public Transform target;
private Transform _visualTrm;
private void Awake()
{
MoveCompo = GetComponent<PlayerMovement>();
AnimCompo = GetComponentInChildren<Animator>();
_visualTrm = transform.Find("Visual");
}
public void FlipX(float xMove)
{
if (xMove > 0)
_visualTrm.eulerAngles = new Vector3(0, 180f, 0);
else if (xMove < 0)
_visualTrm.eulerAngles = new Vector3(0, 0, 0);
}
public Collider2D CheckPlayerInChaseRange()
{
return Physics2D.OverlapCircle(transform.position, chaseRadius, playerMask);
}
public Collider2D CheckPlayerInAttackRange()
{
return Physics2D.OverlapCircle(transform.position, attackRadius, playerMask);
}
private void OnDrawGizmos()
{
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, attackRadius);
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, chaseRadius);
}
internal bool CanAttack()
{
throw new NotImplementedException();
}
}
Enemy가 Idle상태일때는 정지를 해야하기 때문에 enemy 스크립트가 필요합니다.
EnemyState를 생성할때 생성자로 enemy 스크립트를 받아오도록 하겠습니다.
public abstract class EnemyState
{
protected Enemy _enemy;
protected int _animBoolHash;
protected bool _endTriggerCalled;
protected EnemyStateMachine _stateMachine;
public EnemyState(Enemy enemy, EnemyStateMachine stateMachine, string animBoolName)
{
_enemy = enemy;
_stateMachine = stateMachine;
_animBoolHash = Animator.StringToHash(animBoolName);
}
}
Idle 상태로직을 작성해보겠습니다. 대기하다가 플레이어가 추적거리에 들어오면 추적상태로 변경할겁니다.
상태를 변경하려면 stateMachine을 알고있어야합니다. stateMachine도 생성자를 통해 가져오겠습니다.
using UnityEngine;
//행동에 따라 적절한 상태로 전환하기 위한 조건을 확인
public class EnemyBrain : MonoBehaviour
{
private EnemyStateMachine enemyStateMachine;
private Enemy _enemy;
[SerializeField] private LayerMask _playerLayer;
[SerializeField] private float chaseRange = 5f;
[SerializeField] private float attackRange = 1f;
private void Awake()
{
enemyStateMachine = new EnemyStateMachine();
_enemy = GetComponent<Enemy>();
enemyStateMachine.AddState(EnemyStateType.Idle, new EnemyIdleState(_enemy, enemyStateMachine, "Idle"));
enemyStateMachine.AddState(EnemyStateType.Chase, new EnemyChaseState(_enemy, enemyStateMachine, "Chase"));
enemyStateMachine.AddState(EnemyStateType.Attack, new EnemyAttackState(_enemy, enemyStateMachine, "Attack"));
}
private void Start()
{
enemyStateMachine.Initialized(EnemyStateType.Idle);
}
private void Update()
{
enemyStateMachine.currentState.UpdatetState();
}
}
public class EnemyIdleState : EnemyState
{
private readonly float _checkTimer = 0.3f;
private float _lastCheckTime;
public EnemyIdleState(Enemy enemy, EnemyStateMachine stateMachine, string animBoolName) : base(enemy, stateMachine, animBoolName)
{
}
public override void Enter()
{
base.Enter();
_enemy.MoveCompo.SetXMove(0f);
_lastCheckTime = Time.time;
}
public override void UpdatetState()
{
//플레이어와의 거리를 확인하고 추적거리가 되면 추적상태로 변경
if (_lastCheckTime + _checkTimer < Time.time)
{
_lastCheckTime = Time.time;
if (_enemy.CheckPlayerInChaseRange())
{
_stateMachine.ChangeState(EnemyStateType.Chase);
return;
}
}
}
}
추적상태를 만들 차례입니다. EnemyChaseState에서는 추적, 거리확인을 구현하겠습니다.
using UnityEngine;
public class EnemyChaseState : EnemyState
{
public EnemyChaseState(Enemy enemy, EnemyStateMachine stateMachine, string animBoolName) : base(enemy, stateMachine, animBoolName)
{
}
public override void Enter()
{
base.Enter();
}
public override void UpdatetState()
{
//타겟(플레이어) 위치로 이동 시도함
Vector2 dir = _enemy.target.position - _enemy.transform.position;
_enemy.MoveCompo.SetXMove(Mathf.Sign(dir.x));
_enemy.FlipX(Mathf.Sign(dir.x));
//플레이어와 거리가 멀어지면 대기 상태로 변경함
if (!_enemy.CheckPlayerInChaseRange())
{
_stateMachine.ChangeState(EnemyStateType.Idle);
return;
}
//공격거리에 들어오면 공격 상태로 변경함
else if (_enemy.CheckPlayerInAttackRange())
{
_stateMachine.ChangeState(EnemyStateType.Attack);
return;
}
}
public override void Exit()
{
//애니메이션 상태 초기화
base.Exit();
}
}
공격상태를 완성시켜보겠습니다. 공격을 담당하는 스크립트인 EnemyAttack 스크립트를 만들고, 공격하는 기능을 추가해줄겁니다. 다음은 EnemyAttack 스크립트입니다. Enemy는 나뭇잎을 던지는 방식으로 공격합니다. 나뭇잎이 날라가는 스크립트는 생략하겠습니다.
using System;
using UnityEngine;
public class EnemyAttack : MonoBehaviour
{
[SerializeField] private Leaf _leafPrefab;
[SerializeField] private float _coolDown;
private float _lastAttackTime;
private Enemy _enemy;
public bool AnimationEndTrigger = false;
private void Awake()
{
_enemy = GetComponent<Enemy>();
}
private void Start()
{
_enemy.GetComponentInChildren<EnemyAnimator>().OnEndAnimtion += () => AnimationEndTrigger = true;
}
public bool CanAttack() // 쿨타임 확인
{
return Time.time >= _lastAttackTime + _coolDown;
}
public void Attack()
{
_lastAttackTime = Time.time;
Vector2 dir = _enemy.target.position - transform.position;
var bomb = Instantiate(_leafPrefab, transform.position, Quaternion.identity);
bomb.TrowLeaf(new Vector2(dir.x , 0) * 4, 3f);
}
}
마지막으로 EnemyAttackState로 마무리하겠습니다.
using UnityEngine;
public class EnemyAttackState : EnemyState
{
private EnemyAttack _atkCompo;
private Enemy _enemy;
public EnemyAttackState(Enemy enemy, EnemyStateMachine stateMachine, string animBoolName) : base(enemy, stateMachine, animBoolName)
{
_enemy = enemy;
_atkCompo = enemy.GetComponent<EnemyAttack>();
}
public override void Enter()
{
base.Enter();
FacingToPlayer(); //플레이어를 바라보기
_enemy.MoveCompo.SetXMove(0f); //정지하기
_atkCompo.AnimationEndTrigger = false;
if (_atkCompo.CanAttack()) // 공격이 가능하다면 공격
{
_atkCompo.Attack();
}
}
public override void UpdatetState()
{
base.UpdatetState();
if (_atkCompo.AnimationEndTrigger) //애니메이션 끝나면
{
if (!_enemy.CheckPlayerInAttackRange()) // 거리확인
{
_stateMachine.ChangeState(EnemyStateType.Idle);
return;
}
else
{
if (_atkCompo.CanAttack())
{
FacingToPlayer(); //플레이어 처다보기
_atkCompo.Attack();
}
}
}
}
public override void Exit()
{
base.Exit();
}
private void FacingToPlayer()
{
float xDir = _enemy.target.position.x - _enemy.transform.position.x;
_enemy.FlipX(Mathf.Sign(xDir));
}
}
이렇게 대기 - 추척 - 공격 FSM을 구현했습니다.
비록 선생님이 올려주신 자료를 따라서 타이핑했지만 이제 FSM에 더 익숙해지기 위해서 유니티로 다시 구현해보도록 하겠습니다.
감사합니다
'공부블로그' 카테고리의 다른 글
| [공부 블로그] LINQ - 1 (0) | 2025.10.26 |
|---|---|
| [공부 블로그] Base 키워드 (0) | 2025.10.16 |
| [공부블로그] 유니티 FSM에 대하여 - 1 (0) | 2025.09.13 |
| [공부 블로그] 유니티 오브젝트 거리 측정하기 & 원리 (0) | 2025.09.04 |
| [공부 블로그] ref와 out의 차이 (2) | 2025.09.01 |