코승호딩의 메모장

[경량 패턴] 본문

디자인 패턴/게임 프로그래밍 패턴

[경량 패턴]

코승호딩 2023. 9. 26. 23:29

경량 패턴은 공유를 통해 많은 소립 객체들을 효과적으로 지원한다. 나무들로 화면을 가득 채운 숲에서는 GPU에 전달해야 하는 몇백만 개의 폴리곤이 필요할 것이다. 또한 나무에 필요한 데이터는 크기도 크고 숫자도 많다. 이러한 나무들은 대부분 비슷하기 때문에 모든 나무가 다 같이 사용하는 데이터를 뽑아 새로운 클래스에 모을 수 있을 것이다.

class TreeModel {
private:
	Mesh mesh;
	Texture bark;
	texture leaves;
}

class Tree {
private:
	TreeModel* model;
	
	Vec3 Position;
	float height;
	float thickness;
	Color barkTint;
	Color leafTint;
}

게임 내 같은 메시와 텍스쳐를 여러 번 메모리에 올릴 필요가 없기 때문에 TreeModel은 하나면 충분하다. 각 나무 인스턴스는 TreeModel을 참조하기만 하면 된다.

 

Direct3D나 OpenGL 모두 인스턴스 렌더링을 지원한다. 이들 API에서는 데이터 스트림이 두 개 필요한데, 첫 번째 스트림에서는 메시나 텍스쳐와 같은 공유 데이터가 들어가고 두 번째 스트림에서는 인스턴스 목록과 각각 다르게 보이기 위해 필요한 매개변수들이 필요하다. 

 

이처럼 경량 패턴은 객체의 수가 너무 많아서 가볍게 만들고 싶을 때 사용한다. 이 책에서는 데이터 값이 같아 공유할 수 있는 데이터를 자유 문맥 상태라 부르고 인스턴스별로 값이 다른 것을 외부 상태라고 부른다. 


보통 게임에서 풀, 흙, 언덕, 호수 등과 같은 다양한 지형을 땅에 붙일 것이다. 여기에서 땅은 타일 기반으로 만든다고 할 때 즉, 땅은 작은 타일들이 모여 있는 거대한 격자인 셈이다. 지형 종류에는 다양한 속성이 있을 수 있는데 예를 들어 어느 지형에서는 플레이어의 속도가 느리거나 빠를 수 있고 어느 지형에서는 보트로만 건너야할 수 있을 것이다. 그 밖의 텍스쳐 또한 속성이 될 것이다. 이들 속성을 지형 타일마다 따로 저장하기에는 너무 느릴 것이다. 따라서 지형 종류에 열거형을 사용하는 것이 일반적이다.  

enum Terrain {
    TERRAIN_GLASS,
    TERRAIN_HILL,
    TERRAIN_RIVER
    //...
}

class World {
private:
    Terrain tiles[WIDTH][HEIGHT];
}

이렇게 월드를 거대한 격자로 관리할 수 있을 것이다. 그리고 타일 관련 데이터는 다음과 같이 뽑을 수 있을 것이다.

int World::GetMovementCost(int x, int y) {
    switch (tiles[x][y]) {
    	case TERRAIN_GRASS: return 1;
    	case TERRAIN_HILL: return 3;
    	case TERRAIN_RIVER: return 2;
    	//...
}
bool World::IsWater(int x, int y) {
    switch (tiles[x][y]) {
    	case TERRAIN_GRASS: return false;
    	case TERRAIN_HILL: return false;
    	case TERRAIN_RIVER: return true;
    	//...
}

위 코드와 같이 데이터를 뽑아올 수 있긴 하지만 어딘가 지저분하고 이동 비용과 물인지 여부는 지형에 관한 데이터인데World에서 뽑아오도록 하드코딩 되어 있다. 이런 데이터는 하나로 합쳐 캡슐화하는 것이 좋다. 

 

class Terrain {
public:
	Terrain(int movementCost, bool isWater, Texture texture)
	: mMovementCost(movementCost), mIsWater(isWater), mTexture(texture) { 
	}

	int GetMovementCost() const { return mMovementCost; }
	bool IsWater() const { return mIsWater; }
	const Texture& GetTexture() const { return mTexture; }

private:
	int mMovementCost;
	bool mIsWater;
	Texture mTexture;
}

하지만 이렇게만 하면 타일마다 하나씩 만들어야 하는 비용이 든다. 굳이 Terrain 객체가 여러 개 있을 필요 없이 지형에 들어가는 모든 풀 밭 타일은 전부 동일하다. 즉, World 클래스 격자 멤버 변수에 열거형이나 Terrain 객체 대신 Terrain 객체의 포인터를 넣을 수 있다.

 

class World {
public:
    World()
    : mGrassTerrain(1, false, GRASS_TEXTURE),
    : mHillTerrain(3, false, HILL_TEXTURE),
    : mRiverTerrain(2, true, RIVER_TEXTURE) { 
    }
    
    void GenerateTerrain() { 
        for (int x = 0; x < WIDTH; ++x){
            for (int y = 0; y < HEIGHT; ++y) {
                if (random(10) == 0) mTiles[x][y] = &mHillTerrain;
                else mTiles[x][y] = &mGrassTerrain;
                }
            }
      int x = random(WIDTH);
      for (int y = 0; y < HEIGHT; ++y)
      	mTiles[x][y] = &mRiverTerrain;
     }
    
    const Terrain& GetTile(int x, int y) const { return *mTiles[x][y]; }
    
private:
    Terrain mGrassTerrain;
    Terrain mHillTerrain;
    Terrain mRiverTerrain;
    Terrain* mTiles[WIDTH][HEIGHT];
};

지형 종류가 같은 타일들은 모두 같은 Terrain 인스턴스 포인터를 갖게 되는 것이다. Terrain의 인스턴스가 여러 곳에서 사용되기 때문에 동적으로 할당하면 생명주기를 관리하기 어렵다 따라서 World 클래스에 저장해놓는다. 이런식으로 땅을 채우고 지형의 속성 값을 World의 메서드가 아닌 Terrain 객체에서 바로 얻을 수 있다. World 클래스는 더 이상 지형의 세부 정보와 커플링 되는 것이 아니다. int cost = world.GetTile(2, 3).GetMovemetCost(); 를 통해 타일 속성도 바로 얻을 수 있다.

'디자인 패턴 > 게임 프로그래밍 패턴' 카테고리의 다른 글

[관찰자 패턴]  (0) 2023.09.27
[명령 패턴]  (0) 2023.09.25
[게임 프로그래밍 패턴]  (0) 2023.09.25