ホーム < ゲームつくろー! < デザインパターン習得編
FlyWeight
〜ゲーム製作でおなじみのオブジェクト使い回し法
@ 使いまわしはゲームの基本
FlyWeightパターンは、「同じデータを使いまわす」というパターンです。これはゲーム製作では当たり前のように使われていて、とても良く親和します。
最近のSTGは「弾幕系」と言って、画面を埋め尽くさんかのごとく弾を吐かせるのが主流のようです。おおよそですが、1画面に数百の弾が出ています。それらの弾は、ほぼ同じ絵柄をしていますが、1つ1つの弾が別の位置にあって、しかるべき方向に飛んでいます。位置と方向は個々の情報ですが、絵柄は同じです。この時、すべての弾が絵柄までもハードコピーしていたとしたら、弾の数×弾の絵柄分のVRAMが消費されてしまいます。一方絵柄を共通メンバとして参照すると、絵柄のVRAMを占める割合は最小限に小さくなります。
このように、弾クラスの中には「共通するメンバ変数」と「個別のメンバ変数」が混在しています。「共通するメンバ変数」はintrinsic(固有の)状態、「個別のメンバ変数」はextrinsic(外部の)状態と呼ばれています。
オブジェクトの生成にはFacotry Methodパターンが良く使用されます。Factoryクラスは、要求があればそのオブジェクトを内部で生成して渡してくれます。FlyWeightパターンの場合、要求されたオブジェクトがまだ無い場合は新規に生成してポインタを渡しますが、すでに生成されているなら生成済みオブジェクトへのポインタを渡します。図示すると次のような感じです:
クライアントが「オブジェクトをくれ」と要求すると、Factoryオブジェクトは要求されたオブジェクトを検索します。もし、該当する物が無ければ、それを新規に生成保持し、クライアントにはそのポインタを渡します。もし同じものを持っていれば、そのままそのオブジェクトへのポインタを渡すだけです。
要点は、「クライアントは常にポインタをもらっている事に変わりない」という所です。すなわち、クライアントはFactory内部でオブジェクトが新しく生成されたのか、既存のオブジェクトなのかを意識しません。また、クライアントは「受け取ったのが共通するオブジェクトへのポインタである」ことを認識しています。よって、ポインタをたどって何か変更すると他のクライアントが持つオブジェクトにも影響を与えます。
A STGの弾を使いまわし
STGの弾オブジェクトでFlyWeightパターンの例を示しましょう。
今2つの弾の種類があるとします。1つは通常弾ですが、もう1つは破壊可能弾で耐久性があるとします。双方とも「弾」なのでBulletクラスから派生することにしましょう。
class Bullet
{
protected:
int m_ID; // 弾の種類をあらわすID(共有)
BITMAP* m_instBMP; // 弾の絵へのポインタ(共有)
POS m_Pos; // 弾の現在位置(個別)
bool m_isAlive; // 生きてる?
public:
void SetPict(BITMAP *pict); // 弾の絵を登録
int GetID(); // IDを取得
void SetPos( Pos& pos ); // 位置を設定
};
class NormalBullet : public Bullet
{
public:
NormalBullet(){
m_ID = ID_NORMAL; // ノーマル
}
};
class CanBreakingBullet : public Bullet
{
protected:
int m_Permanence; // 耐久力
public:
CanBreakingBullet(){
m_ID = ID_CANBREAKING; // 破壊可能弾
m_Permanence = 100; // 耐久力の初期値
}
}
ノーマル弾はIDをセットするだけ、破壊可能弾はIDとさらに耐久力を設定しています。ごく普通の設計ですね。次に、これらの弾を専門に生成するBulletFactoryクラスを作成します:
class BulletFactory
{
protected:
vector<Bullet*> m_BulletList;
pbulic:
Bullet* GetBullet( int bullet_id )
{
// 使える弾を検索
for( int i = 0; i < m_BulletList.size(); i++ ) {
if( m_BulletList[ i ]->GetID() == bullet_id && m_BulletList[ i ]->isAlive == false )
return m_BulletList[ i ]; // 使い回し弾
}
// 弾は全部使用中だったので新しい種類の弾を生成・保持
Bullet* p_newbullte = NULL;
switch( bullet_id )
{
case ID_NORMAL:
p_newbullte = new NormalBullet();
p_newbullte->SetPict( pNORMALBULLTEPICT ); //<- 節約(共通)部分
break;
case ID_CANBREAKING:
p_newbullte = new CanBreakingBullet()
p_newbullte->SetPict( pCANBREAKINGPICT ); //<- 節約(共通)部分
break;
}
if ( p_newbullet )
m_BulletList.pushback( p_newbullet );
return p_newbullte;
}
}
GetBullet関数は引数のIDに対応する弾オブジェクトへのポインタを返します。太文字部分に注目してください。引数のIDから、まずすでに生成されていて且つ未使用の弾を検索します。未使用の弾があれば、そのポインタを返して終わりです。弾がなければ通常のFactoryクラス同様に弾オブジェクトを生成します。そして、そのポインタをリストに格納します。
この例では、分かりやすくするために単純にし、アルゴリズムの最適化は行っていません。検索部分を工夫(ダブルリストなど)すればo(1)で未使用弾へのポインタを取得することが可能になります。STGの弾は大量に必要になるため、そういう最適化は非常に有効です。
FlyWeightパターンの関係図を示します。
例に挙げたSTGの弾オブジェクトの生成に照らし合わせると次のようになります。