플라이웨이트(Flyweight) 패턴 for Game (C++)
15 Feb 2017 | 디자인패턴 C++Flyweight 패턴
플라이웨이트(Flyweight) 패턴은 객체간 동일한 정보나 유사한 정보들을 같이 공유(Sharing)해서 메모리 사용량을 최소화하는 패턴입니다. 게임에서 나무나 관객 등의 배경을 표현할 때 많이 사용되는 패턴입니다.
나무를 표현하는 클래스
숲의 나무를 표현한다고 할 때, 다음과 같은 데이터들이 필요합니다.
- 줄기, 잎 등을 표현하는 폴리곤 메시(Mesh)
- 나무 껍질과 입사귀 텍스처(Texture)
- 나무의 높이와 굵기
- 각각의 나무들의 색상을 조금씩 다르게 하기 위한 색의 농도(Tint)
클래스로 표현하면 다음과 같습니다.
class Tree { private: Mesh mMesh; Texture mBark; Texture mLeaves; Vector mPosition; double mHeight; double mThickness; Color mBarkColor; Color mLeavesColor; };
데이터 종류도 많지만, 메시나 텍스처의 경우 데이터 크기도 큽니다. 이런 나무를 수십, 수백 그루를 표현해야 할 때, 메모리 소모나 성능적인 면에서 한계가 있습니다.
메모리 사용량을 줄이기 위한 방법
특히 메모리를 많이 사용하면서, 공통적인 부분들을 추출하여 하나의 클래스로 만들 수 있습니다. 예를 들어, 다음과 같은 ‘TreeModel’ 클래스를 만들 수 있습니다.
class TreeModel { private: Mesh mMesh; Texture mBark; Texture mLeaves; };
기존 ‘Tree’ 클래스는
class Tree { private: TreeModel *mTreeModel; Vector mPosition; double mHeight; double mThickness; Color mBarkTint; Color mLeavesTint; };
가 됩니다. 나무가 아무리 많더라도 ‘TreeModel’ 클래스는 하나이기 때문에 메모리 사용량은 확실히 줄어듭니다.
지형을 표현하는 방법
조금 예제를 바꾸어서 이번에는 지형을 표현하는 방법입니다. 지형 정보에는 다음과 같은 정보들이 필요합니다.
- 화면에 렌더링할 때 필요한 텍스처
- 이동할 때 드는 비용(Cost)
- 물인지 육지인지
코드로 표현하면 다음과 같습니다.
enum Terrain { TERRAIN_GRASS, TERRAIN_HILL, TERRAIN_RIVER, };
class World { private: Terrain mTiles[WIDTH][HEIGHT]; }; int World::getMovementCost(int x, int y) { switch (mTiles[x][y]) { case TERRAIN_GRASS: return 1; case TERRAIN_HILL: return 2; case TERRAIN_RIVER: return 3; } } bool World::isWater(int x, int y) { switch (mTiles[x][y]) { case TERRAIN_GRASS: return false; case TERRAIN_HILL: return false; case TERRAIN_RIVER: return true; } }
문제없이 돌아가는 코드이지만 지저분하게 구현되어 있습니다. 데이터와 함수들이 분리되어 있어서 응집도도 떨어지기 때문에 이런 경우는 ‘지형 클래스’를 하나 별도로 구현하는 것이 좋습니다.
class Terrain { public: Terrain(int movementCost, 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; };
그리고 ‘World’ 클래스도 각 타일마다 ‘Terrain’ 인스턴스를 하나씩 가지도록 하는 것이 아니라 객체 포인터를 가지게 하여 중복되는 데이터를 공유해서 사용할 수 있도록 하는 편이 바람직합니다.
class World { private: Terrain *mTiles[WIDTH][HEIGHT]; };
Terrain 인스턴스의 생명 주기를 좀 더 쉽게 관리하기 위해서 ‘World’ 클래스를 다음과 같은 코드로 수정합니다.
class World { public: World() : mGrassTerrain(1, false, TEXTURE_GRASS), mHillTerrain(2, false, TEXTURE_HILL), mRiverTerrain(3, true, TEXURE_RIVER) {} const Terrain &getTile() const; private: void generateTerrain(); Terrain *mTiles[WIDTH][HEIGHT]; Terrain mGrassTerrain; Terrain mHillTerrain; Terrain mRiverTerrain; }; void World::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 &World::getTile(int x, int y) const { return *mTile[x][y]; }