: 객체 생성 메커니즘을 다루는 패턴
: 클래스의 인스턴스가 단 하나만 존재하도록 보장하는 디자인 패턴. 주로 전역 상태를 관리해야 하는 경우에 사용되며, 클래스 내부에서 객체 생성을 통제하여 외부에서new를 통해 여러 개를 생성하지 못하게 한다.
: 게임 전체에서 하나만 존재해야 하고, 어디서든 접근 가능해야 하는 클래스
ClassName::Get() 등을 통해 접근 가능: 언리얼 엔진 내부에서 싱글톤으로 관리하고 있는 클래스가 많기 때문에 이를 이용해서 보통 싱글톤을 구현한다. 일반 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) 객체에 위임하는 디자인 패턴이다. new 대신 Create 메서드를 통해 객체를 생성하는데, 객체 생성의 책임과 로직을 분리하여, 유지보수성과 확장성을 향상시킬 수 있다.
new를 직접 호출하지 않음: 여러 종류의 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));
: 복잡한 객체의 생성 과정을 단계별로 분리하여, 유연하고 가독성 높은 방식으로 객체를 구성할 수 있도록 하는 디자인 패턴. 생성 로직을 '구성 과정'과 '최종 결과'로 분리하여 객체를 완성하는 방식이다.
: 무기를 속성별로 커스터마이징해서 생성
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();
: 클래스나 객체를 조합하는 패턴
: 호환되지 않는 인터페이스를 가진 클래스들을 연결해주는 디자인 패턴. 기존 클래스의 인터페이스를 변경하지 않고, 클라이언트가 요구하는 새로운 인터페이스에 맞춰주는 중간 계층을 제공한다.
: 프로젝트에서는 IAttackable 인터페이스를 사용하고 있는데, 외부에서 가져온 AOldEnemy 클래스는 이 인터페이스를 구현하지 않음. 따라서 AOldEnemy도 IAttackable 처럼 사용할 수 있도록 만들어서 코드 일관성을 유지시킨다.
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);
}
: 객체에 동적으로 새로운 기능을 추가하거나 확장할 수 있게 해주는 디자인 패턴. 객체의 기능을 직접 수정하지 않고, 동적으로 기능을 덧붙이는 방식으로 동작하며, 합성으로 기능을 확장한다. 원래 클래스와 같은 인터페이스를 구현하는 래퍼 객체를 사용한다.
: 기본 공격 시스템이 있는데, 여기에 '불속성', '중독 효과' 같은 데코레이터를 추가하여 공격에 효과를 확장하고 싶다.
IAttack 인터페이스UINTERFACE()
class UAttack : public UInterface
{
GENERATED_BODY()
};
class IAttack
{
GENERATED_BODY()
public:
virtual void PerformAttack() = 0;
};
UCLASS()
class ABasicAttack : public AActor, public IAttack
{
GENERATED_BODY()
public:
virtual void PerformAttack() override
{
UE_LOG(LogTemp, Log, TEXT("기본 공격 실행!"));
}
};
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("☠️ 중독 효과 적용!"));
}
};
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();
// 기본 공격 실행!
//🔥 불속성 데미지 추가!
// ☠️ 중독 효과 적용!
: 복잡한 서브시스템들을 하나의 간단한 인터페이스로 감싸서 외부에서 더 쉽게 사용할 수 있도록 해주는 디자인 패턴. 클라이언트가 내부 구조를 몰라도 작업을 수행할 수 있도록 도와준다.
: 전투 시스템을 위한 퍼사드 클래스 생성
class UAnimHandler
{
public:
void PlayAttackMontage();
};
class USoundHandler
{
public:
void PlayAttackSound();
};
class UDamageHandler
{
public:
void ApplyDamage(AActor* Target, float Amount);
};
UCombatSystemUCLASS()
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);
}
};
UCombatSystem* CombatSystem = NewObject<UCombatSystem>();
CombatSystem->Init();
CombatSystem->PerformAttack(EnemyActor);
: 객체 간 상호작용과 책임 분배 패턴
: 어떤 객체의 상태 변화가 있을 때, 그 객체에 의존하는 다른 객체들에게 자동으로 알림이 가도록 하는 디자인 패턴. 이벤트 시스템이나 상태 알림에 핵심적인 방법이다.
: 플레이어의 체력이 변하면 체력 UI가 자동으로 업데이트되도록 한다.
UINTERFACE()
class UHealthObserver : public UInterface
{
GENERATED_BODY()
};
class IHealthObserver
{
GENERATED_BODY()
public:
virtual void OnHealthChanged(float NewHealth) = 0;
};
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);
}
};
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);
}
}
}
};
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();
}
}
: 요청을 객체로 캡슐화하여 사용자가 요청을 매개변수화하거나, 요청을 큐에 저장하거나 실행 취소를 가능하게 만드는 디자인 패턴.
: 키 입력으로 캐릭터에게 명령을 내림
class ICommand
{
public:
virtual ~ICommand() {}
virtual void Execute() = 0;
};
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());
// 실제 공격 로직 호출 가능
}
}
};
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();
}
};
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");
}
}
: 객체의 내부 상태에 따라 객체의 행동이 달라지게 하는 디자인 패턴. 상태를 별도의 클래스로 캡슐화하고 위임을 통해 행동을 변경한다.
: 플레이어의 상태 시스템
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;
};
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;
};
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);
}
}
: 동일한 문제를 해결하는 여러 알고리즘을 캡슐화하고, 런타임에 알고리즘을 선택할 수 있게 하는 디자인 패턴. 즉, 동일한 작업을 여러 방식으로 수행할 수 있게 해주는 유연한 구조이다.
: 캐릭터가 전략적으로 다른 공격 방식을 선택할 수 있게 한다.
UINTERFACE()
class UAttackStrategy : public UInterface
{
GENERATED_BODY()
};
class IAttackStrategy
{
GENERATED_BODY()
public:
virtual void ExecuteAttack(class AActor* Attacker) = 0;
};
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("원거리 공격 실행!"));
}
};
UPROPERTY()
TScriptInterface<IAttackStrategy> AttackStrategy;
// 전략 설정 함수
void SetAttackStrategy(TScriptInterface<IAttackStrategy> NewStrategy)
{
AttackStrategy = NewStrategy;
}
// 공격 실행
void PerformAttack()
{
if (AttackStrategy)
{
AttackStrategy->ExecuteAttack(this);
}
}