상태 패턴이란?
상태 패턴(State Pattern)이란 객체가 특정 상태에 따라 행동을 달리하는 상황에서 각 상태들을 객체화하여 객체가 상태에게 행동을 위임하도록 하는 디자인 패턴이다.
간단한 클래스 다이어그램으로 예시를 들면
- Monster 클래스 : 현재 상태를 정의하고 내부 상태에 따라 현재 상태를 전환하고 현재 상태에게 행동을 위임한다.
- IMonsterState 인터페이스 : ConcreteState들이 행동을 위임받기 위해 구현해야 할 함수들을 정의한다.
- ConcreteState 클래스 : Monster 클래스가 행동을 위임하기 위해 호출하는 IMonsterState 인터페이스를 구현한다.
Monster 클래스에서는 내부 상태에 따라 기본, 추적, 공격 상태로 전환된다. 이때 현재 상태를 IMonsterState 인터페이스로 선언한 뒤 원하는 상태로 전환을 해가며 현재 상태에 대한 행동은 IMonsterState에 요청한다.
상태 패턴은 여러 조건문과 플래그 변수들을 사용하면서 상태에 따른 행동들을 제어해야 할 때 사용해 볼 수 있다.
상태 패턴 사용 예시
몬스터 구현 조건
기본 상태
- 시작 시 : 경고 표시를 비활성화한다.
- 유지 동안 : 자신의 원래 자리로 돌아간다.
- 종료 시 : 경고 표시를 활성화한다.
추적 상태
- 시작 시 : 이동 애니메이션을 활성화한다.
- 유지 동안 : 플레이어를 추적한다.
- 종료 시 : 이동 애니메이션을 비활성화한다.
공격 상태
- 시작 시 : 공격 쿨타임을 초기화한다.
- 유지 동안 : 공격 쿨타임 적용, 쿨타임마다 공격 애니메이션을 실행한다.
문제 상황 예시
다음은 상태패턴을 사용하지 않고 몬스터를 구현할 시 문제점이다.
using UnityEngine;
public class Monster : MonoBehaviour
{
// 기본 정보
[Header("Base Info")]
public float speed;
public Vector2 basePos;
public Animator anim;
public GameObject warningSign;
// 추적 정보
[Header("Chase Info")]
[SerializeField] private float chaseDis;
private bool isChase;
private Transform playerPos;
// 공격 정보
[Header("Attack Info")]
[SerializeField] private float attackDis;
private bool isAttack;
private float atkCool = 1.5f;
private float atkCur = 0;
private void Awake()
{
basePos = transform.position;
playerPos = GameObject.FindGameObjectWithTag("Player").transform;
}
private void Update()
{
Idle();
Chase();
Attack();
}
// 기본 상태 함수
private void Idle()
{
if (!isChase && !isAttack)
{
warningSign.SetActive(false);
transform.position = Vector2.MoveTowards(transform.position, basePos, speed * 2 * Time.deltaTime);
}
}
// 추적 상태 함수
private void Chase()
{
if (IsPlayerExistInDistance(chaseDis) && !isAttack)
{
isChase = true;
warningSign.SetActive(true);
transform.Translate((playerPos.position - transform.position).normalized * speed * Time.deltaTime);
}
else
{
isChase = false;
}
}
// 공격 상태 함수
private void Attack()
{
if (IsPlayerExistInDistance(attackDis))
{
isAttack = true;
warningSign.SetActive(true);
if (atkCur > 0) atkCur -= Time.deltaTime;
else
{
anim.SetTrigger("Attack");
atkCur = atkCool;
}
}
else
{
isAttack = false;
atkCur = 0;
}
}
// 거리 내에 플레이어 존재 유무 반환 함수
private bool IsPlayerExistInDistance(float distance)
{
return Vector2.Distance(transform.position, playerPos.position) <= distance;
}
}
이렇게 몬스터를 구현하게 될 경우 유지 보수 및 확장이 어려워진다.
예를 들면 공격 상태에서도 플레이어를 추적하고 싶다면 각 상태 함수들의 조건문을 수정해 줘야 하며 또 다른 조건문을 추가해야 하므로 유지 보수가 어려워진다.
또는 기본, 추적, 공격 상태 외에도 죽음, 특수 공격, 기절 등의 상태가 더 추가된다면 그에 따른 플래그 변수와 조건문이 추가되며 이렇게 된다면 확장성이 떨어지고 코드를 망치게 된다.
상태 패턴으로 구현
다음은 상태 패턴을 사용하여 몬스터를 구현하였다.
Monster
Monster 클래스에서는 현재 상태와 각 ConcreteState들을 저장하고 상태 변경 조건을 확인하여 현재 상태를 변경한다. 또한 현재 상태에게 행동을 위임한다.
using UnityEngine;
using System.Collections.Generic;
// ConcreteState들을 저장하는 Dictionary에서 키값으로 사용할 열겨형
public enum StateName
{
IDLE,
CHASE,
ATTACK
}
public class Monster : MonoBehaviour
{
// 기본 정보
[Header("Base Info")]
public float speed;
public Vector2 basePos;
public Animator anim;
public GameObject warningSign;
// 플레이어 체크 정보
[Header("Player Check Info")]
[SerializeField] private float chaseDis;
[SerializeField] private float attackDis;
private Transform playerPos;
// 상태 정보
[Header("State Info")]
private IMonsterState currentState; // 현재 상태
private Dictionary<StateName, IMonsterState> states = new Dictionary<StateName, IMonsterState>(); // ConcreteState들을 저장하는 Dictionary
private void Awake()
{
basePos = transform.position;
playerPos = GameObject.FindGameObjectWithTag("Player").transform;
// AddComponent로 추가한 ConcreteState들을 StateName 열겨형을 키값으로 사용하여 Dictionary에 저장
states.Add(StateName.IDLE, gameObject.AddComponent<MonsterIdleState>());
states.Add(StateName.CHASE, gameObject.AddComponent<MonsterChaseState>());
states.Add(StateName.ATTACK, gameObject.AddComponent<MonsterAttackState>());
// 현재 상태를 Idle(기본) 상태로 설정
ChangeState(StateName.IDLE);
}
private void Update()
{
StateCheck();
UpdateState();
}
// 상태의 변경 조건을 체크하고 상태를 변경하는 함수
private void StateCheck()
{
if (IsPlayerExistInDistance(attackDis))
{
ChangeState(StateName.ATTACK);
}
else if (IsPlayerExistInDistance(chaseDis))
{
ChangeState(StateName.CHASE);
}
else
{
ChangeState(StateName.IDLE);
}
}
// 거리 내에 플레이어 존재 유무 반환 함수
private bool IsPlayerExistInDistance(float distance)
{
return Vector2.Distance(transform.position, playerPos.position) <= distance;
}
// 자기 위치에서 플레이어 위치까지의 방향을 반환하는 함수
public Vector2 DirectionToPlayer()
{
return (playerPos.position - transform.position).normalized;
}
// 상태 변경 함수
private void ChangeState(StateName changeState)
{
// 현재 상태가 없을 시
if (currentState == null)
{
currentState = states[changeState];
currentState.EnterState(this);
}
// 변경하려는 상태가 현재 상태가 아닐 시
else if (currentState != states[changeState])
{
currentState.ExitState(this);
currentState = states[changeState];
currentState.EnterState(this);
}
}
// 매프레임 마다 currentState의 UpdateState를 호출하는 함수
private void UpdateState()
{
currentState?.UpdateState(this);
}
}
IMonsterState
IMonsterState 인터페이스는 해당 인터페이스를 상속받는 ConcreteState가 상태가 시작될 때, 상태가 유지되는 동안, 상태가 종료될 때 실행될 함수를 구현하도록 한다.
public interface IMonsterState
{
public void EnterState(Monster monster); // 상태가 시작될 때 실행
public void UpdateState(Monster monster); // 상태가 유지되는 동안 실행
public void ExitState(Monster monster); // 상태가 종료될 때 실행
}
ConcreteState
각 ConcreteState들은 Monster 클래스의 행동을 대신할 수 있도록 IMonsterState를 구현한다.
using UnityEngine;
public class MonsterIdleState : MonoBehaviour, IMonsterState
{
public void EnterState(Monster monster)
{
// 경고 표시 오브젝트 비활성화
monster.warningSign.SetActive(false);
}
public void UpdateState(Monster monster)
{
// BasePos로 이동
transform.position = Vector2.MoveTowards(transform.position, monster.basePos, monster.speed * 2 * Time.deltaTime);
}
public void ExitState(Monster monster)
{
// 경고 표시 오브젝트 활성화
monster.warningSign.SetActive(true);
}
}
using UnityEngine;
public class MonsterChaseState : MonoBehaviour, IMonsterState
{
public void EnterState(Monster monster)
{
// 이동 애니메이션 활성화
monster.anim.SetBool("IsChase", true);
}
public void UpdateState(Monster monster)
{
// 플레이어 위치로 이동
transform.Translate(monster.DirectionToPlayer() * monster.speed * Time.deltaTime);
}
public void ExitState(Monster monster)
{
// 이동 애니메이션 비활성화
monster.anim.SetBool("IsChase", false);
}
}
using UnityEngine;
public class MonsterAttackState : MonoBehaviour, IMonsterState
{
// 공격 정보
[Header("Attack Info")]
private float atkCool = 1.5f;
private float atkCur = 0;
public void EnterState(Monster monster)
{
// 공격 쿨타임 초기화
atkCur = 0;
}
public void UpdateState(Monster monster)
{
// 공격 로직 작성
if (atkCur > 0) atkCur -= Time.deltaTime;
else
{
monster.anim.SetTrigger("Attack");
atkCur = atkCool;
}
}
public void ExitState(Monster monster) { }
}
Player
Player 클래스에서는 간단한 이동로직만 구현했다.
using UnityEngine;
public class Player : MonoBehaviour
{
public float speed;
private void Update()
{
float x = Input.GetAxisRaw("Horizontal") * speed * Time.deltaTime;
float y = Input.GetAxisRaw("Vertical") * speed * Time.deltaTime;
Vector2 movePos = new Vector2(x, y);
transform.Translate(movePos);
}
}
구현 결과
상태 패턴의 장단점
장점
- 유지 보수 : 각 상태의 행동들을 개별적인 클래스들로 관리할 수 있다.
- 확장 : 각 상태별 행동을 추가, 수정하거나 새로운 상태를 추가할 때 조건문과 플래그 변수 없이 쉽게 추가할 수 있다.
단점
- 관리 : ConcreteState들이 많아질수록 관리해야 할 클래스 수가 늘어난다.
- 전환 처리 : 상태 간의 관계와 조건에 따라 상태 전환을 정의해야 할 경우 코드가 늘어날 수 있다.
상태 패턴을 공부하면서
- 글만으로는 이해가 잘 되지 않아 예시를 찾고 직접 만들어가며 공부해 보니 도움이 됐다.
'디자인 패턴' 카테고리의 다른 글
[디자인 패턴] 방문자 패턴 (0) | 2024.02.19 |
---|---|
[디자인 패턴] 전략 패턴 (0) | 2023.08.17 |
[디자인 패턴] 싱글톤 패턴 (1) | 2023.07.13 |