본문 바로가기
개발/Unity

Unity에서 상태패턴으로 캐릭터 움직임과 공격 구현하기

by DinoDev 2025. 10. 7.
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
반응형