Game Programming Design Patterns - 경량 패턴
게임에서 화면을 가득 채운 빽빽한 숲을 본 적 한 번쯤은 있을 것이다.
수천 그루가 넘는 나무들은 각각 수천 개의 폴리곤으로 이루어져 있는데, 이를 그리기 위해서는 이 데이터들을 CPU에서 GPU로 버스를 통해 전달해야 한다.
각 프레임마다 이 모든 객체들을 GPU로 전달하기에는 무리가 있다.
나무마다 필요한 데이터는 다음과 같다.
각 수천 그루의 나무가 공유하는 데이터는 파란색으로 표시한 mesh와 textures, 각 그루마다 다르게 조절할 수 있는 데이터는 보라색으로 표시한 Location과 Rotation, 크기, 높이, 음영 등이 될 것이다.
이 많은 데이터들을 모두 한 객체에서 전달하기에는 양이 너무 많다.
따라서 이를 앞에서 분류한 것과 같이 모든 나무가 사용하는 데이터 / 각 나무 객체마다 다른 데이터 두 가지로 분류한다.
class TreeModel {
private:
Mesh mesh_;
Texture bark_;
Texture leaves_;
};
class Tree {
private:
TreeModel* model_;
Vector position_;
double height_;
double thickness_;
Color barkTint_;
Color leafTint_;
};
모든 나무 객체에 동일한 메시와 텍스처를 올릴 필요가 없기 때문에 Tree 클래스에서 TreeModel 공유 객체를 참조해주기만 하면 된다.
Instanced Rendering
(인스턴스 렌더링)
GPU로 보내는 데이터의 양을 줄이기 위해서는 앞서 설명한 공유 데이터를 딱 한 번만 보내고 나무들의 인스턴스를 그릴 때 해당 공유 데이터를 사용할 수 있게끔 전달해주면 된다.
Direct3D, OpenGL 모두 인스턴스 렌더링을 지원해준다.
인스턴스 렌더링에서는 데이터 스트림이 두 개 필요하다.
1. 여러 번 렌더링이 되어야 하는 공유 데이터
2. 인스턴스의 목록, 인스턴스들이 다르게 보이기 위해 필요한 매개변수들
경량 패턴
(Flyweight Pattern)
앞서 설명한 예제와 같이 경량 패턴은 어떤 객체의 개수가 너무 많아서 좀 더 가볍게 만들고 싶을 때 사용하게 된다.
인스턴스 렌더링에서는 메모리 크기보다는 데이터들을 GPU 버스로 보내는 시간이 더 중요하지만, 경량 패턴과 개념은 같다.
경량 패턴은 객체 데이터를 두 종류로 나눈다.
1. 고유 상태 (intrinsic state), 자유 - 문맥 상태 (context - free state)
모든 객체가 공유하는 데이터
2. 외부 상태 (extrinsic state)
인스턴스별로 값이 다른 데이터
경량 패턴을 활용할 수 있는 예시에는 다양한 지형으로 이루어진 땅을 들 수 있다.
지형 종류에는 플레이어에게 영향을 주는 속성들이 있다.
대표적으로,
1. 해당 지형을 지날 때 플레이어의 이동 제한에 대한 이동 비용
2. 강이나 바다처럼 보트로 건널 수 있는 곳인지의 여부
3. 렌더링 시 사용할 텍스처
이 속성들은 각 지형별로 다른 값을 지니게 될 것이다.
[ World_Flyweight.h ]
#pragma once
#include "CoreMinimal.h"
enum Terrain
{
TERRAIN_GRASS,
TERRAIN_HILL,
TERRAIN_RIVER
};
/**
*
*/
class DESIGNPATTERNSTUDY_API World_Flyweight
{
private:
Terrain tiles_[10][10];
public:
World_Flyweight();
~World_Flyweight();
int GetMovementCost(int x, int y);
bool IsWater(int x, int y);
};
[ WorldFlyweight.cpp ]
// Fill out your copyright notice in the Description page of Project Settings.
#include "World_Flyweight.h"
World_Flyweight::World_Flyweight()
{
}
World_Flyweight::~World_Flyweight()
{
}
int World_Flyweight::GetMovementCost(int x, int y)
{
switch (tiles_[x][y])
{
case TERRAIN_GRASS:
return 1;
case TERRAIN_HILL:
return 3;
case TERRAIN_RIVER:
return 2;
default:
return 0;
}
}
bool World_Flyweight::IsWater(int x, int y)
{
switch (tiles_[x][y])
{
case TERRAIN_GRASS:
return false;
case TERRAIN_HILL:
return false;
case TERRAIN_RIVER:
return true;
default:
return false;
}
}
World는 지형을 tiles_ 2차원 배열을 통해 관리하게 될 것이고, 인덱스를 이용한 접근으로 해당 지형의 속성값 데이터를 얻을 수 있게 된다.
그러나 이 코드는 문제가 많다.
1. 이동 비용, 물 / 땅 여부 등등은 지형에 관한 데이터인데 함수 자체에서 하드 코딩되어 있다.
2. 같은 지형 종류에 대한 데이터의 정보가 여러 메서드에 나뉘어 있다.
이를 합쳐 Terrain 클래스 자체를 만드는 것이 더 좋다.
[ Terrain.h ]
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
/**
*
*/
class DESIGNPATTERNSTUDY_API Terrain
{
public:
Terrain(int movementCost, bool isWater, UTexture2D* texture)
: movementCost_(movementCost), isWater_(isWater), texture_(texture){}
~Terrain();
int getMovementCost() const {return movementCost_;}
bool isWater() const {return isWater_;}
const UTexture2D* getTexture() const {return texture_;}
// 경량 객체는 변경할 수 없도록 메서드를 const로 만든다.
// (같은 Terrain 객체를 여러 곳에서 사용하는데,
// 한 곳에서 값을 바꾸면 뜻하지 않는 결과가 나오게 되기 때문에)
private:
int movementCost_;
bool isWater_;
UTexture2D* texture_;
};
여기서 경량 패턴이 등장하는데, 이 Terrain 객체를 각 타일마다 모두 만들어주는 대신, 각 지형 별로 한 개의 공유 객체만 만들고, 해당 공유 객체를 참조하여 인스턴스들을 만드는 방법이다.
[ World_Flyweight.h ]
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Terrain.h"
//
// enum Terrain
// {
// TERRAIN_GRASS,
// TERRAIN_HILL,
// TERRAIN_RIVER
// };
/**
*
*/
class DESIGNPATTERNSTUDY_API World_Flyweight
{
private:
Terrain* tiles_[10][10];
public:
World_Flyweight()
: grassTerrain_(1, false, GRASS_TEXTURE),
hillTerrain_(3, false, HILL_TEXTURE),
riverTerrain_(2, true, RIVER_TEXTURE){}
~World_Flyweight();
void GenereateTerrain();
const Terrain& getTile(int x, int y) const
{
return *tiles_[x][y];
}
// int GetMovementCost(int x, int y);
// bool IsWater(int x, int y);
private:
Terrain grassTerrain_;
Terrain hillTerrain_;
Terrain riverTerrain_;
UTexture2D* GRASS_TEXTURE;
UTexture2D* HILL_TEXTURE;
UTexture2D* RIVER_TEXTURE;
};
World_Flyweight 의 생성자에서 Terrain의 공유 객체들을 만들어주고, 해당 객체들의 포인터를 이용해 tiles_를 채울 수 있게 된다.
int cost = world.getTile(2, 3).getMovementCost();
또한 하드코딩으로 구현하였던 각 지형 속성 값의 정보는 위 코드 처럼Terrain 객체에서 바로 얻을 수 있도록 클래스를 구성하였다.
※ 프로젝트
https://github.com/haram1117/GameDesignPatterns_Study
GitHub - haram1117/GameDesignPatterns_Study: GameDesignPatterns_Study
GameDesignPatterns_Study. Contribute to haram1117/GameDesignPatterns_Study development by creating an account on GitHub.
github.com
※ Flyweight Pattern 커밋
Flyweight Pattern · haram1117/GameDesignPatterns_Study@ce7f460
Show file tree Hide file tree Showing 13 changed files with 198 additions and 0 deletions.
github.com