2008-01-14

C++で遊ぶことにした

 弾幕STGをC++で実装する場合について、考えてみた。  弾幕STGには、たくさんの種類の弾が、多数存在するわけだ。  弾はすべて、1フレームごとに状態を更新されなければならないし、当たり判定や、画面外に出たかどうかなどの処理も行わなければならない。  弾ごとにこれらの処理は異なる。  そこで、次のように考えた。  まず、Bulletなるベースクラスを定義して、すべての弾に必要な、座標などのメンバと、必要なメンバ関数を純粋仮想関数で定義する。あとは、弾ごとにこのクラスを継承すればよい。  しかし、ここで問題がある。大量の弾である、クラスのオブジェクトをどうやって管理したらいいのだろう。動的多様性がほしいのだから、すべてBulletクラスのポインタでなければならない。しかし、弾は多数あるので、ひとつひとつnewしていくのはなんだか効率が悪いように思われる。そこで、こんなコードを考えた。
class BulletHolder { private :   float x, y ; public :   virtual void update() = 0 ;   virtual BulletHolder * construct_copy(void * ptr) const = 0 ;   virtual ~BulletHolder() { } } ; #define BULLET_MAKE_CONSTRUCT_COPY_PP(TYPE) \ virtual BulletHolder * construct_copy(void * ptr) const \ { return new(ptr) TYPE(*this) ; } class FastBullet : public BulletHolder { private :   float accel ; //加速度がある public :   BULLET_MAKE_CONSTRUCT_COPY_PP(FastBullet)   virtual void update()   { std::cout << "fast" << std::endl ; }      virtual ~FastBullet() { } } ; class SlowBullet : public BulletHolder { private :   float r ; // 方向がある public :   BULLET_MAKE_CONSTRUCT_COPY_PP(SlowBullet)   virtual void update()   { std::cout << "slow" << std::endl ; }   virtual ~SlowBullet() { } } ; #undef BULLET_MAKE_CONSTRUCT_COPY_PP class Bullet { private :   BulletHolder * ptr ;   static size_t const size = 16 ;   char buf[size] ; //BulletHolderを継承するすべてのクラスが収まるサイズ private :   void * getbufptr()   { return static_cast< void * >( &buf[0] ) ; }   void safe_destruct()   {     if ( ptr != 0 )       ptr->~BulletHolder() ;   } public :   Bullet() : ptr(0) { }   template < typename T >   Bullet ( T const & x )   {     BOOST_STATIC_ASSERT(( boost::is_base_of<BulletHolder, T>::value )) ;     BOOST_STATIC_ASSERT(( sizeof(T) <= size )) ;     ptr = new( getbufptr() ) T(x) ;   }   template < typename T >   Bullet & operator = ( T const & x )   {     BOOST_STATIC_ASSERT(( boost::is_base_of<BulletHolder, T>::value )) ;     BOOST_STATIC_ASSERT(( sizeof(T) <= size )) ;     safe_destruct() ;     ptr = new( getbufptr() ) T(x) ;   }   Bullet( Bullet const & b )   {       if (b.get() != 0)       ptr = b.get()->construct_copy(getbufptr()) ;   }   Bullet & operator = ( Bullet const & b )   {     if ( this != &b && b.get() != 0)     {       safe_destruct() ;       ptr = b.get()->construct_copy(getbufptr()) ;     }     return *this ;   }   BulletHolder * get() const   { return ptr ; }   BulletHolder * operator *()   { return get() ; }   BulletHolder * operator ->()   { return get() ; }   ~Bullet()   { safe_destruct() ; }    } ; int main() {   std::vector<Bullet> v(0) ;   v.push_back(FastBullet()) ;   v.push_back(FastBullet()) ;   v.push_back(SlowBullet()) ;   v.push_back(FastBullet()) ;   v.push_back(SlowBullet()) ;   for (std::vector<Bullet>::iterator iter = v.begin() ;     iter != v.end() ; ++iter )   {     (*iter)->update() ;   } }
 Bulletクラスに、すべての弾クラスが収まるサイズのバッファを用意し、そこにPlacement newでオブジェクトを構築している。  コピーコンストラクタの実装だけが、どうしてもスマートに行かない。  ここまで実装してはたと気づいた。  現実の弾幕STGでは、弾が別の弾を参照することがありえる(たとえばあるランダムに飛ぶ弾を基準に動く弾とか) すると、参照カウンタが必要だ。この場合、Bulletクラスで参照カウンタを実装して、intrusive_ptrにでも渡せばいいのではないだろうか。  そもそも、いくら多数といっても、弾幕STGの弾数は、せいぜい数百程度で、数千も行くことはないだろう。たとえshared_ptrに頼り、さらにSTLのlistを使ったとしても、本当にパフォーマンス上問題があるのか。私のCore2Quadはそんなに遅いのか?  そして、今頃気づいたのだが、上記のコードのコピーコンストラクタをスマートに書くためには、マルチメソッドが必要なのではないだろうか。そうすれば、BulletHolderクラスは、Bulletクラスのことを考えずに、へんなマクロによるおまじないも必要ないのではないだろうか。  と思ったが、マルチメソッドがあったとしても、結局BulletクラスがBulletHolderを継承しているすべてのクラスを網羅する必要があり、あまり違いはないか。

4 comments:

MB said...

これアラインメントとか大丈夫?

江添亮 said...

それもありましたね。
たとえば、これがWindows向けにVC9の32bitコードでコンパイルされたとして、BulletHolderの最初のメンバが8バイトのdoubleとか__int64形だったとしたら、アラインメント合いませんね。

やっぱり無駄に最適化したつもりになるのはやめたほうが賢明のようです。

江添亮 said...

せっかく面白いコードを考え付いたと思ったのに。
現実は複雑です。

Anonymous said...

boost::alignmentなんたらでいけると思います
というよりこれはpolyだと思います
http://d.hatena.ne.jp/mb2sync/20070928