Game Design Pattern

[TOC]

좋은 소프트웨어 구조

추상화와 디커플링의 적절한 적용

Design Pattern

Command Pattern(명령 패턴)

Flyweight Pattern(경량 패턴)

경량 패턴을 사용하기 위한 데이터의 분류

경량 패턴 예

Observer Pattern(관찰자 패턴)

관찰자 패턴 구성 요소

관찰자 패턴 사용시 주의점

연결리스트 관찰자 패턴의 포인터의 포인터 예제 해석

https://gist.github.com/parkpd/4638874487a358e27957971394d42e90

void Subject::removeObserver(Observer* observer) {
  //head변수의 포인터를 받지 않으면 head가 observer일 경우 head에 observer->next를 할당하는 코드가 필요하다.
  Observer** current = &head_;
  while (*current != NULL) {
    if (*current == observer) {
      *current = (*current)->next_;
      observer->next_ = NULL;
      return;
    }
    current = &(*current)->next_;
  }
}) 

요즘의 관찰자 패턴

미래의 관찰자 패턴

Prototype Pattern(프로토타입 패턴)

CPP에서 프로토타입

몬스터를 스폰하는 프로토타입 예제(함수포인터 사용)
//Ghost는 Monster를 상속한다.
Monster* spawnGhost(){
	return new Ghost();
}

typedef Monster* (*SpawnCallback)(); //Monster반환 함수 포인터

class Spawner{
    public:
    	Spawner(SpawnCallback spawn) spawn_(spawn){}
    	Monster* spawnMonster(){return spawn_();}
    private:
    	SpawnCallback spawn_;
}
//스폰 객체 생성
Spawner* ghostSpawner = new Spawner(spawnGhost);
//몬스터 스폰
ghostSpawner.spawnMonster();
몬스터를 스폰하는 프로토타입 예제(템플릿 사용)
//Spawner 클래스를 따로 만드는 이유는 몬스터를 스폰하는 코드에 매번 템플릿 매개변수로 Monster 관련 클래스를 넣지 않기 위함
class Spawner {
    public:
    	virtual ~Spawner(){}
    	virtual Monster* spawnMonster() = 0;
};

template <class T>
class SpawnerFor : public Spawner{
    public:
    	virtual Monster* spawnMonster(){
            return new T();
        }
}

// 스폰 객체 생성, Ghost는 Monster의 유도객체
Spawner* ghostSpawner = new SpawnerFor<Ghost>();

프로토타입 언어 패러다임

자바스크립트에서의 프로토타입
자바스크립트 객체 생성 방식
//클래스 정의(JS에서 클래스는 생성자 함수의 정의와 같다)
function A(b){
	this.b = b; //b라는 필트를 추가한다.
}
//A의 prototype에 fn 함수를 추가한다. new A(1) 이후에 해도 상관없다.
A.prototype.fn = function(){
    console.log(this.b);
};

var a = new A(1);
a.__proto__; // a에 필드가 없을 때 위임하는 객체, A.prototype
a.fn(); // 1, a객체에 fn필드가 없기 때문에 위임 객체는 A.prototype객체에 fn이 있는지 찾아 호출한다.

데이터 모델링을 위한 프로토타입

var a = {
	name:"abc",
};
var b = {
	hp:10
};
b.__proto__ = a;//(참고로 a는 함수가 아니므로 protytype필드가 없다)
console.log(b.name); // abc

Singleton Pattern(싱글턴 패턴)

싱글턴을 사용하는 이유

싱글턴이 문제인 이유

싱글턴의 대안

State Pattern(상태 패턴)

FSM(Finite State Machine - 유한 상태 기계)

FSM을 머드 게임의 맵에 비유
FSM 구현
class Hero{
private:
	HeroState* state_;
}
void Hero::handleInput(Input input) {
    //상태객체가 전이된 상태를 반환한다
	HeroState* state = state_->handleInput(*this, input);
    if(state != nullptr) {
        //현재 상태객체 교체하고 Hero 객체를 갱신한다.
        state_->exit(*this);
        delete state_;
        //새로운 상태 할당
        state_ = state;
        //새로운 상태로 입장
        state_->enter(*this);
    }
    //상태에 따라 갱신
    state_->update(*this);
}

class HeroState{
public:
    HeroState* handleInput(Hero, Input) = 0;
    void enter(Hero) = 0;
    void exit(Hero) = 0;
    void update(Hero) = 0;
}
class HeroStateStanding : public HeroState {
    HeroState* handleInput(Hero &hero, Input input) {
        //현재 상태에서 전이될 수 있는 상태중에 조건에 맞는 상태를 반환한다.
        if(input == PRESS_DOWN){
            return new HeroStateDucking();
        }
    }
    void enter(Hero &hero) {
        //현재 상태에 맞춰 Hero 초기화
    }
    void exit(Hero &hero) {
        //현재 상태의 종료를 위한 정리
    }
    void update(Hero &hero) {
        //hero 갱신
    }
}
class HeroStateDucking : public HeroState { ... }

// ----- 상태 객체를 정적 변수로 만들어 사용 ----- //
// 상태 객체가 상태(멤버변수)를 갖지 않는 경우 
// 함수는 모두 공통으로 사용하기 때문에 정적 상태 변수로 미리 만들어 놓고 사용
class HeroState{
public:
    static HeroStateStanding standing;
    static HeroStateDucking ducking;
    ...
}
class HeroStateStanding : public HeroState {
    HeroState* handleInput(Hero &hero, Input input) {
        //현재 상태에서 전이될 수 있는 상태중에 조건에 맞는 상태를 반환한다.
        if(input == PRESS_DOWN){
            return &HeroState::standing;
        }
    }
    ...
}
FSM의 단점과 해결
병행 상태 기계
계층형 상태 기계
푸시다운 오토마타(push-down Automata)
푸시다운 오토마타를 이해하기 위한 개념
오토마타(Automata)
푸시다운 오토마타
FSM이 유용한 경우

Strategy Pattern(전략 패턴)

Sequencing Patterns(순서 패턴)

Double Buffer Pattern(이중 버퍼 패턴)

의도

구조

사용하는 곳

//의사 코드
void updateAll(){
	for objs.update();//상태를 업데이트 한다. 다음 버퍼에 기록된다.
	for objs.swap();//상태를 변경한다. 현재 버퍼에 다음 버퍼의 값을 기록한다.(포인터를 교체하거나 값을 복사한다)
}

주의사항

디자인 결정 시 고려사항

버퍼를 어떻게 교체할 것인가?
얼마나 정밀하게 버퍼링할 것인가?

Game Loop Pattern(게임 루프 패턴)

의도

동기

//게임 루프
while(true){
	processInput();//이전 호출 이후 들어온 사용자 입력 처리
	update();//게임 시뮬레이션을 한 단계 실행(AI, 물리 순으로 실행)
	render();//게임 화면 그리기
}

게임에서의 시간

언제 쓸 것인가?

주의사항

플랫폼의 이벤트 루프에 맞춰야 할 수도 있다

Update Method Pattern(업데이트 메서드 패턴)

Behavioral Patterns(행동 패턴)

Bytecode Pattern(바이트코드 패턴)

Subclass Sandbox Pattern(하위 클래스 샌드박스 패턴)

의도

동기

Type Object Pattern(타입 객체 패턴)

Decoupling Pattern(디커플링 패턴)

Component Pattern(컴포넌트 패턴)

의도

Event Queue Pattern(이벤트 큐 패턴)

Intent

Motivation

Concept Of Event Queue Pattern

When to Use It

Keep in Mind

예제

A ring buffer 원형 버퍼
Aggregating requests 요청 취합하기
Source Code
class Audio{
public:
    void playSound(soundData){
        //같은 요청이 있는지 확인해서 무시할 수 있다
        for(i=head_; i!=tail; i=(i+1)%MAX_PENDING){
            if(pending_[i] == soundData) return;
        }
        pending_[tail_] = soundData;//요청을 tail_위치에 추가
        tail_ = (tail_+1) % MAX_PENDING;//끝이면 처음으로 보낸다
    }
    
    void update(){
        if(head_ == tail_) return;
        startSound(pending_[head_]);//head_ 위치의 요청 처리
        head_ = (head_+1) % MAX_PENDING;//끝이면 처음으로 보낸다
    }
    
    static const int MAX_PENDING = 16;
    static SoundData pending_[MAX_PENDING];//큐, 보류된 요청
    static int head_;
    static int tail_;
}

멀티스레드에서는?

Design Decisions

What goes in the queue? 큐에 무엇을 넣을 것인가?
If you queue events 큐에 이벤트를 넣는 경우
If you queue messages 큐에 메시지를 넣는 경우
Who can read from the queue? 누가 큐를 읽는가?
A single-cast queue 싱글캐스트 큐
A broadcast queue 브로드캐스트 큐
A work queue 작업 큐
Who can write to the queue? 누가 큐에 값을 넣는가?
With one writer 넣는 측이 하나라면
With multiple writers 넣는 측이 여러 개라면
What is the lifetime of the objects in the queue? 큐에 들어간 객체의 생명주기는 어떻게 관라할 것인가?
Pass ownership 소유권을 전달한다
Share ownership 소유권을 공유한다
The queue owns it 큐가 소유권을 가진다.

See Also 참고자료

Service Locator Pattern 서비스 중개자

Intent

Motivation

The Pattern

When to Use It

Keep in Mind 주의사항

The service actually has to be located. 서비스가 실제로 등록되어 있어야 한다.
The service doesn`t know who is locating it. 서비스는 누가 자기를 가져다가 놓는지 모른다.

예제

The service
The service provider 서비스 제공자
A Locator 중개자
A null service 널 서비스
class NullAudio{
public:
  virtual void play(){/*아무것도 하지 않는다*/}
}

class Locator{
public:
  static void initialize(){//getAudio()보다 먼저 호출되어야함
    service_ = &nullService_;//null객체를 등록
  }
  static Audio& getAudio(){return *service_;}//레퍼런스를 반환. null이 아님을 명시적으로 알림
  static void provide(Audio* service){
    if(service == null){//주입받은 서비스가 null인지 확인
      service_ = nullService_;
    } else {
      service_ = service;
    }
  }
private:
  Audio* service_;
  NullAudio nullService_;
}
Logging decorator 로그 데커레이터
class LoggedAudio : public Audio{
public:
  LoggedAutio(Audio &wrapped):wrapped_(wrapped){}
  virtual void play(){
    log("play()");
    wrapped_.play();
  }
private:
  Audio &wrapped_;
}

//오디오 서비스 로그 활성/비활성
Audio* service = new LoggedAudio(Locator::getAudio());//기존 Audio서비스 데코레이트
Locator::provide(service);//로그 서비스로 바꿔치기 한다.

Design Decisions 디자인 결정

How is the service located? 서비스는 어떻게 등록되는가?
outside code register it 외부 코드에서 등록
Bind to it at compile time 컴파일할 때 바인딩
Configure it at runtime 런타임에 설정 값 읽기
What happens if the service can`t be located? 서비스를 못 찾으면 어떻게 할 것인가?
Let the user handle it 사용자가 알아서 처리하게 한다.
Halt the game 게임을 멈춘다.
Return a null service 널 서비스를 반환한다.
What is the scope of the service? 서비스의 범위는 어떻게 잡을 것인가?
If access is global 전역에서 접근 가능한 경우
If access is restricted to a class 접근이 특정 클래스에 제한되면

See Also 참고사항

Optimization Patterns 최적화 패턴

Data Locality 데이터 지역성

Intent

Motivtion

CPU가 RAM에서 데이터를 가져오는 과정
A data warehouse 데이터 창고
A pallet for your CPU. CPU를 위한 팰릿(팔레트)
Wait, data is performance? 데이터 = 성능?

The Pattern

When to Use It

Keep in Mind

Sample Code

Contiguous arrays 연속 배열
// 개선 전 - 객체를 포인터로 관리
class GameEntity{
    AIComponent *ai;
}
GameEntity* gameEntities;
for ... {
    //gameEntities에 객체의 포인터가 들어있어 있어 접근하며 캐시 미스
    gameEntities[i]->ai->update();
}

// 개선 후 - 포인터가 아닌 객체 배열로 데이터 지역성을 높임
AIComponent* aiComponents = new AIComponent[MAX_ENTITIES];
for ... {
    //간접참조(->)를 안하기 때문에 캐시 히트
    aiComponents[i].update();
}
Packed data 정렬된 데이터
Hot/cold splitting 빈번한 코드와 한산한 코드 나누기

Design Decisions 디자인 결정

How do you handle polymorphism? 다형성은 어떻게 할 것인가?
Don`t (상속을) 사용하지 않는다.
Use separate arrays for each type 종류별로 다른 배열에 넣는다.
Use a collection of pointers 하나의 컬렉션에 포인터를 모아놓는다.
How are game entities defined? 게임 개체(GameEntity)는 어떻게 정의할 것인가?
If game entities are classes with pointers to their components 게임 개체 클래스가 자기 컴포넌트들을 포인터로 들고 있을 때
If game entities are classes with IDs for their components 게임 개체 클래스가 컴포넌트를 ID로 들고 있을 때
If the game entity is itself just an ID 게임 개체가 단순히 ID일 때

See Also

Dirty Flag

Intend

Motivation

Local and world transforms 지역 변환과 월드 변환
Cached world transforms 월드 변환 값 캐싱
Deferred recalculation 재계산 미루기

The Pattern

When to Use It

Keep in Mind

There is a cost to deferring for too long 너무 오래 지연하려면 비용이 든다.
You have to make sure to set the flag every time the state changes 상태가 변할 때마다 플래그를 켜야 한다.
You have to keep the previous derived data in memory 이전 파생 값을 메모리에 저장해둬야 한다.

Sample Code

void GraphNode::render(Transform parentWorld, bool dirty){
    dirty |= dirty_;//계층구조에서 하나라도 켜진경우 그 밑으로 모두 켜진다.
    if(dirty){
        world = local_.combine(parentWorld);//행렬변환
        dirty_ = false;//현재 객체의 dirty 플래그를 끈다.
    }
    renderMesh(mesh_, world_);
    
    for(...){
        children_[i]->render(world, dirty);//자식 객체의 render()를 호출
    }
}

Design Decisions

When is the dirty flag clearned? 더티 플래그를 언제 끌 것인가?
When the result is needed 결과가 필요할 때
At well-defined checkpoints 미리 정해놓은 지점에서 할 때
In the background 백그라운드 처리할 때
How fine-grained is your dirty tracking? 더티 플래그는 값 변화를 얼마나 세밀하게 관리해야 하는가?
If it’s more fine-grained 더 세밀하게 관리된다면
If it’s more coarse-grained 더 듬성듬성하게 관리한다면

See Also

Object Pool 객체 풀

Intent

Motivation

The curse of fragmentation 메모리 단편화의 저주
The best of both worlds 둘 다 만족시키기

The Pattern

When to Use It

Keep in Mind

The pool may waste memory on unneeded objects 객체 풀에서 사용되지 않는 객체는 메모리 낭비화 다를 바 없다.
Only a fixed number of objects can be active at any one time 한 번에 사용 가능한 객체 개수가 정해져 있다.
Memory size for each object is fixed 객체를 위한 메모리 크기는 고정되어 있다.
Reused objects aren’t automatically cleared 재사용되는 객체는 저절로 초기화되지 않는다.
Unused objects will remain in memory 사용 중이지 않은 객체도 메모리에 남아 있다.

Sample Code

class Particle{
    bool inUse;
    union {
        //활성화일 때 사용하는 데이터
        struct{
            double x, y;
        } data;
        //비활성화일 때 사용하는 포인터
        Particle* next;
    } dataAndNext;
}

class ParticlePool{
    Particle particles_[99];
    Particle* head;
    init(){
        // particles_를 초기화하고 next를 채워준다.
    }
    createParticle(){//파티클 생성
        head = head.dataAndNext.next;//head를 교체하고 반환
        return head;
    }
    releaseParticle(Particle* particle){//파티클 해제
        particle.dataAndNext.next = head;//반환된 파티클을 head로 지정
        head = particle;
    }
}

Design Decisions

Are objects coupled to the pool? 풀이 객체와 커플링되는가?
If objects are coupled to the pool 객체가 풀과 커플링된다면
If objects are not coupled to the pool 객체가 풀과 커플링되지 않는다면
What is responsible for initializing the reused objecct? 재사용되는 객체를 초기화할 때 어떤 점을 주의해야 하는가?
If the pool reinitializes internally 객체를 풀 안에서 초기화한다면
If outside code initializes the object 객체를 밖에서 초기화한다면

See Also

Spatial Partition 공간 분할

Intent

Motivation

Units on the field of battle 전장 위의 유닛들
Drawing battle lines 1차원 전장

The Pattern

When to Use It

Keep in Mind

Sample Code

A Sheet of graph paper 모눈종이
A grid of linked units 유닛을 연결 리스트로 저장한 격자
Entering the field of battle 전장 속으로 들어가기
A clash of swords 검의 격돌
Charging forward 진격
At arm’s length 사정거리 안에서

Design Decisions

Is the partition hierarchical or flat? 공간을 계층적으로 나눌 것인가, 균등하게 나눌 것인가?
If it’s a flat partitions 균등하게 나눈다면
If it’s hierarchical 계층적으로 나눈다면
Does the partitioning depend on the set of objects? 객체 개수에 따라 분할 횟수가 달라지는가?
If the partitioning is object-independent 객체 개수와 상관없이 분할한다면
If the partitioning adapts to the set of objects 객체 개수에 따라 영역이 다르게 분할된다면
If the partitioning is object-independent, but hierarchy is object-dependent 영역 분할은 고정되어 있지만, 계층은 객체 개수에 따라 달라진다면
Are objects only stored in the partition? 객체를 공간 분할 자료구조에만 저장하는가?
If it is the only place objects are stored 객체를 공간 분할 자료구조에만 저장한다면
If there is another collection for the objects 다른 컬렉션에도 객체를 둔다면

See Also

그 외 디자인 패턴

Dependency Injection DI 의존성주입

Decorator Pattern 장식자 패턴

용어

Queuing(큐잉)

Tearing(테어링)

Atomic(원자적)

Marshalling(마샬링)

Serialization(직렬화)

Reflection

Locality of reference 참조 지역성

Thrash 캐시 뒤엎기

Branch Misprediction 분기 예측 실패

Data-oriented Design 데이터 중심 디자인

Data driven Design 데이터 주도형 디자인

Dynamic Dispatch 동적 디스패치

Static Dispatch 정적 디스패치

Method Signature 메서드 시그니처

Overhead

영어

참고