게임 개발/디자인 패턴

Game Programming Design Patterns - 경량 패턴

꿀꺽람 2022. 8. 2. 21:06
반응형

게임에서 화면을 가득 채운 빽빽한 숲을 본 적 한 번쯤은 있을 것이다. 

수천 그루가 넘는 나무들은 각각 수천 개의 폴리곤으로 이루어져 있는데, 이를 그리기 위해서는 이 데이터들을 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 커밋

https://github.com/haram1117/GameDesignPatterns_Study/commit/ce7f4606e5d2544b44e7ad1cf7f62e37ba97b50e

 

Flyweight Pattern · haram1117/GameDesignPatterns_Study@ce7f460

Show file tree Hide file tree Showing 13 changed files with 198 additions and 0 deletions.

github.com

 

반응형