프로토타입(Prototype) 패턴 for Game (C++)

|

Prototype 패턴

프로토타입(Prototype) 패턴은 인스턴스를 복제하여 새로운 객체를 생성해내는 방식의 디자인 패턴입니다.

예제

스타크래프트에서 저글링을 생성하는 ‘스포닝풀(Spawning Pool)’을 생각해봅시다. 각 Unit들을 클래스로 표현하면 다음과 같습니다.

class Unit {
  // Implementation
};

class Zergling: public Unit {};
class Hydralisk: public Unit {};
class Mutalist: public Unit {};

이런 경우 각각의 Unit을 생산하는 스포닝풀은 다음과 같이 ‘ZerglingSpawningPool’, ‘HydraliskSpawningPool’,’MutalistSpaswningPool’이 필요하게 됩니다.

class SpawningPool {
 public:
  virtual ~SpawningPool();
  virtual Unit *spawn() = 0;
};

class ZerglingSpawningPool: public SpawningPool {
 public:
  virtual Unit *spawn() {
    return new Zergling();
  }
};
class HydraliskSpawningPool: public SpawningPool {
  // Implementation
};
class MutalistSpawningPool: public SpawningPool {
  // Implementation
};

각 클래스마다 Unit과 SpawningPool이 필요하다보니 클래스 개수도 많아지며, 중복되는 코드도 많아집니다 .여기에 프로토타입 패턴을 적용하면 코드를 좀 더 깔끔하게 관리할 수 있게 됩니다.

프로토타입 패턴은 어떤 객체가 자기와 비슷한 객체를 만들어낼 수 있는 패턴입니다. 현재 예제에서는 어떤 Unit이든 자신과 비슷한 객체로부터 생성을 해낼 수 있게 됩니다.

프로토타입 패턴을 적용한 코드는 다음과 같습니다.

class Unit {
 public:
  virtual ~Unit() {}
  virtual Unit *clone() = 0;
};

class Zergling: public Unit {
 public:
  Zergling(int hp, int atk, int spd) : mHp(hp), mAtk(atk), mSpd(spd) {}

  virtual Unit *clone() {
    return new Zergling(mHp, mAtk, mSpd);
  }

 private:
  int mHp;
  int mAtk;
  int mSpd;
};

class SpawningPool {
 public:
  SpawningPool(Unit *prototype) : mPrototype(prototype) {}
  Unit *spawn() {
    return mPrototype->clone();
  }

 private:
  Unit *mPrototype;
};

프로토타입 패턴은 프로토타입의 클래스 뿐만 아니라 상태까지 같이 복제를 한다는 장점이 있습니다.

각 유닛들의 스포닝풀은 다음과 같은 코드로 만들 수 있습니다.

Zergling* zerglingPrototype = new Zergling(35, 40, 2);
SpawningPool* zerglingSpawingPool = new SpawningPool(zerglingPrototype);


프로토타입의 단점

위와 같은 코드들을 통해 각 유닛당 SpawningPool을 만들 필요는 없어졌습니다. 하지만, 각 유닛마다 clone() 메소드를 구현해야 하고, 코드의 양이 크게 줄어들지도 않습니다.

또한, 복제를 할 때 주소 값들이 가리키고 있는 값들도 모두 복사하는 ‘Deep Clone’을 할 것인지 또는 단순히 주소 값만 복사를 하는 ‘Shallow Clone’을 할 것인지도 명확하지 않습니다.

MIME Content Types

|

일반적으로 미디어 종류를 분류하는 Content Type입니다.

Content Type에 설정할 수 있는 이 값들을 MIME(Multipurpose Internet Mail Extensions) 타입이라고 하며, 다음과 같은 값들을 가질 수 있습니다.


Content Type

  • text/plain : 평범한 텍스트
  • text/html : HTML 문서
  • text/xml : XML 문서
  • text/css : CSS 문서
  • image/gif : GIF 이미지 파일
  • image/jpeg : JPEG 이미지 파일
  • image/png : PNG 이미지 파일
  • video/mpeg : MPEG 비디오 파일
  • video/quicktime : QuickTime 비디오 파일
  • audio/mp3 : MP3 음악 파일
  • application/msword : MS WORD 문서 파일
  • application/pdf : PDF 문서 파일
  • application/zip : ZIP 압축 파일

여기에 예시로 든 값 외에도 많은 종류의 MIME 타입들이 있으며, MIME 타입에 대한 자세한 설명은 여기에서 확인할 수 있습니다.

SW 품질속성 6가지

|

품질 속성(QA : Quality Attribute)은 시스템이 이해 당사자의 요구 사항을 얼마나 잘 만족시키는지를 나타내기 위해 사용하며, 측정하거나 테스트할 수 있는 시스템의 특성을 의미합니다.

품질 속성은 보통 다음과 같이 6개의 요소로 구성됩니다.

image


Stmulus(자극)

자극은 시스템에 도달하는 이벤트를 말합니다. 사용자의 명령이 될 수도 있고, 보안적인 측면에서는 외부로부터의 공격이 될 수도 있습니다.


Source of Stimulus(자극원)

자극원은 자극의 원인입니다. 자극원은 시스템이 처리하는 방식에 영향을 줄 수 있습니다. 예를 들면, 신뢰할 수 있는 사용자로부터의 요청과 신뢰할 수 없는 사용자로부터의 요청은 서로 다르게 수행될 것입니다.


Response(반응)

시스템이 자극에 반응하는 방법입니다.


Response Measure(반응 측정)

반응에 대한 측정입니다. 반응이 요구 사항을 만족시켰는지 여부를 결정하며, Latency(지연 시간)나 Throughput(산출량) 등이 측정 요소가 될 수 있습니다.


Environment(환경)

시나리오가 발생하는 상황입니다. 자극은 특정한 조건에서 발생하며 환경은 자극을 가하는 역할을 합니다.


Artifact(대상)

대상은 시스템을 말합니다. 전체 시스템이 될 수도 있고 특정 부분이 될 수도 있습니다. 예를 들면 데이터 저장소(Data Store)와 메타 데이터 저장소(Metadata Store)에서의 오류는 다르게 처리되어야 할 것입니다.

예제

가용성이라는 시나리오 기반으로 각 요소들을 생각하면 다음과 같습니다.

image

Android 폰으로 USB 테더링하기

|

Android 폰으로 USB 테더링하기

예전에는 별도의 세팅없이 안드로이드 폰으로 USB 테더링이 가능했는데, 어느 순간부터 USB 테더링이 안되도록 변경되었습니다. 그래서 별도의 프로그램을 설치해주어야 USB 테더링을 할 수 있습니다.


프로그램 다운로드

해당 프로그램은 여기에서 다운받을 수 있습니다. 최신 버전으로 다운받도록 합시다. 현재 최신 버전은 여기에서 받을 수 있습니다.


Mac 재부팅

그런 다음, Mac을 재부팅시켜줍니다. 그 이후 안드로이드 폰에서 설정에서 USB 테더링을 켜주면 USB 테더링을 사용할 수 있게 됩니다.

플라이웨이트(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];
}