[게임 프로그래밍 패턴] Chapter3 경량

Jangmanbo·2023년 8월 16일
0

1. 숲에 들어갈 나무들

수많은 나무들이 숲을 이루고 있는 장면은 경량 패턴으로 구현한다.

수천 그루의 나무(각 수천 폴리곤)를 표현하기 위해서는 메모리와 CPU->GPU 전달량이 충분해야 한다.
여기서 나무의 데이터에는 메시, 텍스처, 위치, 높이 등이 있을 것이다.

class TreeModel {
private:
	Mesh mesh_;
    Texture bark_;
    Texture leaves_;
};

class Tree {
private:
	TreeModel* model_;
    
    Vector position_;
    double height_;
    double thickness_;
}

숲의 나무들은 다 비슷해보이므로, 모든 나무를 같은 메시와 텍스처로 표현하도록 하면 나무 객체의 메시, 텍스처 등은 인스턴스마다 동일하게 된다.
각 나무 인스턴스가 TreeModel 객체를 참조하도록 하면 메모리를 아낄 수 있다.


2. 수천 개의 인스턴스

다음으로는 렌더링을 위해 데이터를 CPU->GPU로 보내는 데이터 양을 최소화해야 한다.

  • 공유 데이터(TreeModel)를 딱 한 번만 전달
  • 인스턴스마다 다른 데이터(위치, 높이 등) 전달
  • 나무 인스턴스를 그릴 때 공유 데이터를 사용하도록 명시

이러한 기능은 이미 인스턴싱으로 지원하고 있다.


3. 경량 패턴

경량 패턴은 객체의 개수가 너무 많을 때 가볍게 하기 위해 사용한다.

객체 데이터는 고유 상태(=자유 문맥)과 외부 상태로 나눌 수 있다.

  • 고유 상태: 모든 객체의 데이터값이 동일해서 공유 가능한 데이터
  • 외부 상태: 인스턴스별로 값이 다른 데이터

4. 지형 정보

이번엔 땅을 표현해보자!

enum Terrain {
	TERRAIN_GRASS,
    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;
    }
}

위 코드의 문제점
1. 이동 비용, 물 여부 등 지형에 관한 데이터를 하드코딩
2. 지형 종류에 대한 데이터가 여러 메소드(getMovementCost, isWater)로 나뉘어 있음

-> 지형 데이터를 캡슐화하자!

공유 데이터를 캡슐화

class Terrain {
public:
	Terrain(int movementCost, bool isWater, Texture texture)
    : movementCost_(movementCost),
    isWater_(isWater),
    texture_(texture) {}
    
    int getMovementCost() {return movementCost_;}
    bool isWater() const {return isWater_;}
    const Texture& getTexture() const {return texture_;}
    
private:
	// 고유 상태
	int movementCost_;
    bool isWater_;
    Texture texture_;
};

지형 데이터를 캡슐화를 위해 지형 클래스 선언

Terrain grassTerrain_;
Terrain hillTerrain_;
Terrain riverTerrain_;

각 지형별로 객체는 하나씩만 있으면 되므로 World클래스에 각 지형을 나타내는 Terrain인스턴스를 만든다.

Terrain* tiles_[WIDTH][HEIGHT];

그리고 각 타일이 해당 지형의 인스턴스 포인터를 갖게 한다.

const Terrain& World::getTile(int x, int y) const {
	return *tiles_[x][y];
}

int cost = world.getTile(2, 3).getMovementCost();	// getMovementCost는 Terrain의 메소드

이렇게 해당 타일의 지형 정보를 Terrain 참조자로 리턴하면, 전과 달리 해당 타일의 지형 정보를 World의 메서드가 아닌 Terrain 메서드에서 얻게 된다.

즉, World 클래스와 지형의 정보가 디커플링되었다.


5. 성능에 대해서

경량 패턴이 열거형보다 느릴 거란 걱정을 하기 전에,
열거형을 선언해서 수많은 switch문을 만들기보다는 일단 경량 패턴을 사용해보자

0개의 댓글