서로 안 맞는, 호환되지 않는 인터페이스를 연결해주는 변환기
예시 상황 : 언리얼의 벡터랑 외부 물리엔진의 벡터랑 같은 3D벡터인데, 둘이 호환이 안 됨. 그래서 같은 로직을 다르게 2번 써야함
// 언리얼 엔진의 벡터 클래스
class FVector {
public:
float X, Y, Z; // 벡터 요소를 public 멤버변수로
float Size() const { /* 크기 */ }
};
// 외부 물리 엔진 (PhysX 같은)의 벡터 클래스
class PhysicsVec3 {
private:
float coords[3]; // 벡터 요소를 private 멤버변수배열로
public:
float getX() const { return coords[0]; }
float getY() const { return coords[1]; }
float getZ() const { return coords[2]; }
float magnitude() const { /*크기*/ }
};
// 언리얼 벡터를 처리
void MoveCharacter(FVector direction) {
float speed = direction.Size();
}
// 물리 엔진 벡터도 처리
void MoveCharacterPhysics(PhysicsVec3 direction) {
float speed = direction.magnitude();
}
// 공통 인터페이스
class IVector3D {
public:
virtual float GetX() const = 0;
virtual float GetY() const = 0;
virtual float GetZ() const = 0;
virtual float GetMagnitude() const = 0;
};
// 언리얼 벡터를 위한 어댑터
class FVectorAdapter : public IVector3D {
private:
FVector* vector; // 언리얼 벡터
public:
FVectorAdapter(FVector* vec) : vector(vec) {}
// 인터페이스 구현. 상속받은 자료형에 따라
float GetX() const override { return vector->X; }
float GetY() const override { return vector->Y; }
float GetZ() const override { return vector->Z; }
float GetMagnitude() const override { return vector->Size(); }
};
// 물리 엔진 벡터를 위한 어댑터
class PhysicsVectorAdapter : public IVector3D {
private:
PhysicsVec3* physicsVector; // 물리 엔진 벡터
public:
PhysicsVectorAdapter(PhysicsVec3* vec) : physicsVector(vec) {}
float GetX() const override { return physicsVector->getX(); }
float GetY() const override { return physicsVector->getY(); }
float GetZ() const override { return physicsVector->getZ(); }
float GetMagnitude() const override { return physicsVector->magnitude(); }
};
// 하나의 함수로 모든 벡터 처리 가능
void MoveCharacter(IVector3D* direction) {
float x = direction->GetX();
float y = direction->GetY();
float z = direction->GetZ();
float speed = direction->GetMagnitude();
}
// 사용 예시
void GamePlay() {
// 언리얼 벡터(unrealVec) 사용
FVectorAdapter unrealAdapter(&unrealVec);
MoveCharacter(&unrealAdapter);
// 물리 엔진 벡터(physicsVec) 사용
PhysicsVectorAdapter physicsAdapter(&physicsVec);
MoveCharacter(&physicsAdapter); // 이것도 잘 동작!
}
새로 만든 어댑터 인터페이스만 알면, 라이브러리가 다르더라도, 그 내부를 모르더라도 동일한 규칙으로 사용 가능
나중에 새로운 벡터가 또 생기더라도, 똑같이 인터페이스 이용해서 해주면 되어서 편함
적합한 경우
피해야 할 경우
객체에 새로운 기능을 동적으로 추가해 줄 수 있는 패턴
원본 객체는 보존한 채로 냅두고, 계속 겹겹이 감싸며 기능을 추가
예시 상황 : 기본 무기에 인챈트를 이용하여 여러 효과를 추가해주는 상황
// 상속으로 모든 조합 만들기
class Sword {};
class FireSword : public Sword {}; // 화염 검
class IceSword : public Sword {}; // 얼음 검
class LightningSword : public Sword {}; // 번개 검
// 2개 조합
class FireIceSword : public Sword {}; // 화염+얼음
class FireLightningSword : public Sword {}; // 화염+번개
class IceLightningSword : public Sword {}; // 얼음+번개
// 그 이상의 조합들... 10종류만 해도 1024개의 클래스 생성해야함
상속으로 모든 조합 해버리면, 객체가 너무 많아지고 런타임으로 기능 추가 제거도 안 됨
데코레이터 패턴으로 먼저 모든 무기의 인터페이스를 정의하고, 앞으로 확장될 기능들을 추상화
이 인터페이스를 구체화한 base클래스를 생성
// 모든 무기가 가져야 할 기본 기능
class IWeapon {
public:
virtual float GetDamage() const = 0; // 데미지
virtual float GetAttackSpeed() const = 0; // 공격 속도
virtual FString GetDescription() const = 0; // 설명
};
// 기본 검
class BasicSword : public IWeapon {
public:
float GetDamage() const override { return 50.0f; }
float GetAttackSpeed() const override { return 1.0f; }
FString GetDescription() const override { return "Basic Sword"; }
};
// 모든 인챈트의 부모 클래스
class WeaponDecorator : public IWeapon {
protected:
IWeapon* baseWeapon; // 무기 다형성
public:
WeaponDecorator(IWeapon* weapon) : baseWeapon(weapon) {}
virtual ~WeaponDecorator() { delete baseWeapon;}
// 기본 동작은 그냥 전달 (위임)
float GetDamage() const override { return baseWeapon->GetDamage(); }
float GetAttackSpeed() const override { return baseWeapon->GetAttackSpeed(); }
FString GetDescription() const override { return baseWeapon->GetDescription(); }
};
// 화염 인챈트
class FireEnchantment : public WeaponDecorator {
private:
float fireDamage = 20.0f; // 화염 추가 데미지
public:
FireEnchantment(IWeapon* weapon) : WeaponDecorator(weapon) {}
// 기존 기능에 인챈트 기능 추가
float GetDamage() const override {
return baseWeapon->GetDamage() + fireDamage;
}
FString GetDescription() const override {
return baseWeapon->GetDescription() + " [Fire +20]";
}
};
// 얼음, 번개... 다른 것들도 마찬가지로 추가
void CreateMyWeapon() {
// 기본 검 생성 "Basic Sword - 데미지: 50.0"
IWeapon* mySword = new BasicSword();
// 화염 인챈트 추가 "Basic Sword [Fire +20] - 데미지: 70.0"
mySword = new FireEnchantment(mySword);
// 번개 인챈트 추가
// "Basic Sword [Fire +20] [Lightning +25, Speed +20%] - 데미지: 95.0, 공속: 1.2"
mySword = new LightningEnchantment(mySword);
}
새 인챈트가 추가될 때마다, 기존 무기 base클래스를 수정하는게 아니라, 감싸주는 것
적합한 경우
피해야 할 경우
복잡한 내부시스템을 외부에서는 한 눈에 잘 보이도록 간단한 인터페이스로 통합하여 제공
추상화와 비슷
문제 상황 : 게임 시작 시 수많은 시스템을 초기화하는데 복잡하고 순서도 중요한 상황
void StartGame() {
// 1. 렌더링 시스템 초기화
RenderingSystem* renderer = new RenderingSystem();
renderer->Initialize();
renderer->LoadShaders(); // 셰이더 로딩
renderer->SetupRenderTargets(); // 렌더 타겟 설정
// 렌더링 시스템 초기화 함수들.. //
// 2. 오디오 시스템 초기화
// 3. 입력 시스템 초기화
// 4. 리소스 로딩
// ... 더 많은 시스템들 초기화 과정
}
Facade 패턴은 시스템 마다 자신만의 초기화와 의존시스템이 있음
세부적인 초기화 순서나 방법은 각 시스템 자신만 알면 됨
// 렌더링 시스템 - 그래픽 담당
class RenderingSystem {
public:
void Initialize() {
LoadShaders();
SetupRenderTargets();
// 나머지 과정 계속 호출
}
void LoadShaders() { /* ... */ }
void SetupRenderTargets() { /* ... */ }
// 나머지 초기화 함수들..
};
// 오디오 시스템 - 소리 담당
class AudioSystem {
// 마찬가지로 초기화 구현
};
// 그 외 시스템들도 마찬가지...
그리고 Facade라는 거대한 객체가 각 서브시스템을 내부에 보유
Facade가 보유하고 나중에 위임시킴
그래서 외부에서 얘한테 start를 호출하면 이 객체가 나머지 일을 처리
class GameSystemFacade {
private:
// 모든 복잡한 시스템들은 내부에 숨기기
RenderingSystem* renderer;
AudioSystem* audio;
public:
// Facade가 모든 시스템을 관리
GameSystemFacade() {
renderer = new RenderingSystem();
audio = new AudioSystem();
}
// 메모리 정리도 Facade가 알아서
~GameSystemFacade() {
delete renderer;
delete audio
// 나머지 시스템들..
}
// 싱글게임 초기화
void StartSinglePlayerGame() {
renderer->Initialize();
audio->Initialize();
input->Initialize();
}
// 멀티플레이어 게임 초기화
void StartMultiplayerGame() {
// 멀티플레이어는 네트워크도 필요
renderer->Initialize();
audio->Initialize();
input->Initialize();
network->Initialize();
};
// 실제 게임 시작하는 방법
void NewWayToStartGame() {
GameSystemFacade* gameFacade = new GameSystemFacade();
// 싱글플레이어 함수 하나로 호출
gameFacade->StartSinglePlayerGame();
// 멀티플레이어 함수 하나로 호출
gameFacade->StartMultiplayerGame();
}
외부 개발자 입장에서 복잡한 초기화 방법 몰라도 그냥 start만 호출하면 됨
적합한 경우
피해야 할 경우
한 객체의 상태가 변하면 모든 의존 객체들이 알림을 받고 자동으로 업데이트 됨 (1:N)
문제 상황 : 플레이어 체력이 떨어지면 관련 객체들에게 일일이 다 알려야함
class Player {
public:
void TakeDamage(float damage) {
health -= damage;
// 모든 시스템을 직접 호출
healthBar->UpdateHealth(health);
audio->PlayDamageSound();
// ...
}
};
class IHealthObserver {
public:
virtual void OnHealthChanged(float health) = 0;
};
상태가 바뀌면, 다른 객체들에게 옵저버가 알림을 보냄
플레이어가 모든 시스템에 직접 호출하게 되면 복잡해지고 의존성도 높아짐. 시스템 추가될 때마다도 수정해줘야함.
옵저버 패턴은 플레이어가 그냥 "나 체력 깎임"을 옵저버에게 알리고, 옵저버가 다른 애들한테 알림.
옵저버 인터페이스의 함수는 알림 받을 애들은 이 함수를 구현해야함. 플레이어는 구체적인 UI ,소리 타입 몰라도 그냥 이 인터페이스만 알면 됨.
class HealthBarUI : public IHealthObserver {
// override
void OnHealthChanged(float health) override { /**/ }
};
class AudioObserver : public IHealthObserver {
void OnHealthChanged(float health) override { /**/ }
};
class Player {
private:
float health = 100.0f;
vector<IHealthObserver*> observers;
public:
void AttachObserver(IHealthObserver* obs) {
observers.push_back(obs);
}
void NotifyHealthChanged() {
for (auto obs : observers) {
// 실제 동작방식 모르고 그냥 호출만 하면 됨
obs->OnHealthChanged(health);
}
}
void TakeDamage(float damage) {
health -= damage;
NotifyHealthChanged(); // 모든 관찰자에게 자동 알림
}
};
// 사용 방법
player.AttachObserver(&ui);
player.AttachObserver(&audio);
player.TakeDamage(30); // UI와 Audio가 자동으로 반응
delegate는 이벤트나 동작을 한 객체가 하나의 대표객체에게 어떤 행동을 하라고 지시. 1:1 관계임. 옵저버는 1:N.
적합한 경우
피해야 할 경우
// Command 인터페이스
class ICommand {
public:
virtual void Execute() = 0;
virtual void Undo() = 0;
};
// Receiver (실제 작업을 수행하는 객체)
class Player {
public:
int x = 0, y = 0;
void MoveUp() { y++; }
void MoveDown() { y--; }
};
// Concrete Commands
class MoveUpCommand : public ICommand {
private:
Player* player;
public:
MoveUpCommand(Player* p) : player(p) {}
void Execute() override { player->MoveUp(); }
void Undo() override { player->MoveDown(); }
};
// Invoker (커맨드 실행자)
class CommandManager {
private:
vector<ICommand*> history; // 로그
int currentIndex = -1;
public:
void ExecuteCommand(ICommand* cmd) {
cmd->Execute();
history.push_back(cmd);
currentIndex++;
}
void Undo() {
if (currentIndex >= 0) {
history[currentIndex]->Undo();
currentIndex--;
}
}
};
// 사용 방법
Player player;
CommandManager cmdManager;
auto moveCmd = new MoveUpCommand(&player);
cmdManager.ExecuteCommand(moveCmd); // 실행
cmdManager.Undo(); // 되돌리기!
커맨드 인터페이스를 통해 세부 동작이 달라도 같은 방식으로 호출 가능
플레이어는 리시버. 실제 작업을 수행.
command 구체 객체는 간접적으로 리시버를 호출. 명령과 실제 작업하는 부분을 분리.
커맨드 매니저는 명령을 전부 컨트롤할 객체로 히스토리를 만들어 실행할 내용 다 저장.
행동을 data처럼 다루는 패턴. 행동은 함수가 아니라 객체가 됨
Enhanced Input System, Input Action이 이러한 패턴 방식
적합한 경우
피해야 할 경우
class Enemy {
enum State { IDLE, CHASE, ATTACK };
State currentState = IDLE;
public:
void Update() {
switch(currentState) {
case IDLE:
// ... //
break;
case CHASE:
// ... //
break;
// 상태가 늘어날수록 복잡해짐...
}
}
};
객체의 내부 상태가 변할 때, 객체의 행동도 함께 변하도록 하는 패턴
상태를 별도의 클래스로 캡슐화하고 위임을 통해 행동을 변경
// State 인터페이스
class IEnemyState {
public:
virtual void Update(class Enemy* enemy) = 0;
};
// Concrete States
class IdleState : public IEnemyState {
public:
void Update(Enemy* enemy) override { /*..*/ }
};
class ChaseState : public IEnemyState {
public:
void Update(Enemy* enemy) override { /*..*/ }
};
// 적 객체
class Enemy {
private:
IEnemyState* currentState;
public:
void ChangeState(/*..*/) {
// static으로 메모리 재사용
static IdleState idle;
static ChaseState chase;
// 바꿀 state로 currentState 수정
}
void Update() {
if (currentState) currentState->Update(this);
}
};
// 사용 방법
Enemy enemy;
enemy.ChangeState(IdleState);
enemy.Update(); // 상태에 따라 다른 행동
각 상태 자체가 객체가 되어 동작을 관리
state는 인터페이스를 상속받아 업데이트를 구현
enemy는 그냥 상태만 변경해주고 상태 객체에 동작을 위임. 현재 상태 전환 판단역시 각 상태 안에서. Update만으로 알아서 상태에 따라 동작하게 된다.
behaviour tree, AI에 적합. 언리얼의 애니메이션 블루프린트가 예시.
적합한 경우
피해야 할 경우
동일한 결과를 하는데 그 알고리즘을 전환해주는 패턴
쉬움 난이도면 적이 랜덤하게 움직임. 어려움 난이도면, 적이 플레이어를 바로 추적.
상태패턴이랑 구현 방식은 거의 동일
state는 객체 내부에서 state가 변경되는거고, 전략은 외부에서 전략을 수정하여 객체를 변경
상태 패턴이든, 전략 패턴이든 여러 개의 상황이나 조건에 따라 동작이 달라져야 할 때 사용하면 좋음
다양한 난이도, AI 행동패턴, 팀전술(공격적, 수비적), 무기별 공격 패턴