공부블로그

[공부 블로그] 유니티 FSM에 대하여 - 2

bimtaeur30 2025. 9. 13. 16:51

안녕하세요.

오늘은 어제 포스팅했던 '유니티 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에 더 익숙해지기 위해서 유니티로 다시 구현해보도록 하겠습니다.

감사합니다