플라이웨이트(Flyweight) 패턴 for Game (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];
}