상위 클래스가 제공하는 기능들을 통해서 하위 클래스에서 행동을 정의한다.

 

12.2 동기

Superpower라는 상위 클래스를 만든 후에 초능력별로 이를 상속받는 클래스를 정의한다고 가정

초능력 클래스가 수십 개가 넘으면 초능력 클래스를 길게 하드코딩 하는 것보다 데이터 기반으로 구현하는게 낫다.

타입 객체(13장), 바이트코드(11장), 인터프리터 같은 패턴을 사용할 수 있다.

 

ㄴ 중복 코드가 많아진다.

ㄴ 거의 모든 게임 코드가 초능력 클래스와 커플링된다.

ㄴ 외부 시스템이 변경되면 초능력 클래스가 깨질 가능성이 높다.

ㄴ 모든 초능력 클래스가 지켜야 할 규칙을 정의하기 어렵다.

class Superpower {
	public:
    	virtual ~Superpower() {}
        
	protected:
    	virtual void activate() = 0; // 추상 샌드박스 메서드
        void move(double x, double y, double z) {
         // 코드..
        }
        void playSound(SoundId sound, double volume) {
         // 코드..
        }
        
        double getHeroX() { }
        double getHeroY() { }
        double getHeroZ() { }
    }
}   

class  SkyLaunch : public Superpower {
	protected:
    	virtual void activate() { // 샌드박스 메서드를 오버라이딩한다. 
        // 하늘로 튀어 오른다.
        // 상위 클래스의 메서드들을 조합하여 기능을 구현한다.
        playSound(SOUND_SPROING, 1.0f); 
        spawnParticles(PARTICLE_DUST, 10);
        move(0, 0, 20);
   	}
};

12.5 주의사항

상위 클래스에 코드가 계속 쌓이는 경향이 있다.

많은 하위 클래스들이 상위 클래스를 통해서 게임 코드에 접근하므로 상위 클래스가 하위 클래스에서 접근하려는 모든 시스템(사운드, 이펙트)과 커플링 된다.

그렇기 때문에 상위 클래스를 조금만 바꿔도 문제가 생기는 하위 클래스가 생길 수 있다. (깨지기 쉬운 상위 클래스 문제)

 

12.7 디자인 결정

어떤 기능을 제공해야 하나?

외부 기능을 전부 상위 클래스에서 제공받으면 상위 클래스가 지나치게 복잡해질 수 있기 때문에 하위 클래스에서 처리할 수 있으면 그렇게 하는 것이 낫기 때문에 상위 클래스가 제공할 기능들을 선택해야 한다.

 

상위 클래스가 제공해야 할 기능

1. 모든 하위 클래스가 영향을 받는 기능

2. 외부 시스템의 상태를 변경하는 함수 (set 함수?)

ㄴ 이러한 함수는 외부 시스템과 강하게 커플링되기 때문에 상위 클래스로 옮겨주어 하위 클래스의 결합도를 낮춰야 한다.

 

메서드를 직접 제공할 것인가? 이를 담고 있는 객체를 통해서 제공할 것인가?

개별 기능들을 전부 메서드화 하여 상위 클래스에 넣으면 메서드가 너무 많아질 수 있기 때문에 역할이 비슷한 기능들을 하나의 클래스에 묶고 그 객체를 반환하는 메서드를 제공하는 방법을 사용할 수 있다.

 

상위 클래스는 필요한 객체를 어떻게 얻는가?

1. 상위 클래스의 생성자 인수로 받기

ㄴ 하위 클래스 생성자도 인수로 받아야 하는 문제가 있다.

 

2. 2단계 초기화

ㄴ 상위 클래스 내부에 init 함수를 두어 필요한 객체를 init 함수의 인자로 받는다.

1단계 : 객체 생성

2단계 : 객체->init(필요한 객체들);

 

3, 정적 객체로 만들기

필요한 객체가 예를 들어 파티클 시스템(싱글턴)이라면 상위 클래스에 정적 객체를 만들고 초기화 했을 때 모든 인스턴스에서 파티클 시스템에 접근할 수 있고, 인스턴스별로 파티클 시스템 객체를 저장할 필요가 없어 메모리 사용량을 줄일 수 있다.

class Superpower {
	public:
    	static void init(ParticleSystem* particles) {
        	particles_ = particles;
        }
       	
        // 샌드박스 메서드와 그 외 다른 기능들...
    
    private:
    	static ParticleSystem* particles_;
};

 

4. 서비스 중개자를 이용하기

상위 클래스가 필요로 하는 객체를 직접 가져와 스스로 초기화 하는 방법 (서비스 중개자 패턴, 16장)

class Superpower {
	protected:
    	void spawnParticles(ParticleType type, int count) {
        	particleSystem& particles = Locator::getParticles();
            particles.spawn(type, count);
        }
    }
    // 샌드박스 메서드와 그 외 다른 기능들
};

가상 머신 명령어를 인코딩한 데이터로 행동을 표현할 수 있는 유연함을 제공한다.

 

큰 규모의 프로젝트는 빌드에 시간이 오래 걸릴 수 있기 때문에, 자주 고칠 수 있어야 하는 코드는 데이터 파일로 빼서 쉽게 수정할 수 있도록 해야 한다.

 

기계어의 성능과 인터프리터 패턴의 안정성 사이에서 절충해야 한다.

 

가상 기계어(바이트 코드)와 가상 머신(가상 기계어를 실행하는 에뮬레이터 = 인터프리터)을 사용

 

11.3 패턴

명령어 집합은 실행할 수 있는 저수준 작업들을 정의한다.

명령어는 일련의 바이트로 인코딩된다.

가상 머신은 중간 값들을 스택에 저장해가면서 명령어를 하나씩 실행한다.

 

11.4 언제 쓸 것인가?

1. 게임 구현에 사용된 언어가 너무 저수준이라 만드는 데 손이 많이 가거나 오류가 생기기 쉬운 경우 고수준으로 표현하기 위해 사용한다.

2. 컴파일 시간이나 다른 빌드 환경 때문에 반복 개발하기가 너무 오래 걸리는 경우

3. 정의하려는 행동이 보안에 취약한 경우

 

바이트코드는 네이티브 코드보다는 느리므로 성능이 민감한 곳에는 적합하지 않다.

enum Instruction {
  INST_SET_HEALTH = 0x00,
  INST_SET_WISDOM = 0x01,
  INST_PLAY_AGILITY = 0x02,
  INST_PLAY_SOUND = 0x03,
  INST_SPAWN_PARTICLES = 0x04
};

class VM {
public: 
  void interpret(char bytecode[], int size) {
    for (int i = 0; i < size; i++) {
      char instruction = bytecode[i];
      
      switch (instruction) {
  		case INST_SET_HEALTH:
    	  setHealth(0, 100);
    	  break;
        case INST_SET_WISDOM:
          setWisdom(0, 100);
          break;
  
        case INST_SET_AGILITY:
          setAgility(0, 100);
          break;
  ...
      }
    }
  }
}

대부분의 바이트코드 VM은 1바이트로 명령어를 표현한다. (256개의 명령어)

 

스택 머신

class VM {
  public:
    void interpret(char bytecode[], int size) {
    
    .. 위 예제와 동일 ..
    
    switch (instruction) {
    
      case INST_SET_HEALTH:
        int amount = pop();
        int wizard = pop();
        setHealth(wizard, amount);
        break;
    
    ....
    
      case INST_LITERAL: 
        int value = bytecode[++i];
        push(value);
        break;
    }
    
    
  }
  
  private:
    static const int MAX_STACK = 128;
    int stackSize_;
    int stack_[MAX_STACK];
  
    void push(int value){
      assert(stackSize_ < MAX_STACK);
      stack_[stackSize_++] = value;
    }
    
    int pop(){
      assert(stackSize_ > 0);
      return stack_[--stackSize_];
    }

명령어가 매개변수를 받을 때는 스택을 이용해서 전달할 수 있다.

리터럴 명령어는 바이트 코드에서 바이트 배열에서 다음 인덱스의 바이트 값을 읽는다.

위에서 구현한 숫자 리터럴의 경우 바이트 값을 그대로 int 변수에 저장한다.

 

GET_HEALTH 명령어와 ADD 명령어같은 명령어도 추가할 수 있다.

 

ADD 명령어는 스택에서 두 값을 pop하여 결과를 push하는 방식으로 구현할 수 있다.

 

고수준 행동을 바이트 코드로 만들어 주는 툴이 필요하다

GUI 툴, 텍스트 기반 언어 컴파일러를 사용할 수 있다.

 

11.7 디자인 결정

두가지 종류의 바이트 코드 VM

1. 스택 기반 VM

ㄴ 명령어가 짧다.

ㄴ 코드 생성이 간단하다.

ㄴ 하나의 행동을 수행하는데 필요한 명령어 개수가 많다.

 

2. 레지스터 기반 VM

ㄴ 명령어가 길다.

ㄴ 명령어의 개수는 줄어든다.

 

어떤 명령어를 만들어야 하는가?

1. 외부 원시명령

ㄴ 게임 코드에 접근하고 유저가 볼 수 있는 일들을 처리한다. (위 예제에서는 INST_SET_HEALTH 와 같은 것들)

 

2. 내부 원시명령

ㄴ 리터럴, 연산, 비교, PUSH, POP 같은 명령어들

 

3. 흐름 제어

ㄴ 반복문을 구현하기 위해 jump가 필요하다. (바이트 코드의 실행 위치를 옮기는 명령)

 

4. 추상화

ㄴ 고수준 언어에서의 함수 역할을 하기 위해 call(호출) 과 return(반환) 명령이 필요하다.물론 jump도 필요하다.

 

값을 어떻게 표현할 것인가?

1. 단일 자료형

ㄴ 위의 예제처럼 하나의 자료형을 사용하는 방법

 

2. 태그 붙은 변수

ㄴ 8비트를 사용한다고 가정하면 앞의 2비트는 자료형(int인지 double인지 string인지)를 구분하는데 사용, 나머지 6비트는 데이터로 사용(공용체)

ㄴ 런타임에서 값의 자료형을 확인할 수 있다는 장점이 있다.

ㄴ 타입을 검사하느라 클럭을 낭비한다.

enum ValueType {
  TYPE_INT,
  TYPE_DOUBLE,
  TYPE_STRING
};

struct Value {
  ValueType type;
  union {
    int intValue;
    double doubleValue;
    char* stringValue;
  };
};

 

3. 태그가 붙지 않은 공용체

ㄴ 공용체를 사용하되 자료형 태그는 따로 없어서 사용자가 알아서 해석해야 한다.

ㄴ 안전하지 않다.

 

4. 인터페이스

ㄴ Value 인터페이스를 만들고 IntValue FloatValue 등의 구현 클래스가 상속받게 하는 방법이다.

ㄴ 자료형마다 클래스를 만들어주는게 번거롭고 비효율적(다형성은 포인터를 통해서 동작하기 때문에)이다.

 

결론 : 되도록이면 자료형은 하나만 쓰거나 안되면 태그 붙은 변수를 쓰자.

바이트코드는 어떻게 만들 것인가?

1. 텍스트 기반 언어를 정의할 경우

ㄴ 문법을 정의해야 한다.

ㄴ 파서를 구현해야 한다.

ㄴ 문법 오류를 처리해야 한다.

 

2. UI가 있는 저작 툴을 만들 경우

ㄴ UI를 구현해야 한다.

ㄴ 오류가 적다.

ㄴ 이식성이 낮다. (UI 프레임워크가 OS에 종속적이기 때문에)

컬렉션에 들어 있는 객체별로 한 프레임 단위의 작업을 진행하라고 알려줘 전체를 시뮬레이션한다.

 

모든 개체가 자신의 동작을 캡슐화(update 함수로 만들기)해야 게임 루프에 개체를 쉽게 추가, 삭제할 수 있다.

큰 update 안에 각 객체마다의 update가 있다.

 

10.4 업데이트 메서드는 언제 쓰는 것이 좋은가?

1. 동시에 동작해야 하는 객체나 시스템이 게임에 많은 경우

 

2. 각 객체의 동작은 다른 객체와 독립적이어야 한다.

 

3. 객체는 시간의 흐름에 따라 시뮬레이션되어야 한다.

 

10.5 주의사항

1. 코드를 한 프레임 단위로 끊어서 실행해야 하기 떄문에, 코드가 복잡해질 수 있다.

 

2. 다음 프레임에서 다시 시작할 수 있도록 현재 상태를 저장해야 한다. (상태 패턴(7장) 을 사용하면 좋다.)

 

3. 모든 객체는 매 프레임마다 시뮬레이션되지만 진짜로 동시에 되는 건 아니다.

ㄴ 객체 업데이트 순서를 신경 써야 한다. 한 위치에 하나의 객체만 존재할 수 있다고 가정할 때, 서로 다른 두 객체 A와 B가 같은 위치로 움직이려 한다면 업데이트 순서에 따라 결과가 다를 것이다. 

순차적으로 업데이트를 진행하지 않고 병렬적으로 객체를 업데이트 하고 싶다면 (이중 버퍼 패턴(8장) 같은 게 필요하다.)

 

4. 업데이트 도중에 객체 목록을 바꾸는 건 조심해야 한다.

ㄴ 업데이트 도중 객체를 추가하거나 제거할 때 순차적으로 update하다 보면 아직 렌더링 되지 않은 객체의 update문이 실행될 수도 있고, 업데이트하려는 객체 이전에 있는 객체를 삭제할 경우, 객체를 하나 건너뛸 수 있다.

 

ㄴ 객체 추가 문제는 업데이트 가능 객체 개수를 프레임 시작시에 미리 저장하여 해결할 수 있고, 객체 삭제 문제는 순회 변수를 조정하거나 객체에 '죽었음' 표시를 하고, 전체 루프가 끝날 때 객체 컬렉션에서 제거하여 해결할 수 있다.

 

10.7 디자인 결정

업데이트 메서드를 어느 클래스에 둘 것인가?

1. 개체 클래스

개체마다 update() 함수를 정의 ( 

 

2. 컴포넌트 클래스

컴포넌트 패턴은 한 개체의 일부를 개체의 다른 부분들과 디커플링 한다.

컴포넌트는 알아서 자기 자신을 업데이트 한다.

 

3. 위임 클래스

개체의 update() 내부에서 다른 클래스의 update() 를 호출 

여러 개체가 동작을 공유할 수 있게 해준다.

 

휴면 객체 처리

update가 필요 없는 객체가 생기는 경우 순회 객체 대상에서 제거 해야 한다.

살아 있는 객체만 컬렉션에 모아두면 된다.

 

1. 비활성 객체가 포함된 컬렉션 하나만 사용할 경우

ㄴ 활성 상태인지 확인하여 아니면 넘어간다. (CPU 클럭 낭비 & 데이터 캐시 낭비)

 

2. 활성 객체만 모여 있는 컬렉션을 하나 더 둘 경우

ㄴ 두 번째 컬렉션을 위해 메모리를 추가로 사용해야 한다.

ㄴ 컬렉션 두 개의 동기화를 유지해야 한다. (객체가 생성되거나 소멸되는 경우 두 컬렉션을 모두 변경해야 한다.)

ㄴ 비활성 객체가 많을수록 컬렉션을 따로 두는 게 좋다.

 

게임 시간 진행을 유저 입력, 프로세서 속도와 디커플링 해야 한다.

게임은 유저 입력이 없어도 계속 돌아간다. (루프가 존재한다.)

게임 루프의 코드는 프로그램 실행 시간의 대부분을 차지하기 때문에 최적화를 고려하여 깐깐하게 만들어야 한다.

d3dApp.cpp의 Run()에 존재하는 루프문

게임 루프의 핵심 업무는 어떤 하드웨어에서라도 일정한 속도로 실행될 수 있도록 하는 것이다.

좋은 하드웨어에서의 환경과 좋지 않은 환경에서의 while문 내부 코드 소요 시간은 차이가 있을 수 있기 때문에 프레임을

60FPS로 고정하면 (좋지 않은 환경에서도 60FPS는 보장 된다는 가정 하에) 게임이 느려지는 문제는 회피할 수 있다.

 

Sleep(100); 코드는 사실 sleep(while문 시작 시점 시간 - while문 끝 시점 시간 + 100) 으로 써야 정확하다.

 

가변 시간 간격, 유동 시간 간격

위 예제에서 mTimer.Tick() 함수는 현재 시각을 기록하고 mTimer 객체는 현재 시각과 이전 시각을 갖고 있다.

Update와 Draw 함수에 mTimer 객체를 넘겨줌으로써 시간 간격 차이를 이용해 게임 월드 상태를 진행할 수 있다.

 

예를 들면 총알의 위치를 속도 * 시간 간격으로 계산 할 수 있다.

 

가변 시간 간격 방식을 사용하면 게임이 비결정적이게 된다.

네트워크 게임에서 위의 총알 위치를 계산하는 방법을 예시로 들자면

1초에 총알을 위치를 50번 계산하는 PC에서와 5번 계산하는 PC에서의 총알의 위치는 다를 수 있다.

ㄴ 기존 위치 + 변위 값을 통해 위치를 계산하게 될텐데 변위값은 float이기 때문에 숫자를 누적할수록 반올림 오차가 커질 수 있기 때문이다.

 

물리 계산은 고정 시간 간격을 사용하되 렌더링되는 간격은 유연하게 하는 방법

렌더링되는 간격을 60FPS로 고정시킨다면 프로세서의 낭비이다. (더 자주 렌더링 할 수 있는데도 막는 것이므로)

 

double previous = getCurrentTime();
double lag = 0.0;
while (true) {
  double current = getCurrentTime();
  double elapsed = current - previous;
  previous = current;
  lag += elapsed;
  processInput();
  
  while (lag >= MS_PER_UPDATE) {
  	update();
    lag -= MS_PER_UPDATE;
  }
  render(lag / MS_PER_UPDATE);
}

render 함수의 인자는 위치 보간에 쓰인다.

update()와 render()가 동일한 시각에 호출된다는 보장이 없기 때문에 물체의 움직임이 튀어 보일 수 있다.

 

렌더러가 게임 객체들과 각각의 속도를 안다고 가정할 때, 아마도 정점 셰이더에서 이 인자를 이용해 위치 보간을 하지 않을까 생각한다. (확실하지 않음..) 실제로 물리 계산에 필요한 정보를 수정하는 것은 의미가 없기 때문이다. 

 

60fps 고정 시간 간격 환경에서의 함수 호출 빈도(16ms = 1/60초 라고 가정)

9.7 디자인 결정

1. 게임 루프를 직접 관리하는가, 플랫폼이 관리하는가?

ㄴ 플랫폼 이벤트 루프 사용

ㄴ 게임 엔진 루프 사용

ㄴ 직접 만든 루프 사용

 

2. 전력 소모 문제

ㄴ 프레임율 제한

ㄴ 프레임율 제한 해제

 

3. 게임 플레이 속도는 어떻게 제어할 것인가?

ㄴ 동기화 없는 고정 시간 간격 방식 - 게임 속도가 하드웨어와 게임 복잡도에 영향을 받는다.

ㄴ 동기화하는 고정 시간 간격 방식 - 고정 시간 간격으로 게임을 실행하되, 루프 마지막에 지연(sleep)이나 동기화 지점을 넣어서 게임이 너무 빨리 실행되는 것을 막는다.

ㄴ 가변 시간 간격 방식

ㄴ 업데이트는 고정 시간 간격으로, 렌더링은 가변 시간 간격으로

게임의 렌더링 시스템에서는 프레임 버퍼를 최소 두 개 사용한다. 프레임 버퍼에 대한 읽기와 쓰기를 동시에 수행할 수 있도록 하기 위함이다.

 

전체 장면을 한 번에 화면에 띄워주기 위해 한 프레임 버퍼가 화면에 띄워질 동안 다른 프레임 버퍼는 렌더링 코드에 의해 값이 쓰여진다. 값이 쓰여진 프레임 버퍼는 다시 GPU가 읽어 화면에 출력해 준다. 이 과정이 반복된다.

 

8.3 패턴

버퍼 클래스는 현재 버퍼와 다음 버퍼, 이렇게 두 개의 버퍼를 갖는다.

정보를 읽을 때는 현재 버퍼에 접근, 정보를 쓸 때는 다음 버퍼에 접근, 변경이 끝나면 다음 버퍼와 현재 버퍼를 교체한다.

 

DirectX에서는 렌더링을 위한 이중 버퍼를 지원한다. IDXGISwapChain 를 버퍼 클래스로 사용한다. GetBuffer로 버퍼 객체를 얻을 수 있다.

객체의 내부 상태에 따라 스스로 행동을 변경할 수 있게 허가하는 패턴으로, 이렇게 하면 객체는 마치 자신의 클래스를 바꾸는 것처럼 보인다.

 

7.2 유한 상태 기계의 일종인 플로 차트를 그리자

유니티의 애니메이터 컨트롤러는 상태 머신을 사용하여 애니메이션 클립 전환을 관리한다.

FSM의 특징

1. 가질 수 있는 상태가 한정된다. (점프, 엎드리기, 내려찍기 등)

2. 한 번에 한 가지 상태만 가질 수 있다. (점프와 동시에 엎드리기는 안된다.)

3. 입력이나 이벤트가 기계에 전달된다. (버튼 누르기와 버튼 떼기와 같은 입력)

4. 각 상태에는 입력에 따라 다음 상태로 바뀌는 전이(조건)가 있다. (현재 상태에 전이 조건이 없으면 어느 입력이 들어와도 무시한다.)

상태 패턴은 FSM을 구현하는 방법 중 하나이다.

 

7.3 상태 패턴을 구현 하는 방법 - 열거형과 다중 선택문

enum State{

  STATE_JUMPING,

  STATE_DUCKING,

}

입력이나 이벤트를 받는 부분에서는 상태에 따라 분기한 뒤에 입력에 따라 분기한다.

 

열거형만으로 기능을 충분히 구현할 수 있는 경우는 굳이 인터페이스를 쓰지 않아도 된다.

상태를 바꾸는데 기록해야 될 것들이 많고 복잡해지면 열거형 보다는 상태 별로 클래스를 정의하여 구현하는 방법이 있다.

7.4 인터페이스를 사용하여 구현하는 상태 패턴

1. 상태 인터페이스를 정의한다.

STATE_XXXXX를 사용하는 모든 코드 ( 상태에 의존하는 코드들, 예제에서는 handleInput()과 update() )들을 인터페이스의 가상 메서드로 만든다.

class HeroineState { // 상태 인터페이스
public:
  virtual ~HeroineState() {} 
  virtual void handleInput(Heroine& heroine, Input, input) {}
  virtual void update(Heroine& heroine) {}
};

2. 상태별 클래스를 만든다. (상태 인터페이스를 구현)

class JumpingState : public HeroineState {
public:
  JumpingState() : {}
  
  virtual void handleInput(Heroine& heroine, Input input){
    if(input == PRESS_DOWN) {
      heroine.setGraphics(IMAGE_DIVE);
      // 내려찍기 상태로 전환
  }
  
  virtual void update(Heroine& heroine){
    // jumpTime_을 update에서 감소시키고, jumpTime_이 0 보다 작아지면
    // heroine.state_에 StandingState를 대입하여 상태 전이를 구현할 수 있을 것이다.
  }
  
  private:
    int jumpTime_; // 점프 상태에서만 의미 있는 변수 (체공 시간?)
};

3. 대상(캐릭터)에 상태 객체 포인터를 추가한다.

class Heroine{
public:
  virtual void handleInput(Input input){
    state_->handleInput(*this, input);
  }
  
  virtual void update() { state_->update(*this); }
  
private:
  HeroineState* state_; // 상태 객체 포인터
};

ㄴ 상태를 바꾸려면 이 포인터에 상태별 클래스 객체를 할당하면 된다.

 

7.5 상태별 클래스 객체를 유지하는 두 가지 방법

1. 정적 객체

일반적으로 상위 상태 클래스에 정적 상태별 클래스 객체를 만들어 둔다.

 

2. 상태 객체 만들기

캐릭터 마다 점프력이 달라 체공시간을 다르게 두어야 할 수도 있기 때문에 정적 객체만으로 부족한 경우 사용하는 방법이다. 이 경우에는 상태를 전이할 때마다 상태 객체를 만들어야 한다.

이 방법은 매번 상태 객체를 할당하기 위해 new 키워드를 사용하여 메모리와 CPU를 사용해야 한다.

상태를 동적으로 할당하면서 메모리 단편화가 생길 수 있다.

void Heroine::handleInput(Input input) {
// 상태가 바뀌는 경우(상태를 바꾸는 입력이 들어온 경우) NULL이 아닌 상태 객체를 반환한다.
  HeroineState *state = state_->handleInput(*this, input); 
  if (state != NULL) {
    delete state_; // 기존 상태를 해제하고 새로운 상태를 가리키도록 한다.
    state_ = state;
  }
}


HeroineState* JumpingState::handleInput(Heroine& heroine, Input input) {
  if(input == PRESS_DOWN) {
    return new DivingState();
  }
  return NULL;
}

 

7.6 상태의 입장과 퇴장 함수

현재 상태에서 다음 상태로의 전이가 일어날 때 (state를 대입할 때) 다음 상태의 설정을 초기화 하는 것은 어떻게 보면 현재 상태에서 다음 상태를 설정하는 것이다.

 

상태 객체에 입장 함수 enter를 정의하여 설정을 초기화하는 코드를 넣고,  전이가 일어난 이후 enter를 호출하도록 하면

상태 객체는 캡슐화 되었다고 할 수 있다.

 

7.7 단점

인공지능같이 복잡한 곳에 적용하기 힘들다.

상태 전이 구조를 바꾸기 어렵다.

 

7.8 병행 상태 기계

두 종류의 상태를 유지해야 하는 경우, 두 상태 기계가 서로 전혀 연관이 없다면 이 방법을 사용할 수 있다.

class Heroine {
private:
  HeroineState* state_;
  HeroineState* equipment_;
};

7.9 계층형 상태 기계

서로 다른 여러 가지 상태에서 동일한 입력으로 하나의 상태로 전이되는 경우 단순하게 상태 기계를 구현한다면 상태 전이 코드를 상태마다 중복해 넣어야 한다.

 

계층형 상태 기계

상태를 상위층과 하위층으로 나누고, 하위 상태는 상위 상태를 상속받는 개념이다. 클래스 상속으로 구현할 수 있다.

 

7.10 푸시다운 오토마타

FSM에는 이력 개념(현재 상태는 알 수 있지만 직전 상태가 무엇인지를 따로 저장하지 않음)이 없기 때문에 이전 상태로 쉽게 돌아갈 수 없다.

예를 들면 서있는 상태나 엎드려있는 상태에서 총을 쏘면 FiringState로 전이하고 다시 원래의 상태로 돌아가야 한다고 하면 이전에 엎드려있는 상태였다면 엎드려있는 상태로, 서 있는 상태라면 서 있는 상태로 되돌아가야 한다.

 

푸시다운 오토마타에서는 상태를 스택으로 관리한다.

class Heroine {
public 
  void pushState(HeroineState* state) {}; // s에 새로운 상태를 넣는다.
  void popState() {}; // s에서 최상위 상태(현재 상태)를 빼는 함수를 정의해야 한다.
  						// 수행하면 이전 상태가 현재 상태가 된다.

private:
  // HeroineState* state_;
  // HeroineState* equipment_;
  
  std::stack<HeroineState*> s;
};

싱글턴 패턴은 오직 한 개의 클래스 인스턴스만을 갖도록 보장하고, 이에 대한 전역적인 접근점을 제공한다.

 

싱글턴 패턴의 특징

1. 오직 한 개의 클래스 인스턴스만 갖도록 보장

시스템에서 하나만 있어야 하는 객체는 GameManager나 파일 시스템 클래스와 같은 것들이 있다.

 

2. 전역 접근점을 제공 (전역 변수로써 기능)

class FileSystem {
  public:
    static FileSystem& instance() {
      // 게으른 초기화
      if (instance_ == NULL) {
        instance_ = new FileSystem();
      }
    return *instance_;
  private:
    FileSystem() {}
    static FileSystem* instance_;
};  


class FileSystem{
  public:
    static FileSystem& instance() {
      static FileSystem *instance = new FileSystem();
      return *instance;
    }
    
  private:
    FileSystem() {}
};

어디서든 필요할 때 마다 instance 함수로 클래스 인스턴스를 얻을 수 있다. (인스턴스 정보를 수정할 수 있다.)

FileSystem instance = FileSystem.instance()

 

3. 한 번도 사용하지 않는다면 아예 인스턴스를 생성하지 않는다.

ㄴ 싱글턴은 처음 사용될 때 초기화되므로 게임 내에서 사용되지 않는다면 초기화 되지 않는다. (메모리 사용량 감소)

 

4. 런타임에 초기화된다.

ㄴ 컴파일러에서 정적 변수 초기화 순서를 보장해주지 않기 때문에 정적 변수 사이 안전한 의존 관계를 만들 수 없는데 

게으른 초기화는 이런 문제를 해결해 준다.

 

5. 싱글톤을 상속하여 사용할 수도 있다.

ㄴ 싱글턴 객체가 플랫폼 별로 다르게 정의되어야 하는 경우 사용할 수 있음

 

FileSystem& FileSystem::instance() { // filesystem을 싱글턴으로 만든다.
#if PLATFORM == PLAYSTATION3
  static FileSystem *instance = new PS3FileSystem(); // FileSystem을 상속받는 ps3 전용 filesystem
#elif PLATFORM == WII
  static FileSystem *instance = new WiiFileSystem(); 
#endif
  return *instance;
}

단점

싱글턴 인스턴스는 어디에서든 접근 할 수 있기 때문에 전역 변수로써 기능한다.

= 전역변수의 단점

1. 멀티 스레딩 같은 동시성 프로그래밍에 알맞지 않다.

2. 전역 변수는 커플링을 조장한다.

3. 전역 변수는 코드를 이해하기 어렵다. (어디서든 상태를 변경할 수 있기 때문에 변경하는 부분을 찾기 어렵다.)

 

4. 싱글턴은 문제가 하나뿐일 때도 두 가지 문제를 풀려고 한다.

ㄴ 오직 한 개의 인스턴스를 가지는 기능과 전역 접근 기능 중 어느 한 가지 기능만 따로 사용할 수는 없다.

 

5. 게으른 초기화는 제어할 수 없다.

게임 런타임중, 오디오 시스템 초기화나 파일 시스템 초기화가 일어나면 프레임이 떨어질 수 있다.

게임에서는 메모리 단편화를 막기 위해 힙에 메모리를 할당하는 방식을 세밀하게 제어하는데, 이 때문에 힙 어디에 메모리를 할당할지를 제어할 수 있도록 적절한 초기화 시점을 찾아야 한다.

싱글턴 대신 정적 클래스를 사용하고 정적 함수를 사용하는 것이 더 간단할 수 있다. 이러면 instance() 함수를 호출할 필요도 없다. 클래스이름::함수() 로 바로 함수를 호출할 수 있다.

싱글턴의 대안

1. 한 개의 인스턴스만 보장하는 기능

ㄴ 인스턴스가 이미 생성되었는지 여부를 단언문으로 확인

ㄴ ex) assert(!instantiated_);

 

2. 인스턴스에 쉽게 접근하는 기능

ㄴ 방법 1. 객체를 필요로 하는 함수에 인수로 넘겨주기

 

ㄴ 방법 2. 상위 클래스로부터 얻기

상위 클래스에 객체를 반환하는 함수를 만든다. 함수를 protected로 선언하면 그 클래스를 상속받은 코드에서만 객체에 접근할 수 있게 된다.

 

ㄴ 방법 3. 이미 전역인 객체로부터 얻기

전역에서 접근할 수 있는 Game 클래스가 있다고 가정하면 Log, FileSystem, Audio, Player를 각각 싱글턴으로 만드는 대신

Game 객체 하나만 싱글턴으로 만들고, Log, FileSystem, Audio, Player는 Game 클래스의 멤버 객체로 만드는 방법을 사용할 수 있다.

 

프로토타입 패턴은 원형이 되는 인스턴스를 사용하여 생성할 객체의 종류를 명시하고, 이렇게 만든 견본을 복사해서 새로운 객체를 생성합니다.

 

5.1 몬스터 스포너

템플릿을 사용하여 해결하는 방법

class Spawner {
public:
  virtual ~Spawner() {}
  virtual Monster* spawnMonster() = 0;
};

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

//사용법
Spawner* ghostSpawner = new SpawnerFor<Ghost>();
Monster* monster_ghost = ghostSpawner->spawnMonster();

원형이 되는 인스턴스(Ghost)를 사용하여 생성할 객체(Spawner)의 종류를 명시하고 새로운 객체 (monster_ghost)를 생성한다.

+ Recent posts