개발 낙서장

[Unity 2D] 유닛 확장 본문

Unity/Devlog

[Unity 2D] 유닛 확장

권승준 2023. 1. 23. 14:55

저번에는 정말 간단하게 캐릭터 좌우 움직임과 점프를 구현했다.

그럼 움직임은 여기서 끝인가? 하면 절대 절대 아니다. 나중에 멈춰있어야 하는 동작, 일정 거리만큼 움직이는 동작 등 이동에 제약이 발생하는 동작들이 추가될 경우 문제가 많아질 것이다.

그럼 이것을 어떻게 해결하냐? 유한 상태 머신(Finite State Machine)을 통해 해결할 수 있다.

유한 상태 머신(Finite State Machine)

유한 상태 머신이란 간단하게 오브젝트에 유한한 상태를 부여하고 입력에 따라 현재 상태를 변화시키며 각 상태에 맞는 출력이 이루어지는 패턴? 모델?이라고 한다.

자세한 설명은 다른 블로그나 커뮤니티에 많이 있으니 생략하고 바로 구현해 보자

public enum State { IDLE, MOVE, ATTACK, DIE, }
public interface IState {
    void OnEnter();
    void OnExit();
    void OnUpdate();
    void OnFixedUpdate();
}

우선 각 상태 Enum과 각 상태에 대한 동작을 구현할 인터페이스를 만들었다.

public class Unit : MonoBehaviour
{
    protected Rigidbody2D _rigidbody;
    protected BoxCollider2D _collider;
    protected Animator _animator;

    protected Dictionary<State, IState> _stateDic = new Dictionary<State, IState>();

    protected IState _istate;
    protected State _state;
    public State State
    {
        get { return _state; }
        set
        {
            _stateDic.TryGetValue(_state, out _istate);
            _istate.OnExit();
            _state = value;
            _stateDic.TryGetValue(_state, out _istate);
            _istate.OnEnter();
        }
    }

    public float _moveSpeed = 3f;
    public float _maxSpeed = 10f;
    public float _jumpForce = 15f;

    protected virtual void Awake()
    {
        TryGetComponent(out _rigidbody);
        TryGetComponent(out _collider);
        TryGetComponent(out _animator);
    }

    protected virtual void Update()
    {
        _istate.OnUpdate();
    }

    protected virtual void FixedUpdate()
    {
        _istate.OnFixedUpdate();
    }

    public void Move(float moveX)
    {
        _rigidbody.AddForce(Vector2.right * moveX * _moveSpeed, ForceMode2D.Impulse);
        if (_rigidbody.velocity.x > _maxSpeed)
            _rigidbody.velocity = new Vector2(_maxSpeed, _rigidbody.velocity.y);
        else if (_rigidbody.velocity.x < -_maxSpeed)
            _rigidbody.velocity = new Vector2(-_maxSpeed, _rigidbody.velocity.y);
    }

    public virtual void PlayAnimation(State state)
    {

    }
}

그리고 Unit이라는 스크립트를 새로 만들어 각 유닛들이 해당 스크립트를 상속하게 만들었다. 결국 플레이어 유닛만 존재하는 것이 아니라 여러 유닛들이 존재하게 될 것이므로 확장성을 위해 조금 수정해 주었다.

각 유닛마다 각자의 상태를 관리하기 위해 Dictionary를 만들어주었고 State가 변경될 때마다 Dictionary에서 IState를 꺼내 동작을 실행해 주었다.
Move 함수의 경우 유닛 스크립트에서 직접 실행해 주는 게 낫겠다고 판단해서 이렇게 해두었는데 이렇게 하면
Istate.OnFixedUpdate() -> (IState 내부에서)_unit.Move(moveX) -> (Unit 내부에서) Move(moveX)
이렇게 함수 호출 경로가 조금 복잡 해지게 된다. 그래서 IState에서 Unit의 Rigidbody를 건드려주는 게 좋을지 아니면 그냥 이렇게 하는 게 나을지 조금 고민이긴 하다.
어떤 방식으로 하든 결과는 같겠지만 성능의 차이는 존재할 것이다.

어쨌든 이런 방식으로 간단하게 상태 패턴을 구현했다.

FSM

이전이랑 움직임 자체는 같다. 다만 상태 패턴을 바탕으로 유닛이 동작하기 때문에 직관성이 좋아졌다고 할 수 있다.

다른 유닛 추가

Unit 스크립트를 통해 확장성도 좋아졌고 상태 패턴을 추가해 직관성도 좋아졌다. 그럼 이제 다른 유닛을 만들어볼 차례!
캐릭터를 쫓는 간단한 적 유닛을 만들어보자.

대부분의 적들은 동작이 비슷할 것이므로 위에서 했던 것처럼 Unit을 상속하는 EnemyController, IState를 상속하는 EnemyIdle, EnemyMove 등등을 만들고 간단하게 로직을 짜주자.

    public void OnUpdate()
    {
        var hit = Physics2D.OverlapBox(_unit.transform.position, new Vector2(_range, 0.1f), 0f, LayerMask.GetMask("Player"));
        if (hit)
        {
            _moveX = -(_unit.transform.position.x - hit.transform.position.x);
            if (_moveX < 0f)
                _unit.transform.localScale = new Vector2(1f, 1f);
            else if (_moveX > 0f)
                _unit.transform.localScale = new Vector2(-1f, 1f);
        }
        else
            _moveX = 0f;
    }

    public void OnFixedUpdate()
    {
        _unit.Move(_moveX);
    }

EnemyMove 스크립트의 일부이다. 일정 범위 내에 플레이어가 있는지 탐색하고 있으면 쫓아가고 없으면 그 자리에 가만히 있게 했다.

범위는 이 정도로 해서 저 빨간 범위에 플레이어가 들어가면 추적할 것이고 벗어나면 그 자리에서 멈출 것이다.

적이 플레이어를 탐지해서 잘 따라오는 모습이다. 지금은 진짜 기본 기능만 구현해 놔서 어색하지만 나중에 플레이어와 일정 거리가 됐을 때는 멈추고 공격 등의 상태로 변화시킨다던가 하면 꽤 그럴듯한 적 AI가 될 것 같다.

확실히 FSM을 사용하니 직관성이 좋고 코드 수정이 용이한 것 같다. 하지만 각 상태별로 관리해줘야 하기 때문에 확장성 부분에서는 확실히 단점이 커 보인다.
지금이야 동작 자체가 몇 개 없지만 동작이 100가지 200가지가 된다면? 유닛 별로 다 동작이 달라서 각각 관리해줘야 한다면?
아마 그때가 되면 또 다른 패턴을 찾긴 하겠지만 지금 현재 상태 패턴으로는 수많은 상태들을 관리하기엔 조금 벅찰 것 같다.
이런 간단한 게임 개발에서는 충분히 좋은 패턴이라고 생각한다. 앞으로 공부하면서 계속 수정해 나가야겠다.

'Unity > Devlog' 카테고리의 다른 글

[Unity2D] 인벤토리 시스템  (0) 2023.03.24
[Unity2D] 포톤 서버로 멀티 구현  (0) 2023.02.20
[Unity 2D] 캐릭터 움직임  (0) 2023.01.23
[Unity 3D] NPC 퀘스트 팝업  (0) 2023.01.16
[Unity 3D] NPC 대화  (0) 2023.01.14
Comments