TIL : 유니티 게임 시스템 구현

2025. 4. 29. 20:41[Unity] Game Programing

TIL: Unity Game Systems Implementation

1. 개요

오늘은 유니티 게임 프로젝트에서 핵심 시스템인 공격 시스템, 상호작용 시스템, 전투 상태 관리, 리소스 관리 등을 구현했습니다. 이러한 시스템들은 상태 머신 패턴과 이벤트 기반 통신을 활용하여 구조화되었으며, 코드의 재사용성과 확장성을 고려하여 설계되었습니다.

전체 시스템은 상태 머신 아키텍처를 중심으로, Entity-Component 디자인을 따르고 있으며, 각 기능들이 적절히 분리되어 있습니다. 이는 유지보수성을 향상시키고, 새로운 기능 추가가 용이하게 만들어줍니다.

2. 공격 시스템 (WeaponHandler & AttackHandler)

WeaponHandler 클래스

WeaponHandler는 무기의 공격 기능을 담당하는 컴포넌트로, 충돌 감지와 대미지 처리를 수행합니다.

  • 필드 및 프로퍼티: 공격 애니메이션 클립, 애니메이션 속도, 데미지 값 등을 관리합니다.
  • HashSet<IDamageable>: 이미 대미지를 입힌 대상을 추적하여 중복 대미지를 방지합니다.
  • SetIsAttacking: 공격 상태를 설정하고, 상태 변경 시 대상 목록을 초기화합니다.
  • OnTriggerEnter: 충돌 발생 시 IDamageable 인터페이스를 구현한 객체에 대미지를 적용합니다.
public class WeaponHandler : MonoBehaviour
{
    [field: SerializeField] public AnimationClip attackAnimation { get; private set; }
    [field: SerializeField] public float attackAnimationSpeed { get; private set; } = 1f;

    private HashSet<IDamageable> damageables = new HashSet<IDamageable>();
    [SerializeField] private float damage = 10f;
    private bool isAttacking;

    public void OnTriggerEnter(Collider other)
    {
        if(!isAttacking)
            return;
        
        if(other.gameObject == Owner)
            return;

        if(other.gameObject.TryGetComponent(out IDamageable damageable))
        {
            if(damageables.Add(damageable))
            {
                damageable.TakeDamage(damage);
            }
        }
    }

    public void SetIsAttacking(bool isAttacking)
    {
        if(this.isAttacking != isAttacking)
        {
            this.isAttacking = isAttacking;
            damageables.Clear();
        }
    }
}

AttackingState 클래스

AttackingState는 플레이어가 공격 중인 상태를 관리하는 상태 클래스입니다.

  • 애니메이션 진행 모니터링: 애니메이션의 정규화된 시간을 추적하여 특정 시점에 공격 판정을 활성화하고 비활성화합니다.
  • WeaponHandler 연동: 애니메이션의 특정 구간(40%~45%)에서 무기의 공격 판정을 활성화하고, 애니메이션 완료(80% 이상) 시 판정을 비활성화합니다.
  • 상태 전환: 공격 완료 후 CombatIdlingState로 자동 전환됩니다.

공격 상태의 핵심은 애니메이션과 공격 판정의 정밀한 동기화입니다. GetNormalizedTime 메서드를 사용하여 애니메이션의 진행 상태를 확인하고, 적절한 타이밍에 WeaponHandler의 SetIsAttacking 메서드를 호출하여 판정을 활성화/비활성화합니다.

3. 상호작용 시스템 (Interactable & InteractionHandler)

Interactable 추상 클래스

Interactable은 플레이어가 상호작용할 수 있는 모든 오브젝트의 기본 클래스입니다.

  • 상호작용 애니메이션: 각 상호작용 객체는 고유한 상호작용 애니메이션을 정의할 수 있습니다.
  • 애니메이션 속도: 상호작용 애니메이션의 재생 속도를 조절할 수 있습니다.
  • 추상 메서드 Interact: 자식 클래스에서 구현해야 하는 실제 상호작용 로직입니다.
public abstract class Interactable : MonoBehaviour
{
    [field: SerializeField] public AnimationClip interactAnimation { get; private set; }
    [field: SerializeField] public float interactAnimationSpeed { get; private set; } = 1f;

    public abstract void Interact(GameObject obj);
}

InteractionHandler 클래스

InteractionHandler는 플레이어의 상호작용 감지 및 관리를 담당하는 컴포넌트입니다.

  • HashSet과 List 자료구조: 중복 방지와 순서 관리를 동시에 처리합니다.
  • 거리 기반 정렬: 여러 상호작용 객체가 있을 때 가장 가까운 객체를 우선적으로 선택합니다.
  • 이벤트 기반 통신: onInputInteract 이벤트를 통해 입력 처리와 상태 변경을 분리합니다.
  • 비동기 처리: UniTask를 활용한 상호작용 후 객체 상태 확인을 비동기적으로 수행합니다.

InteractionHandler는 최적화를 위해 HashSet과 List를 함께 사용합니다. HashSet은 O(1) 시간 복잡도로 빠른 조회와 중복 방지를 제공하고, List는 순서가 있는 컬렉션으로 정렬과 인덱스 접근이 용이합니다. isDirty 플래그를 사용하여 필요한 경우에만 정렬을 수행하는 것도 중요한 최적화 기법입니다.

InterctingState 클래스

InterctingState는 플레이어가 상호작용 중인 상태를 관리하는 상태 클래스입니다.

  • 대상 방향으로 회전: 상호작용 객체를 향해 플레이어를 자연스럽게 회전시킵니다.
  • 애니메이션 재생: 적절한 상호작용 애니메이션을 재생합니다.
  • 애니메이션 진행 모니터링: 정규화된 시간을 통해 애니메이션 완료를 감지합니다.
  • 상호작용 실행: 애니메이션 완료 시 InteractionHandler의 OnInteract 메서드를 호출합니다.

4. 전투 상태 시스템 (CombatState)

AlivePlayerCombatState 클래스

AlivePlayerCombatState는 모든 전투 관련 상태의 기본 클래스입니다.

  • 이벤트 구독: 데미지 및 사망 이벤트에 대한 핸들러를 등록합니다.
  • 애니메이션 설정: SetAttackAnimation 메서드를 통해 공격 애니메이션을 동적으로 변경합니다.
  • 입력 처리: OnInputAttack 메서드를 통해 공격 입력을 처리합니다.
  • 비동기 애니메이션 체크: UniTask를 활용하여 데미지 애니메이션 완료를 감지합니다.

AlivePlayerCombatStateMachine 클래스

AlivePlayerCombatStateMachine은 전투 관련 상태들을 관리하는 상태 머신입니다.

  • 상태 초기화: CombatIdlingState, AttackingState, AimingState, DeadState 등의 상태를 생성하고 초기화합니다.
  • 상태 전환: ChangeState 메서드를 통해 현재 상태를 변경합니다.

전투 상태 머신 다이어그램

CombatIdlingState ────► AttackingState
       │                   │
       │                   │
       ▼                   ▼
   AimingState ─────────► DeadState
                

5. 리소스 관리 시스템 (ResourceHandler & ResourceStat)

ResourceStat 클래스

ResourceStat은 체력, 허기, 갈증 등의 게임 내 자원을 관리하는 클래스입니다.

  • 현재값과 최대값: Current와 Maximum 속성으로 자원의 현재 상태와 최대치를 관리합니다.
  • 이벤트 기반 알림: onResourceChanged 이벤트를 통해 자원 변화를 통지합니다.
  • 자원 조작 메서드: Add, Subtract, Modify, SetMaximum 등의 메서드로 자원을 조작합니다.
public class ResourceStat : Stat
{
    public float Current { get; private set; }
    public float Maximum;
    
    public ResourceStat(float baseValue) : base(baseValue)
    {
        Maximum = baseValue;
        Current = baseValue;
    } 

    public event Action<float, float> onResourceChanged; // (current, max)
    
    public void Add(float amount)
    {
        Current += amount;
        Current = Mathf.Clamp(Current, 0, Maximum);
        onResourceChanged?.Invoke(Current, Maximum);
    }
    
    public void Subtract(float amount)
    {
        Current -= amount;
        Current = Mathf.Clamp(Current, 0, Maximum);
        onResourceChanged?.Invoke(Current, Maximum);
    }
}

ResourceHandler 클래스

ResourceHandler는 게임 내 자원 객체(아이템, 수집 가능한 리소스 등)에 대한 처리를 담당합니다.

  • IDamageable 인터페이스 구현: 데미지를 받을 수 있는 객체로서의 기능을 제공합니다.
  • 시각적 피드백: DOTween을 활용한 피격 효과를 제공합니다.
public class ResourceHandler : MonoBehaviour, IDamageable
{
    public ResourceStat Hp { get; private set; } = new ResourceStat(100f);

    public void TakeDamage(float damage)
    {
        Hp.Subtract(damage);
        transform.DOScale(0.8f, 0.1f).OnComplete(() => transform.DOScale(1f, 0.1f));
    }
}

6. 피해 처리 인터페이스 (IDamageable)

IDamageable 인터페이스는 데미지를 받을 수 있는 모든 객체에 대한 일관된 인터페이스를 제공합니다.

  • 단일 메서드: TakeDamage 메서드를 통해 데미지 처리를 표준화합니다.
  • 구현 클래스: AlivePlayer, ResourceHandler 등 다양한 클래스에서 구현됩니다.
public interface IDamageable
{
    void TakeDamage(float damage); 
}

인터페이스를 통한 구현은 다양한 객체 유형(플레이어, 적, 리소스 등)에 일관된 피해 처리 메커니즘을 적용할 수 있게 해줍니다. 이는 코드의 재사용성을 높이고, 새로운 피해 가능 객체 타입 추가를 용이하게 합니다.

7. 상태 머신 아키텍처

이 프로젝트는 상태 머신 패턴을 적극적으로 활용하여 캐릭터의 다양한 행동과 상태를 관리하고 있습니다.

계층적 상태 머신 구조

  • AlivePlayerStateMachine: 최상위 상태 머신으로, 이동 및 전투 상태 머신을 관리합니다.
  • AlivePlayerMovementStateMachine: 이동 관련 상태(Idling, Running, Sprinting 등)를 관리합니다.
  • AlivePlayerCombatStateMachine: 전투 관련 상태(CombatIdling, Attacking, Aiming 등)를 관리합니다.

상태 클래스 계층 구조

  • IState: 모든 상태가 구현해야 하는 기본 인터페이스입니다.
  • AlivePlayerState: 모든 플레이어 상태의 공통 기능을 제공하는 기본 클래스입니다.
  • AlivePlayerMovementState: 이동 관련 상태의 공통 기능을 제공합니다.
  • AlivePlayerCombatState: 전투 관련 상태의 공통 기능을 제공합니다.
  • 구체적 상태 클래스: IdlingState, RunningState, AttackingState 등의 특화된 상태 클래스입니다.

상태 머신 계층 구조

AlivePlayerStateMachine
├── AlivePlayerMovementStateMachine
│   ├── IdlingState
│   ├── RunningState
│   ├── SprintingState
│   ├── InterctingState
│   └── DeadState
└── AlivePlayerCombatStateMachine
    ├── CombatIdlingState
    ├── AttackingState
    ├── AimingState
    └── DeadState
                

이러한 상태 머신 아키텍처는 복잡한 캐릭터 동작을 관리하기 위한 효과적인 방법을 제공합니다. 각 상태는 자신만의 책임을 가지며, 상태 간 전환은 명확한 규칙에 따라 이루어집니다. 이는 코드의 모듈화와 재사용성을 향상시키고, 새로운 상태 추가가 용이하게 합니다.

8. 애니메이션 제어 시스템

게임 내 캐릭터와 객체의 애니메이션은 Unity의 Animator 컴포넌트와 AnimatorOverrideController를 통해 제어됩니다.

AnimatorOverrideController 활용

  • 동적 애니메이션 교체: 기본 애니메이션을 상황에 맞게 교체할 수 있습니다.
  • 상황별 재생 속도 조절: 애니메이션 클립마다 다른 재생 속도를 적용할 수 있습니다.

애니메이션 진행 모니터링

  • GetNormalizedTime 메서드: 애니메이션의 진행 상태를 0~1 사이의 값으로 반환합니다.
  • 특정 타이밍 이벤트: 애니메이션의 특정 지점에서 이벤트를 발생시킬 수 있습니다.
protected float GetNormalizedTime(Animator animator, string tag, int layerIndex = 0)
{
    AnimatorStateInfo currentInfo = animator.GetCurrentAnimatorStateInfo(layerIndex);
    AnimatorStateInfo nextInfo = animator.GetNextAnimatorStateInfo(layerIndex);

    if (animator.IsInTransition(layerIndex) && nextInfo.IsTag(tag))
        return nextInfo.normalizedTime;
    else if (!animator.IsInTransition(layerIndex) && currentInfo.IsTag(tag))
        return currentInfo.normalizedTime;
    else
        return 0f;
}

9. 최적화 기법

자료구조 최적화

  • HashSet 활용: 중복 방지와 빠른 조회를 위해 HashSet을 사용합니다.
  • HashSet과 List 병행: InteractionHandler에서는 두 자료구조를 함께 사용하여 각각의 장점을 활용합니다.
  • Dirty 플래그: 필요한 경우에만 비용이 많이 드는 연산(정렬 등)을 수행합니다.

비동기 처리

  • UniTask 활용: 애니메이션 체크, 대미지 효과 등을 비동기적으로 처리합니다.
  • 메인 스레드 부하 감소: 비동기 처리를 통해 프레임 드롭을 방지합니다.
async UniTaskVoid DamageEffect()
{
    float elapsedTime = 0f;
    while(elapsedTime < 1.5f)
    {
        stateMachine.ReusableData.MovementSpeedPercentage = 0.7f;
        elapsedTime += Time.deltaTime;
        await UniTask.Yield();
    }

    stateMachine.ReusableData.MovementSpeedPercentage = 1f;
}

10. 이벤트 기반 통신

시스템 간의 통신은 주로 C# 이벤트와 Action 델리게이트를 통해 이루어집니다.

주요 이벤트

  • AlivePlayer.onDamaged, onDead: 피해 및 사망 상태를 알립니다.
  • InteractionHandler.onInputInteract: 상호작용 입력 발생을 알립니다.
  • ResourceStat.onResourceChanged: 자원 변경을 알립니다.

이벤트 기반 설계의 이점

  • 느슨한 결합: 컴포넌트 간의 직접적인 의존성을 줄입니다.
  • 확장성: 새로운 이벤트 구독자를 쉽게 추가할 수 있습니다.
  • 코드 모듈화: 관련 기능을 논리적으로 분리할 수 있습니다.

결론

오늘 구현한 시스템들은 상태 머신 아키텍처를 중심으로 서로 유기적으로 연결되어 있습니다. 이 시스템들은 게임 내에서 플레이어의 움직임, 전투, 상호작용 등 핵심 메커니즘을 제공합니다. 특히 인터페이스와 추상 클래스를 활용한 설계는 코드의 재사용성과 확장성을 크게 향상시키고 있습니다.

다양한 최적화 기법과 비동기 처리를 통해 성능을 개선했으며, 이벤트 기반 통신으로 시스템 간의 결합도를 낮춰 유지보수성을 향상시켰습니다. 이러한 설계는 향후 게임 기능 확장 시에도 안정적인 기반을 제공할 것입니다.

앞으로 더 많은 기능을 추가함에 있어서도 이러한 구조적 설계 원칙을 유지하면서 발전시켜 나갈 계획입니다.