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

 

싱글턴 패턴의 특징

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)를 생성한다.

 

감시자 패턴은 객체 사이에 일 대 다의 의존 관계를 정의해두어, 어떤 객체의 상태가 변할 때 그 객체에 의존성을 가진 다른 객체들이 그 변화를 통지 받고 자동으로 업데이트될 수 있게 만듭니다.

 

관찰차 패턴을 java에서는 java.util.Observer 라이브러리로 지원하고, C# 에서는 event 키워드로 지원한다.

4.1 업적 달성

업적의 종류가 보통 광범위하기 때문에 단순하게 생각한다면, 업적의 조건을 만족하게 되는 부분에 업적 달성 함수를 호출하게 된다면 코드가 지저분해질 것이다.

 

대상 객체는 자신을 관찰하는 모든 관찰자들 배열 (혹은 리스트)를 가지며, 특정 기준이나 사건 발생 시에 onNotify 함수를 호출시켜 알림을 모두에게 보낸다. 

순차적으로 onNotify 메서드를 호출하기 때문에 (동기적) 한 관찰자의 onNotify 반환이 늦으면 느려질 수 있다.

관찰자 포인터 배열을 사용하는 대신에 연결 리스트를 사용할 수도 있다.

공유를 통해 많은 수의 소립(fine-grained) 객체들을 효과적으로 지원하는 패턴

 

3.1 숲에 들어갈 나무들

나무들이 동일한 메시 정보(폴리곤, 정점), 텍스처를 사용한다면 이 특성들을 공유 데이터로써 GPU에 한 번만 보내야 한다.

3.2 수천 개의 인스턴스

나무 인스턴스를 그릴 때 공유 데이터를 사용하도록 한다.

 

Direct3D, OpenGL에서는 하드웨어적으로 인스턴스 렌더링을 지원한다.

https://lemonyun.tistory.com/57

 

16. 인스턴싱과 절두체 선별

인스턴싱 : 한 장면에서 같은 물체를 여러 번 그리는 것, 성능을 크게 향상할 수 있다. 절두체 선별 : 시야 절두체 바깥에 있는 일단의 삼각형들을 간단한 판정으로 골라내서 기각하는 기법 16.1

lemonyun.tistory.com

DirectX 12를 이용한 3D 게임 프로그래밍 입문 16장에서 다룬 인스턴싱에서는 아래와 같은 자료를 셰이더에 전달했다.

 

 

공유 데이터 : 재질 자료를 담은 구조적 버퍼, 텍스처 배열, 정점 버퍼 뷰, 인덱스 버퍼 뷰

개별 데이터 : 인스턴스별 자료 항목을 담은 구조적 버퍼 (material index, world matrix, textransform)

3.4 지형 정보 (경량 패턴 사용 예시)

맵이 격자 타일 형태라고 할때 

Terrain tiles_[WIDTH][HEIGHT] 형태의 배열을 사용하여 타일 정보를 관리할 수 있다.

ㄴ Terrain을 enum으로 사용하는 경우

ㄴ 지형 종류에 대한 데이터 (예를 들면 해당 지형에서의 기온)를 위한 함수가 별도로 필요함

 

지형 종류에 대한 데이터 캡슐화를 위해 Terrain을 클래스로 정의

하나씩 선언된 물, 풀, 용암에 대한 Terrain이 공유 데이터로써의 역할을 한다.

명령 패턴 - 간단하게 정리하자면 함수 호출을 매개변수화 한 것?

콜백 함수, 함수 포인터를 사용하여 구현할 수 있다.

 

2.1 입력키 변경

 

버튼을 눌렀을때 특정 메서드(행동)가 수행하도록 하는 코드가 있다면

 

1. 모든 행동을 실행하는 공통 상위 클래스 Command를 정의 (execute 함수가 있는 인터페이스)

2. 행동별로 상위 클래스를 상속받는 하위 클래스를 만들고 execute 함수를 정의 (execute에서 실제로 메서드를 호출)

3. 입력 핸들러 코드에는 버튼마다 상위 클래스 포인터를 저장

4. 입력 핸들러 초기화 시점에 버튼에 하위 클래스 객체를 바인드함

 

2.2 액터에게 지시하기

위의 예에서 execute 함수에 매개변수로 GameActor 객체를 받는다.

 

명령을 실행할 때 액터만 바꾸면 플레이어가 아닌 액터에 명령을 실행 할 수 있다.

플레이어와 같은 명령을 사용하는 AI 캐릭터가 있다면 적용할 수 있다.

 

Command 객체를 선택하는 부분 (입력 핸들러 코드)과 액터(누가 명령을 수행하는지)를 디커플링함으로써 코드가 유연해진다.

 

2.3 실행취소와 재실행

Command 클래스에 가상 함수 Undo()를 정의한다.

 

어떤 일을 하는지를 정의한 명령 객체(Command를 상속받은)를 반복해서 생성한다.

그 명령 객체는 이전 명령의 상태를 저장할 수 있어야 한다. (execute() 함수에서 현재 상태를 이전 상태에 기록하고 상태를 변경해야 한다.)

 

Undo() 함수는 현재 상태를 이전 상태로 바꿔야한다.

 

이전 명령의 상태를 저장하고 있어야 하므로 Command 자료형을 저장할 수 있는 배열이나 스택같은 곳에 명령 객체를 동적으로 생성해 저장하고 있어야 한다. (버튼 등의 입력으로 명령을 수행할 때 마다)

 

이전 상태로 돌아가기 위해서는 배열이나 스택의 포인터를 이동해가면서 포인터가 가리키는 곳의 명령 객체의 Undo() 함수를 호출하면 된다. 

 

재실행은 게임에서 잘 쓰이지 않을 수도 있지만, '리플레이'는 게임에서 자주 쓰인다.

매프레임마다 전체 게임 상태를 저장하는 대신 전체 개체가 실행하는 명령 모두를 매 프레임 저장한 뒤 리플레이 할때 저장한 명령들을 순서대로 실행해 게임을 시뮬레이션한다.

 

1.1 좋은 소프트웨어 구조란?

코드를 얼마나 쉽게 변경할 수 있느냐가 코드 설계를 평가하는 척도가 된다.

 

코드를 고치려면 고치려는 부분의 기존 코드를 이해해야 하기 때문에 작업에 관련된 코드의 양을 줄여야 한다.

이 관점에서 보면 커플링이 적은 코드가 곧 좋은 소프트웨어 구조를 만든다.

 

하지만 지나치게 확장성에 신경쓰다 보면 추상화를 위한 보조 코드가 더 많아져 실제 작업 코드를 찾기가 어려워질 수도 있고, 좋은 구조를 유지하는데에도 꾸준한 노력이 필요하기 때문에 비용이 추가적으로 발생할 수도 있다.

 

1.3 성능과 속도

프로그램의 유연성과 성능은 반비례 관계에 있다.

코드를 유연하게 만드는 많은 패턴이 가상함수, 인터페이스, 포인터, 메세지 같은 메커니즘에 의존하는데, 다들 어느정도의 런타임 비용을 요구하기 때문이다.

하지만 유연성이 좋아야 게임을 쉽게 변경할 수 있고, 개발 속도가 빨라진다.

 

최적화 기법은 구체적인 제한을 선호한다. 

ㄴ 인터페이스나 가상함수의 사용을 줄인다.

ㄴ 성능상 비용이 줄어든다.

 

처음에는 코드를 유연하게 유지하다가 기획이 확실해진 다음에 추상 계층을 제거하여 최적화를 진행하는 타협안이 있다.

 

1.4 나쁜 코드의 장점

기획 확인이 필요할때, 필요한 기능만 대강 돌아가도록 하는 코드는 적절하다.

이렇게 만든 버릴 코드는 확실히 버릴 수 있어야 한다.

 

23.1 뼈대 좌표계들의 계통구조

캐릭터의 골격은 계통구조로 만들어진다. 예를들면 팔은 상박, 하박 손으로 이루어지고 이들은 부모 자식 관계이기 때문에부모가 회전하면 자식들도 회전한다.

 

각 뼈대(상박, 하박, 손)의 기하구조는 자신의 국소 좌표계와 관절로 모형화된다.

물체의 회전을 편하게 하기 위해 관절은 물체 국소 좌표계의 원점에 둔다.

 

사람의 팔을 예로 들면 상박(위팔)이 하박(아래팔)의 부모가 된다.

 

23.2 메시 스키닝

결속 공간(전체 표피가 정의된 국소 좌표계) ----(오프셋 변환)----> (뼈대 국소 공간)

스키닝에서는 각 뼈대의 뿌리변환 (뼈대 국소 공간 -> 뿌리 공간)을 구한다.

 

뿌리변환 = 부모변환 * 부모의 뿌리변환으로 정의하기 때문에 하위 뼈대의 뿌리변환을 얻기 위해서는 부모의 뿌리변환이 반드시 정의되어 있어야한다. 그렇기 때문에 트리를 하향식으로 운영하여 부모의 뿌리변환이 항상 존재하도록 한다. 상향식으로 하게 되면 공통의 조상을 공유하는 뼈대들에 대해 동일한 행렬 곱셈을 중복해서 수행해야 한다.

 

23.3 정점 혼합

정점 혼합 : 골격을 감싸는 표피의 정점들을 애니메이션 하는 방법을 위한 알고리즘 

표피는 연속적인 메시인데 관절 같은 부위의 정점은 표피의 한 정점에 영향을 주는 뼈대가 여러 개일 수 있다. 이 경우 정점의 최종 위치는 영향을 주는 뼈대들의 최종 변환들의 가중 평균으로 결정된다. 이런 방식으로 정점을 혼합하면 관절 주변에서 정점들이 매끄럽게 전이되어서 적당히 탄력있는 모습의 표피가 만들어진다.

'읽은 책 > DirectX 12를 이용한 3D 게임 프로그래밍 입문' 카테고리의 다른 글

22. 사원수 (quaternion)  (0) 2022.07.11
21. 주변광 차폐  (0) 2022.07.10
20. 그림자 매핑  (0) 2022.07.08
19. 법선 매핑  (0) 2022.07.04
18. 입방체 매핑  (0) 2022.07.02

사원수를 쓰는 이유

x y z 축을 순서에 따라 회전시키게 되면 축이 겹치게 되는 현상(gimbal lock)이 생기기 때문

 

복소수와 사원수

특별한 사원수의 곱

사원수 곱셈은 교환법칙을 만족하지 않지만 결합법칙은 만족한다.

사원수 곱셈의 항등원 e = (0, 0, 0, 1)이다. (항등 사원수)

사원수 곱셈은 덧셈에 대한 분배법칙을 만족한다.

 

실수 s = (0, 0, 0, s)

벡터 u = (x, y, z, 0)

 

22.2.5 사원수의 켤레와 사원수의 크기

사원수 q = (q1, q2, q3, q4) = (u, q4)의 켤레를 q*로 표기한다.

q* = (-q1, -q2, -q3, q4) = (-u, q4)

 

켤레의 성질

1. (pq)* = q*p*

2. q와 q*를 더하면 실수가 된다. (0, 0, 0, 2q4)가 되므로

3. q와 q*의 곱은 사원수 크기의 제곱(실수)이다.

4. q q* = q* q = || q ||² = 사원수 q의 크기의 제곱

22.2.6 역 사원수

사원수 곱셈의 항등원 (0, 0, 0, 1)을 만드는 역 사원수

크기가 1인 단위사원수의 경우 역 사원수 =  켤레 사원수가 된다.

 

22.2.7 극형식

단위 사원수 q = (q1, q2, q3, q4) = (u, q4)에 대하여 ||u||² + q4² = 1 이다.

u와 같은 방향의 단위벡터를 n이라고 하면 u = sinθn 로 나타낼 수 있고, sin²θ n² + cos²θ = 1이기 때문에

q = (sinθn, cosθ)로 나타낼 수 있다.

 

θ 대신에 -θ를 대입할 경우 (n sin(-θ), cosθ) -> (-n sinθ, cosθ) = q *(켤레 사원수)

 

 

22.3 단위 사원수와 회전

회전 연산자

순사원수 p =(v, 0) (v = 3차원 점 또는 벡터)를 축 n에 대해 θ각도 만큼 회전시킨 사원수

 

q와 -q가 같은 결과를 반환하는데 이는 회전 방향의 차이이다.

 

회전행렬을 사원수 회전 연산자로 변환할 수 있고 사원수 회전 연산자를 회전행렬으로 변환할 수 있다.


22.4 사원수 보간

단위 사원수는 4차원 단위구 구면에 놓인 4차원 단위벡터이다.

 

b와 -b가 기하적으로는 같은 회전이지만, 4차원 단위구에서는 반대 방향을 뜻하고 

slerp(a, b, t)와 slerp(a, -b, t)는 짧은 호를 따라 보간될 것인지 긴 호를 따라 보간될 것인지를 결정한다.

||a - b||²와 ||a + b||² 중 ||a - b||²이 더 작은경우에는 a, b를 사용하는 것이 짧은 호를 따라 보간된다는 뜻이다.

a, -b를 사용하면 긴 호를 따라 보간된다.

 

연습문제

 

'읽은 책 > DirectX 12를 이용한 3D 게임 프로그래밍 입문' 카테고리의 다른 글

23. 캐릭터 애니메이션  (0) 2022.07.12
21. 주변광 차폐  (0) 2022.07.10
20. 그림자 매핑  (0) 2022.07.08
19. 법선 매핑  (0) 2022.07.04
18. 입방체 매핑  (0) 2022.07.02

+ Recent posts