핵심 디자인 패턴

김혁·2025년 7월 26일

챌린지

목록 보기
1/14

생성 패턴

: 객체 생성 메커니즘을 다루는 패턴

싱글톤 패턴 (Singleton)

: 클래스의 인스턴스가 단 하나만 존재하도록 보장하는 디자인 패턴. 주로 전역 상태를 관리해야 하는 경우에 사용되며, 클래스 내부에서 객체 생성을 통제하여 외부에서new를 통해 여러 개를 생성하지 못하게 한다.

싱글톤 패턴이 필요한 상황

: 게임 전체에서 하나만 존재해야 하고, 어디서든 접근 가능해야 하는 클래스

  • 게임 매니저 : 게임 상태(시작, 종료, 일시정지 등)을 관리
  • UI 매니저 : 전체 UI 요소를 통합 관리
  • 오디오 매니저 : BGM, 효과음 등을 전역에서 재생/중지
  • 입력 매니저 : 플레이어 입력 처리
  • 데이터 매니저 : 저장/로드, 설정 관리

장점

  1. 글로벌한 접근성 : 어디서든 ClassName::Get() 등을 통해 접근 가능
  2. 인스턴스 하나만 유지 : 메모리 낭비 방지, 상태 일관성 유지
  3. 초기화 타이밍 제어 : 직접 제어 가능하거나, 필요 시 생성됨

단점

  1. 테스트 어려움 : 전역 상태가 테스트를 방해하고, 상태 초기화가 힘듦.
  2. 의존성 증가 : 많은 클래스가 싱글톤에 의존하면 강한 결합이 발생.
  3. 멀티스레드 환경에서 주의 필요 : 동기화 처리가 없으면 여러 개의 인스턴스 생성 가능

언리얼에서의 예시

: 언리얼 엔진 내부에서 싱글톤으로 관리하고 있는 클래스가 많기 때문에 이를 이용해서 보통 싱글톤을 구현한다. 일반 C++ 방식은 GC 대상으로 될 수 있기 때문에 주의해야 한다.
: UGameInstance는 게임 전체에서 유지되는 객체이기 때문에 전역 데이터 관리나 매니저에 적합하다. 또는 Subsystem 기반의 싱글톤 스타일도 사용된다.

#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "GameManager.generated.h"

UCLASS()
class MYPROJECT_API UGameManager : public UGameInstance
{
    GENERATED_BODY()

public:
    UFUNCTION(BlueprintCallable)
    void StartGame();

    UFUNCTION(BlueprintCallable)
    void EndGame();
};

// 사용법
if (UGameManager* GameManager = Cast<UGameManager>(GetGameInstance()))
{
    GameManager->StartGame();
}

팩토리 패턴 (Factory)

: 객체 생성 로직을 숨기고, 객체 생성을 하나의 공장(Factory) 객체에 위임하는 디자인 패턴이다. new 대신 Create 메서드를 통해 객체를 생성하는데, 객체 생성의 책임과 로직을 분리하여, 유지보수성과 확장성을 향상시킬 수 있다.

팩토리 패턴이 필요한 상황

  • 다양한 몬스터, NPC 생성 : Zombie, Orc, Dragon 등 서브 클래스에 따라 생성 로직 분리
  • 무기, 아이템 생성 : 무기 종류에 따라 다른 기능이나 모델을 가진 인스턴스 생성
  • 이펙트, 스킬 생성 : 스킬 ID 또는 타입으로 특정 이펙트 오브젝트 생성
  • 객체 풀링과 연동 : 팩토리 패턴을 통해 재사용 가능한 객체를 관리

장점

  1. 생성 로직 분리 : 클라이언트 코드가 new를 직접 호출하지 않음
  2. 유지보수 쉬움 : 클래스가 추가되더라도 팩토리 내부만 수정하면 됨
  3. 객체 생성 조건을 통합 관리 : 초기화 로직, 복잡한 로직 등을 감출 수 있음

단점

  1. 클래스 수 증가 : 팩토리 클래스, 서브 클래스 등으로 코드 양이 증가해서 복잡해질 수 있음
  2. 과도한 추상화 위험 : 단순한 경우에도 무거운 구조로 만들 수 있음
  3. 오버헤드 증가 : 성능이 극도로 중요한 경우, 직접 생성보다 오버헤드 증가로 인해 성능 하락

언리얼에서의 예시

: 여러 종류의 Enemy를 ID나 Enum으로 생성

// AEnemyBase 기반으로 Orc, Zombie 클래스 생성

UENUM(BlueprintType)
enum class EEnemyType : uint8
{
    Orc,
    Zombie
};

class EnemyFactory
{
public:
    static AEnemyBase* SpawnEnemy(UWorld* World, EEnemyType Type, FVector Location)
    {
        if (!World) return nullptr;

        TSubclassOf<AEnemyBase> EnemyClass = nullptr;

        switch (Type)
        {
            case EEnemyType::Orc:
                EnemyClass = AEnemyOrc::StaticClass();
                break;

            case EEnemyType::Zombie:
                EnemyClass = AEnemyZombie::StaticClass();
                break;

            default:
                break;
        }

        if (EnemyClass)
        {
            return World->SpawnActor<AEnemyBase>(EnemyClass, Location, FRotator::ZeroRotator);
        }

        return nullptr;
    }
};

// 사용법
AEnemyBase* SpawnedEnemy = EnemyFactory::SpawnEnemy(GetWorld(), EEnemyType::Orc, FVector(0.0f, 0.0f, 100.0f));

빌더 패턴 (Builder)

: 복잡한 객체의 생성 과정을 단계별로 분리하여, 유연하고 가독성 높은 방식으로 객체를 구성할 수 있도록 하는 디자인 패턴. 생성 로직을 '구성 과정'과 '최종 결과'로 분리하여 객체를 완성하는 방식이다.

빌더 패턴이 필요한 상황

  • 캐릭터 커스터마이징 : 스탯, 외형, 장비 등을 단계별로 설정하여 생성
  • 아이템 생성 : 속성, 이펙트, 등급 등을 유연하게 조합
  • UI 위젯 생성 : 텍스트, 색상, 이벤트 등을 동적으로 조립
  • 퀘스트/스킬 생성 : 이름, 목표, 보상 등을 설정
  • 시네마틱 구성 : 단계별 이벤트를 쌓아서 하나의 시퀀스 빌드

장점

  1. 복잡한 객체 생성 논리 분리 가능
  2. 코드 가독성 향상
  3. 객체 조합 다양화
  4. 불변 객체 생성에 적합

단점

  1. 클래스 수 증가
  2. 간단한 객체에 사용하면 복잡
  3. 설계 구조 파악이 어려움

언리얼에서의 예시

: 무기를 속성별로 커스터마이징해서 생성

USTRUCT(BlueprintType)
struct FWeaponData
{
    GENERATED_BODY()

    UPROPERTY(BlueprintReadWrite)
    FString Name;

    UPROPERTY(BlueprintReadWrite)
    int32 Damage;

    UPROPERTY(BlueprintReadWrite)
    float Range;

    UPROPERTY(BlueprintReadWrite)
    FString ElementType;
};

class WeaponBuilder
{
private:
    FWeaponData WeaponData;

public:
    WeaponBuilder& SetName(const FString& InName)
    {
        WeaponData.Name = InName;
        return *this;
    }

    WeaponBuilder& SetDamage(int32 InDamage)
    {
        WeaponData.Damage = InDamage;
        return *this;
    }

    WeaponBuilder& SetRange(float InRange)
    {
        WeaponData.Range = InRange;
        return *this;
    }

    WeaponBuilder& SetElement(const FString& Element)
    {
        WeaponData.ElementType = Element;
        return *this;
    }

    FWeaponData Build()
    {
        return WeaponData;
    }
};

// 사용법
WeaponBuilder Builder;
FWeaponData FireSword = Builder
    .SetName(TEXT("Flame Sword"))
    .SetDamage(100)
    .SetRange(150.f)
    .SetElement(TEXT("Fire"))
    .Build();

구조 패턴

: 클래스나 객체를 조합하는 패턴

어댑터 패턴 (Adapter)

: 호환되지 않는 인터페이스를 가진 클래스들을 연결해주는 디자인 패턴. 기존 클래스의 인터페이스를 변경하지 않고, 클라이언트가 요구하는 새로운 인터페이스에 맞춰주는 중간 계층을 제공한다.

어댑터 패턴이 필요한 상황

  • 외부 라이브러리 사용 : 외부 시스템의 API와 게임 로직이 맞지 않을 때
  • 이전 버전 코드 재사용 : 기존 시스템을 수정하지 않고 새 시스템과 연동하고 싶을 댸
  • 멀티 플랫폼 대응 : 플랫폼별 다른 API를 하나의 공통 인터페이스로 감쌀 때
  • 다양한 AI/NPC 인터페이스 통합 : 서로 다른 방식의 AI를 하나의 시스템으로 통합 제어할 때

장점

  1. 기존 코드 변경 없이 재활용 가능
  2. 외부 라이브러리와의 통합 용이
  3. 인터페이스 통일로 코드 일관성 유지

단점

  1. 클래스 수 증가
  2. 과도하게 사용하면 구조 복잡 : 단순한 데이터 변환만 필요한 경우에는 구조만 복잡해짐
  3. 오버헤드 증가 : 성능이 극도로 중요한 경우, 오버헤드 증가로 인해 성능 하락

언리얼에서의 예시

: 프로젝트에서는 IAttackable 인터페이스를 사용하고 있는데, 외부에서 가져온 AOldEnemy 클래스는 이 인터페이스를 구현하지 않음. 따라서 AOldEnemyIAttackable 처럼 사용할 수 있도록 만들어서 코드 일관성을 유지시킨다.

  • IAttackable 인터페이스
UINTERFACE(MinimalAPI)
class UAttackable : public UInterface
{
    GENERATED_BODY()
};

class IAttackable
{
    GENERATED_BODY()

public:
    virtual void TakeDamage(int32 Amount) = 0;
};
  • 외부 클래스
UCLASS()
class AOldEnemy : public AActor
{
    GENERATED_BODY()

public:
    void ReceiveHit(int32 Damage) // 이름도 다름
    {
        UE_LOG(LogTemp, Log, TEXT("OldEnemy took %d damage"), Damage);
    }
};
  • 어댑터 클래스
UCLASS()
class AOldEnemyAdapter : public AActor, public IAttackable
{
    GENERATED_BODY()

private:
    AOldEnemy* Adaptee;

public:
    void Initialize(AOldEnemy* InOldEnemy)
    {
        Adaptee = InOldEnemy;
    }

    virtual void TakeDamage(int32 Amount) override
    {
        if (Adaptee)
        {
            Adaptee->ReceiveHit(Amount); // 이름 다른 메서드 호출
        }
    }
};

// 사용법
AOldEnemy* OldEnemy = GetWorld()->SpawnActor<AOldEnemy>();
AOldEnemyAdapter* Adapter = GetWorld()->SpawnActor<AOldEnemyAdapter>();
Adapter->Initialize(OldEnemy);

if (IAttackable* Target = Cast<IAttackable>(Adapter))
{
    Target->TakeDamage(50);
}

데코레이터 패턴 (Decorator)

: 객체에 동적으로 새로운 기능을 추가하거나 확장할 수 있게 해주는 디자인 패턴. 객체의 기능을 직접 수정하지 않고, 동적으로 기능을 덧붙이는 방식으로 동작하며, 합성으로 기능을 확장한다. 원래 클래스와 같은 인터페이스를 구현하는 래퍼 객체를 사용한다.

데코레이터 패턴이 필요한 상황

  • 버프/디버프 시스템 : 공격력 증가, 체력 회복, 상태 이상 등 효과를 조합
  • 무기 업그레이드 : 총에 스코프, 소음기, 탄창 등 부착물을 동적으로 부여
  • 이펙트 추가 : 기본 공격에 불속성, 얼음속성 같은 시각 효과 부여
  • 행동 보강 : AI 행동이나 플레이어 동작에 조건부 동작 추가

장점

  1. 기능을 유연하게 추가 가능 : 상속 없이 런타임에 기능 추가 가능
  2. 기존 클래스 변경 없이 확장 가능
  3. 중첩 조합 가능 : 여러 데코레이터를 조합해 다양한 기능 구현 가능

단점

  1. 객체 수 증가 : 데코레이터가 많아질수록 객체 계층 복잡해짐
  2. 디버깅 어려움 : 여러 데코레이터가 감싸고 있어서 흐름 추적 어려움
  3. 과도한 사용시 오히려 구조 불명확

언리얼에서의 예시

: 기본 공격 시스템이 있는데, 여기에 '불속성', '중독 효과' 같은 데코레이터를 추가하여 공격에 효과를 확장하고 싶다.

  1. IAttack 인터페이스
UINTERFACE()
class UAttack : public UInterface
{
    GENERATED_BODY()
};

class IAttack
{
    GENERATED_BODY()

public:
    virtual void PerformAttack() = 0;
};
  1. 기본 공격 클래스
UCLASS()
class ABasicAttack : public AActor, public IAttack
{
    GENERATED_BODY()

public:
    virtual void PerformAttack() override
    {
        UE_LOG(LogTemp, Log, TEXT("기본 공격 실행!"));
    }
};
  1. 데코레이터 클래스들
UCLASS()
class AAttackDecorator : public AActor, public IAttack
{
    GENERATED_BODY()

protected:
    IAttack* WrappedAttack;

public:
    void Initialize(IAttack* InAttack)
    {
        WrappedAttack = InAttack;
    }

    virtual void PerformAttack() override
    {
        if (WrappedAttack)
        {
            WrappedAttack->PerformAttack();
        }
    }
};

UCLASS()
class AFireAttackDecorator : public AAttackDecorator
{
    GENERATED_BODY()

public:
    virtual void PerformAttack() override
    {
        Super::PerformAttack();
        UE_LOG(LogTemp, Log, TEXT("🔥 불속성 데미지 추가!"));
    }
};

UCLASS()
class APoisonAttackDecorator : public AAttackDecorator
{
    GENERATED_BODY()

public:
    virtual void PerformAttack() override
    {
        Super::PerformAttack();
        UE_LOG(LogTemp, Log, TEXT("☠️ 중독 효과 적용!"));
    }
};
  1. 사용 예시
ABasicAttack* Basic = GetWorld()->SpawnActor<ABasicAttack>();
AFireAttackDecorator* Fire = GetWorld()->SpawnActor<AFireAttackDecorator>();
Fire->Initialize(Basic);

APoisonAttackDecorator* Poison = GetWorld()->SpawnActor<APoisonAttackDecorator>();
Poison->Initialize(Fire);

IAttack* FinalAttack = Cast<IAttack>(Poison);
FinalAttack->PerformAttack();
// 기본 공격 실행!
//🔥 불속성 데미지 추가!
// ☠️ 중독 효과 적용!

퍼사드 패턴 (Facade)

: 복잡한 서브시스템들을 하나의 간단한 인터페이스로 감싸서 외부에서 더 쉽게 사용할 수 있도록 해주는 디자인 패턴. 클라이언트가 내부 구조를 몰라도 작업을 수행할 수 있도록 도와준다.

퍼사드 패턴이 필요한 상황

  • 게임 초기화 : 설정, 맵 로딩, 플레이어 스폰 등을 하나의 매니저로 처리
  • UI 시스템 제어 : 여러 위젯(인벤토리, 퀘스트, 미니맵 등)을 하나의 UI 매니저로 묶기
  • 전투 시스템 : 애니메이션, 사운드, 데미지 처리 등 여러 컴포넌트를 하나의 클래스로 통합

장점

  1. 단순한 인터페이스 : 복잡한 내부 구조를 몰라도 사용 가능
  2. 의존성 감소 : 클라이언트는 서브 시스템에 직접 접근할 필요가 없음
  3. 유지보수 쉬움 : 내부 로직 바뀌어도 퍼사드만 잘 유지하면 됨

단점

  1. 너무 많은 기능 포함 시 비대해질 수 있음
  2. 서브시스템이 너무 유동적이면 퍼사드 클래스도 변경이 잦아짐

언리얼에서의 예시

: 전투 시스템을 위한 퍼사드 클래스 생성

  1. 여러 하위 컴포넌트 클래스
class UAnimHandler
{
public:
	void PlayAttackMontage();
};

class USoundHandler
{
public:
    void PlayAttackSound();
};

class UDamageHandler
{
public:
    void ApplyDamage(AActor* Target, float Amount);
};
  1. 퍼사드 클래스 생성 : UCombatSystem
UCLASS()
class UCombatSystem : public UObject
{
    GENERATED_BODY()

private:
    UPROPERTY()
    UAnimHandler* AnimHandler;

    UPROPERTY()
    USoundHandler* SoundHandler;

    UPROPERTY()
    UDamageHandler* DamageHandler;

public:
    void Init()
    {
        AnimHandler = NewObject<UAnimHandler>();
        SoundHandler = NewObject<USoundHandler>();
        DamageHandler = NewObject<UDamageHandler>();
    }

    void PerformAttack(AActor* Target)
    {
        AnimHandler->PlayAttackMontage();
        SoundHandler->PlayAttackSound();
        DamageHandler->ApplyDamage(Target, 50.0f);
    }
};
  1. 사용 예시
UCombatSystem* CombatSystem = NewObject<UCombatSystem>();
CombatSystem->Init();
CombatSystem->PerformAttack(EnemyActor);

행동 패턴

: 객체 간 상호작용과 책임 분배 패턴

옵저버 패턴 (Observer)

: 어떤 객체의 상태 변화가 있을 때, 그 객체에 의존하는 다른 객체들에게 자동으로 알림이 가도록 하는 디자인 패턴. 이벤트 시스템이나 상태 알림에 핵심적인 방법이다.

옵저버 패턴이 필요한 상황

  • 체력 시스템 : 체력이 변할 때 UI 체력바, 사운드, 이펙트 등이 반응
  • 퀘스트 시스템 : 퀘스트 조건 달성 시 관련 시스템에 자동으로 알림 전파
  • 이벤트 시스템 : 글로벌 이벤트(날씨 변화, 시간 경과) 전파
  • AI 행동 트리 : 타겟이 사라질 경우 여러 노드에 알림

장점

  1. 낮은 결합도 : Subject는 Observer의 구체 구현을 몰라도 됨
  2. 동적 확장성 : Observer를 동적으로 추가/제거 가능
  3. 코드 재사용성 증가 : 다양한 Observer가 같은 이벤트를 재사용 가능

단점

  1. 디버깅 어려움 : 누가 어디서 알림을 받는지 추적하기 힘듦.
  2. 성능 저하 : 알림 대상이 많을수록 호출 부담 증가

언리얼에서의 예시

: 플레이어의 체력이 변하면 체력 UI가 자동으로 업데이트되도록 한다.

  1. 옵저버 인터페이스 정의
UINTERFACE()
class UHealthObserver : public UInterface
{
	GENERATED_BODY()
};

class IHealthObserver
{
	GENERATED_BODY()
    
public:
	virtual void OnHealthChanged(float NewHealth) = 0;
};
  1. Observer 클래스
UCLASS()
class UHealthWidget : public UUserWidget, public IHealthObserver
{
    GENERATED_BODY()

public:
    virtual void OnHealthChanged_Implementation(float NewHealth) override
    {
        // 체력 바 업데이트
        UE_LOG(LogTemp, Warning, TEXT("Health updated: %f"), NewHealth);
    }
};
  1. Subject 클래스
UCLASS()
class AMyCharacter : public ACharacter
{
    GENERATED_BODY()

private:
    float Health = 100.0f;

    UPROPERTY()
    TArray<TScriptInterface<IHealthObserver>> Observers;

public:
    void RegisterObserver(const TScriptInterface<IHealthObserver>& Observer)
    {
        Observers.Add(Observer);
    }

    void UnregisterObserver(const TScriptInterface<IHealthObserver>& Observer)
    {
        Observers.Remove(Observer);
    }

    void SetHealth(float NewHealth)
    {
        Health = NewHealth;
        NotifyHealthChanged();
    }

    void NotifyHealthChanged()
    {
        for (auto& Observer : Observers)
        {
            if (Observer)
            {
            	// UINTERFACE 기반 인터페이스 사용 시 자동 생성되는 Execute_ 접두어 함수
                // 내부에서 안전하게 캐스팅 및 널 체크
                Observer->Execute_OnHealthChanged(Observer.GetObject(), Health);
            }
        }
    }
};
  1. 사용 예시
void AMyHUD::BeginPlay()
{
    Super::BeginPlay();

    AMyCharacter* Player = Cast<AMyCharacter>(UGameplayStatics::GetPlayerCharacter(this, 0));
    UHealthWidget* HealthUI = CreateWidget<UHealthWidget>(GetWorld(), HealthWidgetClass);

    if (Player && HealthUI)
    {
        Player->RegisterObserver(HealthUI);
        HealthUI->AddToViewport();
    }
}

커맨드 패턴 (Command)

: 요청을 객체로 캡슐화하여 사용자가 요청을 매개변수화하거나, 요청을 큐에 저장하거나 실행 취소를 가능하게 만드는 디자인 패턴.

커맨드 패턴이 필요한 상황

  • 키 바인딩을 동적으로 변경하고 싶을 때
  • Undo / Redo 기능이 필요할 때
  • 매크로 시스템 (명령 모음 저장 후 재실행)
  • 스킬 시퀀스 실행, AI 명령 큐

장점

  1. 요청 캡슐화 : 명령을 독립적 객체로 만들어 재사용성, 확장성 향상
  2. Undo/Redo 구현 용이
  3. 실행 명령 큐 관리 가능
  4. Invoker와 Receiver의 강한 결합 제거

단점

  1. 클래스 수 증가 : 명령마다 별도의 클래스를 만들어야 함
  2. 과도한 설계 가능성 : 단순한 경우에는 과한 구조가 될 수 있음

언리얼에서의 예시

: 키 입력으로 캐릭터에게 명령을 내림

  1. 명령 인터페이스
class ICommand
{
public:
	virtual ~ICommand() {}
    virtual void Execute() = 0;
};
  1. 명령 클래스(이동 명령, 공격 명령) 생성
class MoveCommand : public ICommand
{
private:
    AActor* Target;
    FVector Direction;

public:
    MoveCommand(AActor* InTarget, FVector InDirection)
        : Target(InTarget), Direction(InDirection) {}

    virtual void Execute() override
    {
        if (Target)
        {
            Target->AddActorWorldOffset(Direction);
        }
    }
};

class AttackCommand : public ICommand
{
private:
    AActor* Attacker;

public:
    AttackCommand(AActor* InAttacker) : Attacker(InAttacker) {}

    virtual void Execute() override
    {
        if (Attacker)
        {
            UE_LOG(LogTemp, Warning, TEXT("%s 공격 실행!"), *Attacker->GetName());
            // 실제 공격 로직 호출 가능
        }
    }
};
  1. Invoker 클래스
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class YOURGAME_API UInputHandlerComponent : public UActorComponent
{
    GENERATED_BODY()

private:
    TMap<FName, ICommand*> Commands;

public:
    void BindCommand(FName ActionName, ICommand* Command)
    {
        Commands.Add(ActionName, Command);
    }

    void HandleInput(FName ActionName)
    {
        if (Commands.Contains(ActionName))
        {
            Commands[ActionName]->Execute();
        }
    }

    virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override
    {
        for (auto& Pair : Commands)
        {
            delete Pair.Value;
        }
        Commands.Empty();
    }
};
  1. 사용 예시
void AMyCharacter::SetupCommands()
{
    UInputHandlerComponent* Handler = FindComponentByClass<UInputHandlerComponent>();

    if (Handler)
    {
        Handler->BindCommand("MoveForward", new MoveCommand(this, FVector(100, 0, 0)));
        Handler->BindCommand("Attack", new AttackCommand(this));
    }
}

void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);

    PlayerInputComponent->BindAction("MoveForward", IE_Pressed, this, &AMyCharacter::OnMoveForward);
    PlayerInputComponent->BindAction("Attack", IE_Pressed, this, &AMyCharacter::OnAttack);
}

void AMyCharacter::OnMoveForward()
{
    if (InputHandler)
    {
        InputHandler->HandleInput("MoveForward");
    }
}

void AMyCharacter::OnAttack()
{
    if (InputHandler)
    {
        InputHandler->HandleInput("Attack");
    }
}

상태 패턴 (State)

: 객체의 내부 상태에 따라 객체의 행동이 달라지게 하는 디자인 패턴. 상태를 별도의 클래스로 캡슐화하고 위임을 통해 행동을 변경한다.

상태 패턴이 필요한 상황

  • 캐릭터 상태 전환 (Idle -> Run -> Jump -> Attack)
  • 몬스터 AI (Patrol -> Chase -> Attack -> Die)
  • 문 상호작용 (닫힘 <-> 열림 <-> 잠김)
  • 보스 페이즈 전환

장점

  1. 상태 전이 로직이 깔끔하게 분리됨 -> 개방/폐쇄 원칙 준수
  2. 각 상태가 독립적이기 때문에 유지보수가 쉬움
  3. 동적으로 상태 추가/삭제 가능

단점

  1. 상태 수가 많아지면 클래스 수 증가
  2. 상태 간 공유 로직이 많을 경우 중복 코드 발생 가능

언리얼에서의 예시

: 플레이어의 상태 시스템

  1. 상태 인터페이스
class IPlayerState
{
public:
	virtual ~IPlayerState() {}
    
    virtual void Enter(AMyPlayer* Player) = 0;
	virtual void Exit(AMyPlayer* Player) = 0;
	virtual void HandleInput(AMyPlayer* Player, const FString& Input) = 0;
	virtual void Update(AMyPlayer* Player, float DeltaTime) = 0;
};
  1. 여러 상태 클래스
class IdleState : public IPlayerState
{
public:
	virtual void Enter(AMyPlayer* Player) override;
	virtual void Exit(AMyPlayer* Player) override;
	virtual void HandleInput(AMyPlayer* Player, const FString& Input) override;
	virtual void Update(AMyPlayer* Player, float DeltaTime) override;
};

class MoveState : public IPlayerState
{
public:
	virtual void Enter(AMyPlayer* Player) override;
	virtual void Exit(AMyPlayer* Player) override;
	virtual void HandleInput(AMyPlayer* Player, const FString& Input) override;
	virtual void Update(AMyPlayer* Player, float DeltaTime) override;
};
  1. 캐릭터 본체
UCLASS()
class YOURGAME_API AMyPlayer : public ACharacter
{
	GENERATED_BODY()

private:
	IPlayerState* CurrentState;

public:
	AMyPlayer();
	virtual void Tick(float DeltaTime) override;

	void HandleInput(const FString& Input);

	void ChangeState(IPlayerState* NewState);

protected:
	virtual void BeginPlay() override;
};

AMyPlayer::AMyPlayer()
{
	PrimaryActorTick.bCanEverTick = true;
	CurrentState = nullptr;
}

void AMyPlayer::BeginPlay()
{
	Super::BeginPlay();
	ChangeState(new IdleState());
}

void AMyPlayer::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	if (CurrentState)
	{
		CurrentState->Update(this, DeltaTime);
	}
}

void AMyPlayer::HandleInput(const FString& Input)
{
	if (CurrentState)
	{
		CurrentState->HandleInput(this, Input);
	}
}

void AMyPlayer::ChangeState(IPlayerState* NewState)
{
	if (CurrentState)
	{
		CurrentState->Exit(this);
		delete CurrentState;
	}

	CurrentState = NewState;

	if (CurrentState)
	{
		CurrentState->Enter(this);
	}
}

전략 패턴 (Strategy)

: 동일한 문제를 해결하는 여러 알고리즘을 캡슐화하고, 런타임에 알고리즘을 선택할 수 있게 하는 디자인 패턴. 즉, 동일한 작업을 여러 방식으로 수행할 수 있게 해주는 유연한 구조이다.

전략 패턴이 필요한 상황

  • 무기 시스템 : 캐릭터가 칼, 활, 마법 등 다양한 무기를 사용할 수 있도록 구현할 때
  • AI 행동 : 상황에 따라 다른 이동 방식(걷기, 날기, 점프 등)을 선택해야 할 때
  • 스킬 시스템 : 특정 스킬 슬롯에 어떤 효과를 넣느냐에 따라 다르게 동작할 때

장점

  1. 코드 분리 : 알고리즘을 독립된 클래스로 분리해 재사용성과 유지보수성 향상
  2. 런타임 교체 가능 : 실행 중에 행동을 변경할 수 있음
  3. 확장성 상승 : 전략을 추가할 때 기존 코드를 수정하지 않아도 됨

단점

  1. 클래스 수 증가 : 전략마다 클래스를 만들어야 해서 구조가 커질 수 있음
  2. 내부 구현 파악 어려움 : 런타임에 어떤 전략이 사용되는지 파악이 어려울 수 있음

언리얼에서의 예시

: 캐릭터가 전략적으로 다른 공격 방식을 선택할 수 있게 한다.

  1. 전략 인터페이스 정의
UINTERFACE()
class UAttackStrategy : public UInterface
{
	GENERATED_BODY()
};

class IAttackStrategy
{
	GENERATED_BODY()
    
public:
	virtual void ExecuteAttack(class AActor* Attacker) = 0;
};
  1. 전략 클래스 구현
UCLASS()
class UMeleeAttackStrategy : public UObject, public IAttackStrategy
{
    GENERATED_BODY()

public:
    virtual void ExecuteAttack(AActor* Attacker) override
    {
        UE_LOG(LogTemp, Warning, TEXT("근접 공격 실행!"));
    }
};


UCLASS()
class URangedAttackStrategy : public UObject, public IAttackStrategy
{
    GENERATED_BODY()

public:
    virtual void ExecuteAttack(AActor* Attacker) override
    {
        UE_LOG(LogTemp, Warning, TEXT("원거리 공격 실행!"));
    }
};
  1. 사용 예시
UPROPERTY()
TScriptInterface<IAttackStrategy> AttackStrategy;

// 전략 설정 함수
void SetAttackStrategy(TScriptInterface<IAttackStrategy> NewStrategy)
{
    AttackStrategy = NewStrategy;
}

// 공격 실행
void PerformAttack()
{
    if (AttackStrategy)
    {
        AttackStrategy->ExecuteAttack(this);
    }
}

profile
게임 개발자를 향해..

0개의 댓글