728x90
반응형
유니티에서 캐릭터의 이동, 공격, 피격, 죽음 같은 행동을 관리할 때
조건문(if, switch)으로 상태를 구분하다 보면 코드가 점점 복잡해집니다.
이럴 때 유용한 설계 방법이 바로 상태패턴(State Pattern) 입니다.
이번 글에서는 상태패턴을 이용해 2D 캐릭터가 움직이고, 공격하고, 맞고, 죽는 간단한 예제를 만들어봅니다.
🧩 상태패턴이란?
상태패턴은 “객체의 상태에 따라 행동이 달라지는 것”을 각 상태를 클래스로 분리해서 관리하는 방법이에요.
보통 이렇게 바뀌죠 👇
Before (조건문 기반) | After (상태패턴 기반) |
if문이 점점 길어짐 | 각 상태별 클래스로 분리 |
상태 전환 시 코드 수정 많음 | 상태 클래스만 수정하면 됨 |
테스트 어려움 | 독립적 테스트 가능 |
🚗 예제 시나리오
이번 예제에서 캐릭터는 다음 5가지 상태를 가집니다.
상태설명
Idle | 가만히 서 있는 상태 |
Walk | 이동 중인 상태 |
Attack | 공격 중 상태 |
Hit | 피격되어 잠시 멈추는 상태 |
Dead | 체력이 0이 되어 죽은 상태 |
🧱 프로젝트 구조
Scripts/
├── Character.cs
├── ICharacterState.cs
├── IdleState.cs
├── WalkState.cs
├── AttackState.cs
├── HitState.cs
└── DeadState.cs
🧩 1. 상태 인터페이스 정의
모든 상태가 공통으로 가져야 하는 구조입니다.
public interface ICharacterState
{
void OnEnter();
void OnExit();
void Update();
}
각 상태는 “들어올 때”, “나갈 때”, “프레임마다 실행할 때” 세 가지 함수를 가집니다.
🧍 2. Character 클래스 만들기
캐릭터는 현재 상태를 가지고 있으며, 입력을 받아서 상태를 바꿉니다.
using UnityEngine;
public class Character : MonoBehaviour
{
private ICharacterState _currentState;
// 상태 객체
private IdleState _idleState;
private WalkState _walkState;
private AttackState _attackState;
private HitState _hitState;
private DeadState _deadState;
// 이동 관련
[SerializeField] private float _moveSpeed = 3f;
private Vector2 _moveDir;
// 체력 및 공격
[SerializeField] private int _health = 100;
private bool _canAttack = true;
private float _attackCooldown = 1f;
// 컴포넌트
private Animator _animator;
private SpriteRenderer _renderer;
private void Awake()
{
_animator = GetComponent<Animator>();
_renderer = GetComponent<SpriteRenderer>();
_idleState = new IdleState(this);
_walkState = new WalkState(this);
_attackState = new AttackState(this);
_hitState = new HitState(this);
_deadState = new DeadState(this);
}
private void Start()
{
ChangeState(_idleState);
}
private void Update()
{
if (_currentState == _deadState)
return;
HandleInput();
_currentState.Update();
}
private void HandleInput()
{
// 공격
if (Input.GetKeyDown(KeyCode.Space) && _canAttack)
{
ChangeState(_attackState);
return;
}
// 이동
float x = Input.GetAxisRaw("Horizontal");
float y = Input.GetAxisRaw("Vertical");
_moveDir = new Vector2(x, y).normalized;
if (_moveDir != Vector2.zero)
ChangeState(_walkState);
else
ChangeState(_idleState);
}
public void Move(Vector2 dir)
{
transform.Translate(dir * _moveSpeed * Time.deltaTime);
if (dir.x != 0)
_renderer.flipX = dir.x < 0;
}
public void ChangeState(ICharacterState newState)
{
if (_currentState == newState)
return;
_currentState?.OnExit();
_currentState = newState;
_currentState.OnEnter();
}
public void TakeDamage(int damage)
{
if (_currentState == _deadState)
return;
_health -= damage;
if (_health <= 0)
ChangeState(_deadState);
else
ChangeState(_hitState);
}
public void SetAttackCooldown()
{
_canAttack = false;
Invoke(nameof(ResetAttackCooldown), _attackCooldown);
}
private void ResetAttackCooldown() => _canAttack = true;
public Vector2 GetMoveDir() => _moveDir;
public Animator Animator => _animator;
}
🧠 3. 각 상태 클래스 만들기
🟢 IdleState
using UnityEngine;
public class IdleState : ICharacterState
{
private readonly Character _character;
public IdleState(Character character)
{
_character = character;
}
public void OnEnter()
{
Debug.Log("대기 상태 진입");
_character.Animator.Play("Idle");
}
public void OnExit() { }
public void Update() { }
}
🟡 WalkState
using UnityEngine;
public class WalkState : ICharacterState
{
private readonly Character _character;
public WalkState(Character character)
{
_character = character;
}
public void OnEnter()
{
Debug.Log("걷기 상태 진입");
_character.Animator.Play("Walk");
}
public void OnExit() { }
public void Update()
{
Vector2 moveDir = _character.GetMoveDir();
_character.Move(moveDir);
}
}
🔴 AttackState (공격 쿨타임 포함)
using UnityEngine;
public class AttackState : ICharacterState
{
private readonly Character _character;
private float _attackDuration = 0.5f;
private float _timer;
public AttackState(Character character)
{
_character = character;
}
public void OnEnter()
{
Debug.Log("공격 상태 진입");
_character.Animator.Play("Attack");
_character.SetAttackCooldown();
_timer = _attackDuration;
}
public void OnExit() { }
public void Update()
{
_timer -= Time.deltaTime;
if (_timer <= 0)
{
_character.ChangeState(new IdleState(_character));
}
}
}
⚡ HitState (피격 상태)
using UnityEngine;
public class HitState : ICharacterState
{
private readonly Character _character;
private float _hitDuration = 0.3f;
private float _timer;
public HitState(Character character)
{
_character = character;
}
public void OnEnter()
{
Debug.Log("피격 상태 진입");
_character.Animator.Play("Hit");
_timer = _hitDuration;
}
public void OnExit() { }
public void Update()
{
_timer -= Time.deltaTime;
if (_timer <= 0)
{
_character.ChangeState(new IdleState(_character));
}
}
}
⚫ DeadState (죽음 상태)
using UnityEngine;
public class DeadState : ICharacterState
{
private readonly Character _character;
public DeadState(Character character)
{
_character = character;
}
public void OnEnter()
{
Debug.Log("죽음 상태 진입");
_character.Animator.Play("Dead");
}
public void OnExit() { }
public void Update() { }
}
🧩 상태패턴의 장점
✅ 각 상태가 독립적이라 수정이 쉽습니다.
✅ if문 없이 상태 전환이 깔끔합니다.
✅ 상태 추가(예: 점프, 스킬 등)가 간단합니다.
✨ 마무리
상태패턴은 복잡한 행동을 가진 캐릭터를 관리할 때 정말 강력합니다.
특히 유니티 같은 프레임 기반 구조에서는 “현재 상태가 어떤 로직을 실행할지”를 분리해두면 디버깅도, 확장도 훨씬 쉬워집니다.
728x90
반응형
'개발 > Unity' 카테고리의 다른 글
Unity에서 커맨드(Command) 패턴 활용하기 (0) | 2025.10.11 |
---|---|
Unity에서 추상 클래스 기반 Singleton 패턴 구현하기 (0) | 2025.10.06 |