본문 바로가기
게임개발/유니티 엔진

애니메이션 State 패턴 적용

by do_ng 2024. 2. 26.

상태 패턴이란?

객체가 특정 상태에 따라 행위를 달리하는 상황에서 상태를 조건문으로 검사해서 행위를 달리하는 것이 아닌 상태를 객체화하여 상태가 행동을 할 수 있도록 위임하는 패턴

 

여기서 '상태'란, 객체가 가질 수 있는 어떤 조건이나 상황을 의미한다. 

예를들어 티비가 켜져 있는 상태라면 음량 버튼을 누르면 음량이 증가하거나 감소한다. 

티비가 꺼져 있는 상태라면 음량 버튼을 눌러도 티비의 음량은 바뀌지 않는다. 

즉, 티비(객체)의 전원 상태에 따라서 메소드가 바뀐다. 

이처럼 객체가 특정 상태에 따라 행위를 달리하는 상황에서 사용되는 최적의 패턴이 state pattern 이라고 보면 된다.

 

객체 지향 프로그래밍에서의 클래스는 꼭 사물, 생물만을 표현할 수 있는 게 아니라 경우에 따라서 무형태의 행위, 동작도클래스로 묶어서 표현할 수 있다.

그래서 상태를 클래스로 표현하면 클래스를 교체해서 상태의 변화를 표현할 수 있고, 객체 내부 상태 변경에 따라 객체의 행동을 상태에 특화된 행동들로 분리해 낼 수 있으며 새로운 행동을 추가하더라도 다른 행동에 영향을 주지 않는다. 

 

상태 패턴 구현 방법

상태 패턴의 구조에는 세 가지 핵심 요소가 있다. 

  • Context 클래스는 클라이언트가 객체의 내부 상태를 변경할 수 있도록 요청하는 인터페이스를 정의하고 현재 상태에 대한 포인터(커서, 현재 상태를 가리키는)를 보유한다. 
  • State 인터페이스는 구체적인 상태 클래스를 만들때 지켜야 할 일종의 규칙 역할을 담당
  • ConcreteState 클래스는 구체적인 상태 클래스를 의미하며 State 인터페이스를 구현하고 Context 오브젝트가 상태의 동작을 트리거하기 위해 호출하는 Handle() 노출한다.

클라이언트는 객체의 상태를 업데이트하고자 Context 객체를 활용해 클라이언트가 요청한 상태로 설정한다.

Context는 항상 객체의 현재 상태를 가지고 있으며 상태에 대한 처리는 State 인터페이스를 구현한 ConcreteState 클래스에서 이루어지게 된다. 

 

1. 플레이어의 상태 정의 

public enum PlayerStateEnum
{
    Die,
    Moving,
    Wait,
}

Die : 아무 행동도 하지 못함

Moving : 클라이언트가 마우스로 해당 위치를 클릭했을 때 플레이어가 이동

Wait : 플레이어가 해당 위치에 도달했을 때 멈춤 

 

2. 각 상태별로 세부적인 동작 방법 구현

 

1. 구체적인 상태 클래스가 구현할 기본 인터페이스(State)

public interface PlayerState
{
    void Handle(PlayerController controller);
}

해당 상태에서 다른 상태 클래스로 넘어가거나 PlayerController 객체의 변수값을 가져오가나 수정할 필요가 있기 때문에PlayerController의 인스턴스를 참조할 수 있도록 파라미터로 넘겨준다.

 

2. context 클래스 구현

public class PlayerStateContext
{
    public PlayerState CurrentState
    {
        get; set;
    }

    private readonly PlayerController _playerController;

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

    // 현재 플레이어의 상태
    public void Transition()
    {
        CurrentState.Handle(_playerController);
    }

    // 현재 플레이어 상태를 업데이트 후 그 상태로 변경 
    public void Transition(PlayerState state)
    {
        CurrentState = state;
        Transition();
    }
}

 

3. PlayerController 클래스

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

public class PlayerController : MonoBehaviour
{   
    [SerializeField]
    public float speed = 10.0f;
    
    public Vector3 destPos;
    public float wait_run_ratio = 0.0f;

    private PlayerStateContext _playerStateContext;
    private PlayerState dieState, moveState, waitState;

    public enum PlayerStateEnum
    {
        Die,
        Moving,
        Wait,
    }
    public PlayerStateEnum playerState = PlayerStateEnum.Wait;

    void Start()
    {       
        Managers.Resource.Instantiate("UI/UI_Btn");

        Managers.Input.mouseAction -= OnMouseClicked;
        Managers.Input.mouseAction += OnMouseClicked;

        // state 패턴 호출 
        _playerStateContext = new PlayerStateContext(this);

        // PlayerController 컴포넌트가 붙어있는 오브젝트에 PlayerMoveState 클래스도 컴포넌트로 붙임
        moveState = gameObject.AddComponent<PlayerMoveState>();
        waitState = gameObject.AddComponent<PlayerWaitState>();
        dieState = gameObject.AddComponent<PlayerDieState>();
    }

    public void PlayerMove()
    {
        _playerStateContext.Transition(moveState);
    }

    public void PlayerWait()
    {
        _playerStateContext.Transition(waitState);
    }

    public void PlayerDie()
    {
        _playerStateContext.Transition(dieState);
    }

    void OnMouseClicked(Defines.MouseEvent evt)
    {
        if (playerState == PlayerStateEnum.Die)
            return;
                
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 2.0f);       

        RaycastHit hit; 
        if (Physics.Raycast(ray, out hit, 100.0f, LayerMask.GetMask("Wall")))
        {
            destPos = hit.point;
            PlayerMove();
            // Debug.Log($"Raycast Camera @{hit.collider.gameObject.name}");
        }
    }
}

 

플레이어 GameObject와 연결된 컴포넌트이며 플레이어의 정보(속도, 체력 ..등)를 보유하고 있고 Context 오브젝트와 상태를 초기화하는 작업을 수행한다. 

플레이어의 상태변화에 대한 로직은 각 상태 클래스에서 수행되며 PlayerController는 어떤 상태가 발생했을 때 각 상태에 맞는 메소드를 호출하기만 한다. 

 

만약 플레이어의 모든 상태 변화에 대한 로직을 PlayerController 클래스 한 곳에서 구현했다면 유지 및 관리가 힘든 장황한 controller 클래스를 사용해야 한다. 상태 패턴은 클래스를 간소화하고 유지 및 관리가 쉽도록 만들 수 있다.

 

4. PlayerState 인터페이스를 구현한 각 상태 클래스

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

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

    void UpdateMoving()
    {
        Vector3 dir = _playerController.destPos - transform.position;

        // 목적지 까지 도달한경우 (실수에서 실수를 빼면 오차범위가 있기 때문에 딱 0으로 나눠떨어지지 않음)
        if (dir.magnitude < 0.0001f)
        {
            Debug.Log("목적지 도착!");
            _playerController.PlayerWait();
        }
        else
        {
            float moveDist = Mathf.Clamp(_playerController.speed * Time.deltaTime, 0, dir.magnitude);
            transform.position += dir.normalized * moveDist;
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 20 * Time.deltaTime);

            MovingAnimationState(GetComponent<Animator>(), _playerController.speed);
        }
    }

    public void MovingAnimationState(Animator anim, float playerSpeed)
    {
        anim.SetFloat("Speed", playerSpeed); // 현재 게임상태에 대한 정보를 애니메이션 파라미터쪽으로 넘겨줌
    }

    // 애니메이션 이벤트 
    public void OnRunEvent()
    {
        Debug.Log("run event!");
    }

    void Update()
    {
        if(_playerController != null)
        {
            if (_playerController.playerState == PlayerController.PlayerStateEnum.Moving)
            {
                UpdateMoving();
            }
        }        
    }

    public void Handle(PlayerController playerController)
    {
        if (playerController != null)
            _playerController = playerController;

        _playerController.playerState = PlayerController.PlayerStateEnum.Moving;        
    }
}

 

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

public class PlayerWaitState : MonoBehaviour, PlayerState
{
    private PlayerController _playerController;

    public void WaitAnimationState(Animator anim)
    {
        _playerController.playerState = PlayerController.PlayerStateEnum.Wait;
        anim.SetFloat("Speed", 0.0f);
    }

    public void Handle(PlayerController playerController)
    {
        if (playerController != null)
            _playerController = playerController;

        WaitAnimationState(GetComponent<Animator>());
    }
}

 

새로운 상태가 추가된다 하더라도 기존의 상태 클래스를 수정하지 않고 독립적으로 새로운 상태 클래스를 만들어서 추가하면 되기 때문에 확장성이 용이하다.

해당 상태에서 다른 상태로 넘어간다고 하더라도 Context를 통해서 다른 상태를 호출하면 되기 때문에 조건문으로 모든 상태를 구현하였을 때의 복잡성을 줄일 수 있다.

 

상태 패턴의 장단점

 

상태 패턴의 장점부터 살펴보자. 

 

플레이어의 상태 변화를 조건문으로만 구분할 경우 플레이어의 상태 변화가 많아지게 되면 코드가 매우 복잡해져서 추후에 유지보수 하기가 힘든데 플레이어 상태 변화와 관련된 모든 로직이 다른 상태 클래스로 분산되므로 코드 복잡도를 줄일 수 있다.

 

상태 패턴의 단점을 살펴보자. 

 

실제로 상태 패턴을 만들 때는 상태별로 클래스를 생성해야 되는데 상태가 많아질수록 클래스도 늘어나서 관리해야할 클래스가 많아지기 때문에 객체에 적용할 상태가 적거나 상태 변화가 거의 이루어지지 않은 경우에 상태 패턴으로 구현하는 것이 불필요 할 수 있음


 

참고 1 : https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EC%83%81%ED%83%9CState-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90#:~:text=%EC%83%81%ED%83%9C%20%ED%8C%A8%ED%84%B4(State%20Pattern)%EC%9D%80,%EC%9C%84%EC%9E%84%ED%95%98%EB%8A%94%20%ED%8C%A8%ED%84%B4%EC%9D%84%20%EB%A7%90%ED%95%9C%EB%8B%A4.

 

💠 상태(State) 패턴 - 완벽 마스터하기

State Pattern 상태 패턴(State Pattern)은 객체가 특정 상태에 따라 행위를 달리하는 상황에서, 상태를 조건문으로 검사해서 행위를 달리하는 것이 아닌, 상태를 객체화 하여 상태가 행동을 할 수 있도

inpa.tistory.com

 

참고 2 : 유니티로 배우는 게임 디자인 패턴(책)

'게임개발 > 유니티 엔진' 카테고리의 다른 글

UI 자동화  (0) 2024.03.03
UI 앵커 사용법  (0) 2024.02.27
에니메이션 Lerp  (0) 2024.02.22
Raycast  (0) 2024.02.19
싱글톤 패턴 적용  (0) 2024.02.02