본문 바로가기
Games/게임 디자인 패턴

[게임 디자인 패턴] 상태 패턴

by NCTP 2024. 7. 10.

// 유니티로 배우는 게임 디자인 패턴 제 2판 (데이비드 바론, 구진수) 을 기준으로 작성되었습니다.

참고자료 : https://unity.com/kr/how-to/develop-modular-flexible-codebase-state-programming-pattern

 

개요 - 상태 패턴 State Pattern

 

게임이 진행되면서, 게임 속의 존재(Entity)들은 저마다의 상태가 존재한다.

예를 들어, 플레이어 캐릭터는 다음과 같은 상태들이 존재할 수 있다.

  • 가만히 서있는 Idle 상태
  • 입력을 받아 움직이는 Move 상태
  • 더 빠르게 움직이는 Dash 상태
  • 점프하는 Jump 상태
  • ...

 

게임 속 존재들의 상태들을 관리하고, 내부적으로 동작하도록 제어하기 위해서 사용하는 디자인 패턴이 바로 상태 패턴이다.

상태 패턴을 통해 엔티티의 개별 상태와 상태 동작을 정의할 수 있다.

 

엥, 그거 그냥 if문써서 관리하면 되는거 아닌가요?

코드 살펴보기

아주 간단한 상태 패턴의 코드부터 살펴보자.

using System;
using System.Collections;
using System.Collections.Generic;
using Player;
using UnityEngine;

public class Item : MonoBehaviour
{
    private enum ItemStates
    {
        Idle,
        Acquired
    }
    private ItemStates itemState;

    void DoItemEffect()
    {

    }

    // Update is called once per frame
    void Update()
    {
        switch(itemState)
        {
            case ItemStates.Idle:
            break;
            case ItemStates.Acquired:
            DoItemEffect();
            break;
        }
    }
}

 

아이템 클래스의 상태별 동작을 간단하게 구현해보았다.

클래스 내에서 enum으로 상태들을 정의한 뒤에, Update 문에서 switch 문을 통해 상태 별 분기를 만들어줬다.

 

작성할 코드의 양이 적을 경우 이렇게 구현하는 것은 나쁘지 않다. 하지만 게임 속 엔티티들의 동작을

하나의 스크립트 안에 담기에는 코드의 양이 너무 방대해질 가능성이 높다.

 

하나의 cs스크립트에 몇천, 심지어 만 줄의 코드를 작성할 수도 있다..!

극단적인 예를 들었지만, 어느정도만 되어도 코드의 가독성 / 유지보수 측면에서 좋지 않은 결과를 불러온다.

 

그런 현상을 막기 위해, 인터페이스를 사용해서 상태 패턴을 구현할 수 있다.

 

코드를 살펴보기 전, 본 코드들은 러닝액션 게임의 Player를 상태 패턴으로 구현하는 데에 사용되었음을 참고하자.

코드 살펴보기 - IPlayerState 인터페이스

namespace Player
{
    public interface IPlayerState
    {
        void Handle(PlayerController controller);
        void DeleteController();
    }

}

 

Handle 매서드는 PlayerController 인스턴스를 전달한다.

이를 통해 상태 클래스가 PlayerController의 public 속성에 접근할 수 있다.

 

해당 상태에서 벗어날 때, PlayerController를 삭제하는 DeleteController 메서드도 존재한다.

이는 뒤에서 더 자세히 살펴보자.

 

코드 살펴보기 - PlayerStateContext

namespace Player
{
    public class PlayerStateContext
    {
        public IPlayerState CurrentState
        {
            get; set;
        }
        private readonly PlayerController _playerController;

        public PlayerStateContext(PlayerController playerController)
        {
            _playerController = playerController;
        }

        public void Transition()
        {
            CurrentState.Handle(_playerController);
        }
        public void Transition(IPlayerState state)
        {
            CurrentState = state;
            CurrentState.Handle(_playerController); 

        }
    }
}

 

StateContext는 해당하는 State를 저장하고 있다가, 컨트롤러가 요구할 경우 해당 State를 처리하는 역할을 한다.

상태의 전환은 StateContext를 통해 이루어진다고 보면 된다.

 

코드 살펴보기 - PlayerController 

using System.Collections;
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
using UnityEngine;

namespace Player
{
    public class PlayerController : MonoBehaviour
    {
        private IPlayerState _idleState, _moveState, _attackState; // Idle, Move, Attack States

        private PlayerStateContext _playerStateContext;
        
        void Awake()
        {
            _playerStateContext = new PlayerStateContext(this);
            _idleState = gameObject.AddComponent<PlayerIdleState>();
            _moveState = gameObject.AddComponent<PlayerMoveState>();
            _attackState = gameObject.AddComponent<PlayerAttackState>();

            _playerStateContext.Transition(_moveState);
        }

        public void IdlePlayer()
        {
            _playerStateContext.Transition(_idleState);
        }
        public void MovePlayer()
        {
            _playerStateContext.Transition(_moveState);
        }
        public void AttackPlayer()
        {
            _playerStateContext.Transition(_attackState);
        }
    }
}

 

구현이 어느정도 진행된 코드에서 핵심만 뽑아온 모습이라 불완전하다고 느껴질 수 있지만, 핵심만 보자.

 

플레이어는 현재 Idle, Move, Attack 상태를 가지고 있다.

PlayerController 는 정의해둔 PlayerContext를 통해 상태를 변경할 수 있다.

 

만약 각 상태들과 PlayerContext와 같은 기능들을 하나의 cs 스크립트에 박아넣었따면,

유지관리가 어려운 장황한 controller 클래스가 완성되었을 것이다.

 

이제 상태별 클래스를 구현하면 완성이다!

 

코드 살펴보기 - PlayerMoveState


    public class PlayerMoveState : MonoBehaviour, IPlayerState
    {
        private PlayerController _playerController;

        private int _jumpCount = 1;
        private bool _isJumping = false;
        public void Handle(PlayerController playerController)
        {
            if(!_playerController)
            {
                _playerController = playerController;
            }
            _playerController.CurrentSpeed = _playerController.speed;
            Debug.Log("Player Move");
        }
        public void DeleteController()
        {
            _playerController = null;
        }
        
        void Update()
        {
            if(_playerController)
            {
                
                float horizontal = Input.GetAxis("Horizontal");
                float vertical = Input.GetAxis("Vertical");
                //ector2 dir = new Vector2(horizontal, vertical);
                //Debug.Log(_playerController.speed);
                transform.Translate(new Vector3(horizontal, vertical, 0.0f) * _playerController.CurrentSpeed * Time.deltaTime);
            }
        }


    }

 

이렇게 구현한 뒤, Context를 통해 플레이어 상태를 Move로 변경하면,

MoveState 클래스 내의 Update문이 동작하면서 Move 상태에서의 액션들을 수행하게 된다.

 

마치며

이번 글에서는 유니티에서 사용할 수 있는 상태 패턴에 대해 살펴보았다.

 

  • 게임 속 엔티티의 상태별 행동을 상태 패턴을 통해 관리할 수 있다.
  • 너무나 길어질 수 있는 구현 방식을 보완할 수 있는 인터페이스 구현이 가능하다.

 

다음 글에서는 게임의 전역 상태를 정의한 뒤, Event Bus로 관리하는 법을 공부해보자.

댓글