ホーム < ゲームつくろー! < ゲーム製作技術編 < オブジェクト管理付きファクトリクラスの実装

その4 オブジェクト管理付きファクトリクラスの実装

 タスクが初期化をする時、オブジェクトを外注します。外注するのはオブジェクトの共有が必要なためです。タスクが要求したオブジェクトを作成し、そのポインタを渡す役目をするのが「生成者(ファクトリクラス)」です。生成者はただオブジェクトを作るだけでなく、それを管理してタスクが共有を望んでいるならば共有オブジェクトを渡します。

 この章ではそんな生成者をどう実装するか考えてみることにしましょう。



@ 生成物管理のイメージはこんなです

 まずは、ファクトリクラスがどのような形式でオブジェクトをストックするか図にしてみました:


 生成者は「オブジェクトID」で自分が作成したオブジェクトを識別します。このIDを与えるのはタスクです。あるキャラクタは生成者Aが作る事のできるオブジェクトを複数使いたい事があります。そのために1つのIDに複数のオブジェクトを登録できるようにします。上では通し番号になっていますが、配列として扱うのは面倒なので任意IDにしておきましょう。つまり、オブジェクトIDと要素IDの2つでオブジェクトを指定する形式にします。



A 壊す事を考えた時に発生するごっつい問題

 ファクトリクラスでオブジェクトを作るのは簡単です。大抵はメソッド内で「new」するだけです。指定のオブジェクトIDに該当する生成物を引き渡すのもそれ程難しくなくできます。問題は「削除」なんです。

 例えば、あるタスクが「ID01234を下さい」と要求してきたとします。生成者はそのIDに該当するオブジェクトを検索してそのスマートポインタを渡します。この動作が成立するには、生成者がID01234のオブジェクトを持っている必要があります。ですから、タスクが新規オブジェクトのスマートポインタを受け取った時、その参照カウンタは常に「2」となります。

 では、そのタスクがメモリから消えたとしましょう。この時保持しているのは生成者だけなので、参照カウンタは1になります。しかし、それは生成者がいなくならなければ0になれないので、ずっと生成者の中に残る事になります。つまり、このままだと大規模なゲームではメモリがパンクしまいます。

 これはオブジェクトを消すタイミングを定義する必要があります。オブジェクトを消すのは「自分だけが参照している事が明らかな時」にします。参照カウンタをチェックして1であるオブジェクトはリストからはずします。オブジェクトを壊すには、多分この手段しかありません。そこで、このチェックをするOptimaizeメソッドをファクトリクラスに設ける事にします。これはここで作成したい全てのファクトリクラスが持つため、親ファクトリクラスに定義します。

 ただそうなりますと、このメソッドを呼ぶ「誰か」が必要になります。それは各ファクトリのOptimaizeメソッドを呼び出す管理人である「ファクトリマネージャ」を新設して彼に任せる事にしましょう。



B ファクトリマネージャとファクトリの自動登録

 ファクトリマネージャは、簡単に言えば全ファクトリに対して指示を与える大親分です。削除チェックのタイミングも大親分が知っています。

 大変なのは、ファクトリクラスの登録です。何せ、いつどこで誰が何の種類のファクトリクラスを使うのかわかりません。それを事前に考えて全ファクトリクラスをファクトリマネージャに登録するのはあまりに辛い手間です。そこで何とか「使用したファクトリクラスをマネージャに自動登録できないか」を考えてみます。

 とっかかりの第1歩は、ファクトリクラスを「シングルトン(Singleton)」で実装する事です。シングルトンとは、プログラム内でそのオブジェクトが1つだけである事を保証するデザインパターンの一つです。これは、次のような実装を行います:

CHogeFactoryシングルトンファクトリクラス
class CHogeFactory : public IObectFactory
{
protected:
   CHogeFactory(){}

public:
   static CHogeFactory& Instance()
   {
      static CHogeFactory instObj;
      return instObj;
   }

   virtual DWORD Create( sp<IHoge> &spHoge )
   {
      spHoge.SetPtr( new CHoge );
      return 0;
   }
};
(このプログラムは仮組みです)

 注目するところは2ヶ所。まず、CHogeFactoryのコンストラクタをprotetcted宣言します。これにより、CHogeFactoryは外部から一切生成出来なくなります。ではどのようにオブジェクトを得るのか?こはInstanceメソッドを、

CHogeFactory::Instance();

のように直接呼び出します。これを可能にするため、Instanceメソッドはstatic宣言しています。Instanceメソッドの内部ではCHogeFactoryのオブジェクトがこれまたstaticで生成され返されます。ここがうまいところで、2度目以降のInstanceメソッドの呼び出しではすでに生成されたオブジェクトが返されるようになります。これにより、CHogeFactoryはプログラム内で1つしか存在できないことが保証されます。

 この実装を各クラスごとに行うわけですが、さすがにそれは面倒。そこで、テンプレートクラス化してしまいます。上のファクトリクラスをテンプレートクラスにすると、こうなります:

シングルトンテンプレートファクトリクラス
template <class T>
class CGeneralSPFactory : public IObectFactory
{
protected:
   CGeneralFactory(){}

   public:
   static CGeneralSPFactory &Instance()
   {
      static CGeneralSPFactory<T> inst;
      return inst;
   }

   DWORD Create( sp<T> &spObj )
   {
      spObj.SetPtr( new T );
      return 0;
   }
};
(このプログラムは仮組みです)

 これで、T型として定義できるあらゆるクラスのシングルトンファクトリクラスができちゃいます。ただし、T型には純粋仮想クラスなどnewによる生成が出来ないクラスは取れません。つまり、DirectXのComコンポーネントなどはこのテンプレートクラスが使えません。COMコンポーネントは生成・削除方法が独特なのでちょっと面倒ですが1つ1つ丁寧にファクトリクラスを作ります。

 では次にファクトリマネージャ(CSPFactoryManager)を作ります。ファクトリマネージャには登録メソッドであるRegistFactoryメソッドがあるとします:

CHogeFactoryシングルトンファクトリクラス
class CSPFactoryManager
{
protected:
   // 管理ファクトリオブジェクトポインタ
   static list<ISPObjectFactory*> m_pFactoryList;

protected:
   // コンストラクタ
   CSPFactoryManager(){};

public:
   // オブジェクト生成メソッド(シングルトン)
   static CFactoryManager &Instance()
   {
      // static宣言で1つだけ存在できる事を保証
      static CSPFactoryManager inst;
      return inst;
   }

   // 管理するファクトリオブジェクトを登録
   bool RegistFactory( ISPObjectFactory* ptr )
   {
      m_pFactoryList.push_back( ptr );
      return true;
   }

};

// staticなリストをメモリに配置
list<ISPObjectFactory*> CFactoryManager::m_pFactoryList;

実は、CSPFactoryManagerクラスもシングルトンです。この方が管理が楽になるためです。RegistFactoryメソッドはISPObjectFactoryインターフェイスポインタをm_pFactoryListリストに登録します。

 ファクトリマネージャをテンプレートファクトリクラス内に取り込むと、こうなります:

シングルトンテンプレートファクトリクラス
template <class T>
class CGeneralSPFactory : public IObjectFactory
{
protected:
   CGeneralSPFactory()
   {
      CSPFactoryManager::Instance().RegistFactory( this );
   }

public:
   static CGeneralSPFactory &Instance()
   {
      static CGeneralSPFactory<T> inst;
      return inst;
   }

   DWORD Create( sp<T> &spObj )
   {
      spObj.SetPtr( new T );
      return 0;
   }
};

 コンストラクタに自分自身の登録コードを追加します。これはこのテンプレートクラスがシングルトンで、しかもオブジェクトがstaticであることが分かっていて保証されているために可能なコードになっています。static変数の寿命はグローバルなので、ファクトリクラスもファクトリマネージャもプログラムの最初から終わりまでず〜っと存在します。ファクトリクラスの数が数千というレベルになる事はきっとありませんので、まぁ問題ないでしょう。

 ということで、ファクトリクラスの自動登録が見事完成〜(考えるの大変でした(^-^;)


 ちなみに、テンプレートクラスは静的な型定義なので、

sp<int> spInt1, spInit2;
CGeneralSPFactory<int>::Instance().Create( spInt1 );
CGeneralSPFactory<int>::Instance().Create( spInt2 );

sp<double> spDouble;
CGeneralSPFactory<double>::Instance().Create( spDouble );

とした時に、ちゃんと各型に対するファクトリクラスを1つだけ作って登録してくれます。便利です。



C 本題:オブジェクトの管理方法

 どうやら各クラス別のファクトリクラスとその管理はうまくできそうです。ちょっと遠道しましたが、本題である1つのファクトリクラスのオブジェクト管理について見ていく事にしましょう。

 ここで作成するファクトリクラスは、内部に自分が生成したオブジェクト(のスマートポインタ)を保持し、新規生成と参照の両方をカバーします。「新規」か「既存」かはオブジェクトIDで判別します。ということは、IDを検索できなければなりません。

 検索が断然早いのは木構造です。そこでスマートポインタオブジェクトはツリーで管理する事にします。オブジェクトIDとスマートポインタは1対1の関係になりますので、STLのmapが最適ですね。同じIDが無いというのもこのコンテナの使用を後押ししてくれます。

 ただ、@の図にありますように、1つのオブジェクトIDは複数のオブジェクトを持てるのでした。ですから、検索のキーは実は2つ(オブジェクトIDと要素ID)あります。よって、「mapにmapを持たせる」仕様になります。テンプレートファクトリクラス内のmapメンバ変数の定義を見てみましょう:

シングルトンテンプレートファクトリクラス
typedef size_t OBJID;
typedef size_t ELEMID;

template <class T>
class CGeneralSPFactory : public IObjectFactory
{
   typedef pair< ELEMID, sp<T> >   ELEMPAIR;   // 要素ペア
   typedef map<ELEMID, sp<T> >     ELEMMAP;    // 要素マップ
   typedef sp<ELEMMAP>           SPELEMMAP;    // 要素マップのスマートポインタ
   typedef pair<OBJID, SPELEMMAP >     OBJIDPAIR; // オブジェクトペア
   typedef map< OBJID, SPELEMMAP >     OBJIDMAP;  // オブジェクトマップ

   OBJIDMAP m_IDMap; // オブジェクト格納マップ

};

 沢山あるtypedefはすべて簡便記載のためです。こういうのを見ると目がくらんでしまうのですが、注意深く見れば大丈夫です。
 まず、ELEMPAIR型は要素IDとスマートポインタのペアを定義します。ELEMMAPはその要素ペアを格納するmapです。次のSPELEMMAPは「mapのスマートポインタ」です。そしてOBJPAIR型やOBJMAP型はそのmapスマートポインタを保持します。
 どうしてわざわざmapスマートポインタにするのか?これはパフォーマンスのためなんです。もし要素マップをそのままオブジェクトマップに保持すると、OBJIDMAPへの挿入や削除時に「要素マップの全コピー」が行われます。これは強烈にパフォーマンスを犠牲にします。これが単にスマートポインタであれば、高々12バイトくらいのポインタの挿入・削除を行うだけなので、かかる負担は最小限になります。単純なポインタにしないのはもちろん削除の手間を省くためです。

 ファクトリクラスがオブジェクトを生成する時には1つのオブジェクトIDと配列の要素番号を指定するので、先ほどのCreateメソッドは引数の変更を含めちょっと複雑になります:

CGeneralSPFactory::Createメソッド
template <class T>
OBJID CGeneralSPFactory<T>::Create(Create( OBJID id, ELEMID elem, sp<T> &spOut )
{
   // オブジェクトIDチェック
   OBJIDMAP::iterator ObjID = m_IDMap.find( id );
   if( ObjID == m_IDMap.end() )
   {
      // 新しいID内要素マップを作成してオブジェクトを登録
      sp<T> spNewObj( CreateNewObj() );
      ELEMPAIR NewElemPair( elem, spNewObj );

      // 新オブジェクトIDと要素マップをオブジェクトマップに追加
      ELEMMAP NewElemMap( new map<ELEMID,sp<T> > );
      NewElemMap->insert( NewElemPair );
      OBJIDPAIR ObjPair( id, NewElemMap );
      m_IDMap.insert( ObjPair );
      spOut = spNewObj;
      return id;
   }

   // 要素IDのチェック
   map<ELEMID,sp<T> >::iterator it = (*ObjID).second->find(elem);
   if( it == (*ObjID).second->end())
   {
      // 新規作成
      sp<T> spNewObj( CreateNewObj() );
      ELEMPAIR NewPair( elem, spNewObj );
      (*ObjID).second->insert( NewPair );
      spOut = spNewObj;
   }
   else
   {
      // 既存オブジェクトの参照渡し
      spOut = it->second;
   }
   return id;
};

 やっていることはオブジェクトIDが存在しているかをチェックしているだけです。mapのチェックの仕方はちょっと独特なので、慣れないと「うわ〜」と引きますよね。
 オブジェクトが存在しなければ、新しい空のオブジェクトを作りマップに登録します。指定のオブジェクトIDや要素IDがある場合、既存オブジェクトの参照渡しとなります。この形態はもう殆ど変わりません。

 これで、例えば次のような呼び出しでオブジェクトをどんどん追加していけます。

sp<CPosInfo> spPos1, spPos2;
CGeneralSPFactory<CPosInfo>::Instance().Create( 0, 0, spPos1 );
CGeneralSPFactory<CPosInfo>::Instance().Create( 1, 0, spPos2 );

sp<double> spDouble1, spDouble2;
CGeneralSPFactory<double>::Instance().Create( 10, 4, spDouble );
CGeneralSPFactory<double>::Instance().Create( 10, 3, spDouble );

こうなってくると、オブジェクトのID管理が今後の課題となりそうですよね。



D オブジェクトの削除

 ファクトリクラスが保持しているオブジェクトはOptimaizeメソッドの呼び出しにより参照数がチェックされ、参照数が1になった場合は削除されます。スマートポインタの削除は単にマップから要素を抜くだけです:

CGeneralSPFactory::Optimaizeメソッド
// マップを最適化する
bool Optimaize()
{
   // 参照カウントが1のオブジェクトは取り除く
   OBJIDMAP::iterator ObjIt;
   for(ObjIt=m_IDMap.begin(); ObjIt!=m_IDMap.end();)
   {
      ELEMMAP::iterator it;
      for(it=ObjIt->second->begin(); it!=ObjIt->second->end();)
      {
         if(*(*it).second.GetRefPtr() != 1){
         it++;
         continue;
         }
         // 削除対象
         it = ObjIt->second->erase(it);
      }

      // 要素数が0のオブジェクトIDは取り除く
      if( ObjIt->second->size()==0 )
      {
         ObjIt = m_IDMap.erase( ObjIt );
         continue;
      }
      ObjIt++;
   }
   return true;
}


STLのコンテナ群でイテレータを回遊しながら削除する方法はちょっと独特です。eraseメソッドは指定のイテレータを取り除き「次のイテレータ」を返してくれます。そこでforループのカウントアップは削除しなかった場合にのみ行います。ですからfor文の3番目が空定義になっているんです。

 このメソッドを呼ぶのはファクトリマネージャです。

CSPFactoryManager::Optimaizeメソッド
// 各ファクトリを最適化する
virtual bool Optimaize()
{
   list<ISPObjectFactory*>::iterator it;
   for(it=m_pFactoryList.begin(); it!=m_pFactoryList.end();it++)
      (*it)->Optimaize();
   return true;
}

 ここが面白いところでして、登録はCSPFactoryManagerが管理しますが、ファクトリクラスの最適化のタイミングはその派生クラスで管理できます。ですから例えば300フレーム進む度に最適化するとか、任意のタイミングで最適化する調整が可能です。



E COMシングルトンファクトリを作る

 スマートポインタの場合生成は全部newで行けますが、DirectXのCOMコンポーネントは生成メソッドを通さないとオブジェクトを取得できません。ですから先ほど作成したテンプレートファクトリを使えません。そこで、COM用のファクトリクラスを作ってみます。

 すべてのCOMファクトリに共通するのはコンポーネントの検索方法と削除(最適化)方法です。ですからこの部分はテンプレートにしてしまいます。一方生成方法は各COMについて独特であるため、テンプレートクラスの派生クラスでそれぞれ定義します。

 まずはテンプレートクラスの実装から見てみましょう(ちょっと長くてすいません):

ICOMObjectFactoryテンプレートインターフェイス
template <class T>
class ICOMObjectFactory : public IObjectFactory
{
   typedef pair< ELEMID, T > ELEMPAIR; // 要素ペア
   typedef map< ELEMID, T > ELEMMAP;   // 要素マップ
   typedef sp<ELEMMAP> SPELEMMAP;               // 要素マップのスマートポインタ
   typedef pair<OBJID, SPELEMMAP > OBJIDPAIR;   // オブジェクトペア
   typedef map< OBJID, SPELEMMAP > OBJIDMAP;    // オブジェクトマップ

   OBJIDMAP m_IDMap; // オブジェクト格納マップ

protected:
   // プロテクトコンストラクタ
   ICOMObjectFactory(){}

   // 指定要素の検索
   typename ELEMMAP::iterator Search( OBJID id, size_t elem )
   {
      // オブジェクトIDチェック
      OBJIDMAP::iterator ObjID = m_IDMap.find( id );
      if( ObjID == m_IDMap.end() )
      {
         // 新しいID内要素マップを作成
         T cpNewObj; // 空生成
         ELEMPAIR NewElemPair( elem, cpNewObj );
         SPELEMMAP NewElemMap( new ELEMMAP );
         NewElemMap->insert( NewElemPair );

         // 新オブジェクトIDと要素マップをオブジェクトマップに追加
         OBJIDPAIR ObjPair( id, NewElemMap );
         m_IDMap.insert( ObjPair );

         // 新規作成したイテレータを出力
         return NewElemMap->begin();
      }

      // 要素IDのチェック
      ELEMMAP::iterator it = (*ObjID).second->find(elem);
      if( it == (*ObjID).second->end())
      {
         // 新規作成
         T cpNewObj; // 空生成
         ELEMPAIR NewPair( elem, cpNewObj );
         return (*ObjID).second->insert( NewPair ).first; // insertはイテレータを返す
      }

      // 既存オブジェクトの参照渡し
      return it;
   }


public:
   // マップを最適化する
   bool Optimaize()
   {
      // 参照カウントが1のオブジェクトは取り除く
      OBJIDMAP::iterator ObjIt;
      for(ObjIt=m_IDMap.begin(); ObjIt!=m_IDMap.end();)
      {
         ELEMMAP::iterator it;
         for(it=ObjIt->second->begin(); it!=ObjIt->second->end();)
         {
            if((*it).second.GetPtr()!=NULL)
            {
               // カウントチェック
               ULONG cnt = (*it).second->AddRef()-1;
               (*it).second->Release();
               if(cnt != 1){
               it++;
               continue;
            }
            // 削除対象
            it = ObjIt->second->erase(it);
            }

         // 要素数が0のオブジェクトIDは取り除く
         if( ObjIt->second->size()==0 )
         {
            ObjIt = m_IDMap.erase( ObjIt );
            continue;
         }
         ObjIt++;
      }
      return true;
   }
};


 こういう長いのを見るのは辛いものですいませんです。でもテクニックが色々入っていますのでお得ですよ(お得って(^-^;;)。このテンプレートクラスでは生成作業は行いません。またコンストラクタがプロテクト宣言されているので、このテンプレート自体を使うことはできません。Searchメソッドは指定のオブジェクトIDと要素IDに該当する要素マップイテレータを返します。この戻り値が、

typename ELEMMAP::iterator

となっていますが、このtypenameが無いとエラーになってしまいます。こういう所は知らないとバグ取りできなくて泣きますよね(T_T)。このイテレータは指定IDの指定要素IDをピンポイントで指しています。コードの太文字部分を見ますと、すべて必ずT型のオブジェクトが存在している事がわかります。新規の場合もT型オブジェクトを指すイテレータが返っています。派生クラスでは生成作業の最初でこの検索を行いイテレータを取得し、そこに生成オブジェクトを流し込みます。

 「どの編がCOMポインタなん?」と思われるかもしれませんが、それはOptimaizeメソッド内でAddRefメソッドとReleaseメソッドが使用されている所にあります。これによりT型はCOMポインタ型以外は設定できません。

 このテンプレートを派生してCOMオブジェクトを生成するファクトリクラスをどう作成するかは、すぐ次のサンプルプログラムで出てきますが、例えばCOMテクスチャファクトリクラスのクラス宣言はこんな感じなんです。

COMTextureFactoryクラス宣言部分抜粋
class CComTextureFactory : public ICOMObjectFactory<Com_ptr<IDirect3DTexture9> >

親クラスに型を具体的に指定すると、テンプレートクラスは普通のクラスに戻ります。これ、プチテクニックです。こうすることで、テクスチャ専用のファクトリクラスになります。後は生成部分を作成するだけです。



F オブジェクトの後片付け

 ファクトリクラスはstatic宣言されています。またファクトリマネージャもstaticで定義されています。今回これらは消去とともに内部に確保されたオブジェクトもすべて解放できるつもりだったのですが、試してみますとどうもうまくいかないようなんです。static宣言されたオブジェクト特有の問題なのかはよくわかりません。少なくともメモリリークの心配が残ります。

 そこでファクトリクラスにClearAllメソッドを追加します。やることはファクトリクラスが持つオブジェクトマップをすべてクリアするだけです。:

ClearAllメソッド
// 全オブジェクトクリア
virtual void ClearAll()
{
   m_IDMap.clear();
}

このメソッドは誰でも呼べますが、ファクトリマネージャが最後に全ファクトリクラスに対して呼ぶと効果的です:

CFactoryManager::ClearAllメソッド
// 全ファクトリにクリア命令を出す
virtual void ClearAll(){
   list<IObjectFactory*>::iterator it;
   for(it=m_pFactoryList.begin(); it!=m_pFactoryList.end();it++)
      (*it)->ClearAll();
}

これをゲームが終了する時に呼び出しますと、すべてのオブジェクトが滞りなく解放されます。まぁ、ちょっとしたお約束だと思えば問題なしです(^-^;



G ファクトリクラステストプログラム

 では、この章で取り上げたファクトリクラスのテストプログラムです。何にしようか考えたのですが、キーを押すと画面内にテクスチャが貼り付けられたスプライトがどんどん出現するプログラムにします。描画位置(CPosInfo)、テクスチャ(IDirect3DTexture9)、スプライト(ID3DXSprite)がオブジェクトになります。描画位置とスプライトは個別の情報なので新しいオブジェクトIDを振り続けます。一方テクスチャは共有リソースとして共有オブジェクトIDでまかなう事にします。

 COMテクスチャファクトリクラスはEで出てきたICOMObjectFactoryテンプレートクラスから派生させます。検索や最適化の部分は親クラスが実装してくれていますので、子クラスでは生成部分とシングルトン化に集中するだけです:

CComTextureFactory宣言部
class CComTextureFactory : public ICOMObjectFactory<Com_ptr<IDirect3DTexture9> >
{
   typedef Com_ptr<IDirect3DTexture9> ComTexture;

protected:
   // コンストラクタ(マネージャ登録をする)
   CComTextureFactory()
   {
      CFactoryManager::Instance().RegistFactory( this );
   }

   public:
   // オブジェクト生成メソッド(シングルトン)
   static CComTextureFactory &Instance()
   {
      static CComTextureFactory inst; // static宣言で1つだけ存在できる事を保証
      return inst;
   }

public:
   // Comテクスチャをファイルから作成
   HRESULT CreateTextureFromFile( OBJID objid, ELEMID elemid, LPDIRECT3DDEVICE9 pDevice, LPCTSTR pSrcFile, ComTexture &cpTexture)
   {
      // 格納イテレータ取得
      ELEMMAP::iterator it = Search( objid, elemid );
      if(it->second.GetPtr()!=NULL){
         // 既存
         cpTexture = it->second;
         return true;
      }

      // 新規作成
      HRESULT res = D3DXCreateTextureFromFile( pDevice, pSrcFile, it->second.ToCreator() );
      cpTexture = it->second;
      return res;
   }
};

親クラスのお陰でソースが短いですね。シングルトンとファクトリマネージャへの登録部分はこの章ですでに説明しました。COMテクスチャを生成する部分は、ここではD3DXCreateTextureFromFile関数のラッパメソッドになっています。最初に格納するイテレータを取得し、作成したテクスチャを直接格納しています。ラッパメソッドなので、戻り値もHRESULTとDirectXに揃えます。これだけでオブジェクト管理をするテクスチャファクトリになるのですから、なんか儲けもんです(笑)。

 スプライトファクトリも全く同様です。スプライトの場合はD3DXCreateSprite関数が生成関数ですので、これをラップします。

 テストプログラムの中心はオブジェクトファクトリクラスとファクトリマネージャクラスの作成方法と扱い方なので、その他の細かな実装は完成版のプログラムをご覧下さい(今回さすがに疲れてきました(^-^;)。サンプルプログラムをこちらにアップします