C++で擬似タスクに挑戦

はぬぅーん.い…忙しい!!
いよいよ追い込まれてきたぁ!!!!

なんか,PC再インストールしてから,HDが変な音するなぁ〜
怖いぜ!


さて,今回は「タスクプログラミング」に挑戦!!
タスクっていっても,マルチタスクとかそんなんじゃなくて,
ゲーム用のプログラムの書き方(なのかな?)っぽいやつです.
ネットサーフィン中に結構見かけるので,「一度はやってみたいなぁ〜」
と常々思ってました.(思うだけでやってませんが)

実際にタスクプログラムとかやったことないけど,どうにかして タスクをクラスを使って実装してみたいっす!!
C言語のまま,関数のポインタとか構造体のキャストとかを使いまくるのは,オラには無理っす!
絶対頭こんがらかるっす!(TДT)ポインタきら〜い

ちなみに今回は,タスクシステム全てを実装できていません.
1度もタスクシステムとか使ったことないので,何が必要なのかよくわかんないなぁ
やっぱり1度タスク使ってゲーム作ってみないとダメなぁ
でも一応,指針…というかメドが付きそうなので雛型を作ってみました.


タスクのメリット

さてさて,タスクシステム自体についてはこの日記では詳しく書きません.
ていうか,未だよくわかってないです(TДT)

タスクシステムのメリットについて,自分なりに考えてみました.
 1.拡張が容易
 2.条件分岐が減らせる
 3.メモリのサイズ予測ができる

1.拡張が容易
んー,使ったことないんで,よくはわからないんですが,どこのページみても「拡張が容易」って書いてあるから,そうじゃないかな〜なんて…( ̄∀ ̄;)>ハハ
でも,タスク間の独立性を高くすることができれば,新キャラの導入とか,仕様変更があってもダイジョブなんじゃないかなー?
新キャラ追加する場合は,新しいタスクを作ればいいだけで,メインプログラムを変更する必要がないかもしれないし,独立してるんならプログラミングを分業できるかもしれないし.
仕様変更の時だって,独立性が高いならパーツを組替えるノリでタスクを切り替えればいいだけかも知れないしなぁ…
(関係ないけど,プログラムの分業ってやったことないから,やってみたいなぁ)

2.条件分岐が減らせる
これは,ステートマシンとかそういうときに役に立ちそう.
あるいは,キャラ別の処理とかのときに,判定が減らせそうだなぁ.
void キャラごとの処理()
{
  switch( キャラのタイプ )
  {
  case ジゴロ :
    ジゴロビーム();
    break ;

  case おねぇ:
    フェロモンビーム();
    break ;

  case 幼女:
    もえもえビーム();
    break ;
  }
}

		 ↓変更!


void キャラごとの処理()
{
  キャラタスク->ビーム()
}
みたいな感じになったりするんじゃないかなぁ〜
(でもこのくらいなら,C++の仮想関数使えばいいかな)

3.メモリサイズの予測ができる
これは,重要っぽい!!
ていうか,1番メリット感じる!
あらかじめ,タスクの数も,1つのタスクサイズも決まっているから, タスクに関しての最大メモリ使用量が予測できるからねぇ
それに,動的にメモリ確保するわけじゃないから,何度もタスクを確保したり開放したり しても,フラグメンテーションとか発生しないし早そうだ!!
(まぁ,無駄に確保してしまう領域があるのは否めないけどねぇ)
特にコンシューマ機用のゲームとか,メモリが限られてるからねぇ


さて,とりあえずいろいろメリットはありそうですねぇ


new のオーバライド

基本的には,全てのタスクの元になる「TaskBase」クラスを作り, 各々のタスクは「TaskBase」を継承して作成できるようにしてみたいにゃー.
最終的には,固定長のタスクとして領域を確保しないと,「メモリサイズ固定」という メリットがなくなるなぁ.

  TaskList.Add( new EnemyTask() );
みたいな感じで,タスクを追加できるといいかなー.
あるいは,
  // メモリ確保すると,自動的にタスクに追加
  TaskBase * pTask = new EnemyTask();

  // 開放すると自動的にタスクからハズされる
  delete pTask ;
ってな感じで,自動的にタスクに追加されると便利…かも(?)
ただ,リストからの削除に関しては,削除依頼フラグとか使わないと バグが出そうな予感( ̄∀ ̄)

ということで,とりあえずnewとdeleteのオーバライドしないといけないっぽいなぁ.
でも,operator とかあんまり(ていうか全く)使ったことないんだよね.ハハ…

実験として,new と delete をオーバライドしてちゃんと動くか試してみよぅ
あとで,new とか delete したときに,メモリプールから固定サイズの領域を もらうようにすればいいはずだし.

#include <stdio.h>
#include <stdlib.h>


///////////////////////////////////////////////////////////////////////////////
//
// new 演算子のオーバライド実験用の,派生元クラス
//
///////////////////////////////////////////////////////////////////////////////
class NewTestBase
{
public :

  //-------------------------------------------------------------------------
  //  new
  //-------------------------------------------------------------------------
  void * operator new ( size_t size )
  {
    printf( " NewTestBaseサイズ:%d\n", sizeof(NewTestBase) );
    printf( " 確保サイズ:%d\n", size );

    return malloc( size );		
  }


  //-------------------------------------------------------------------------
  //  delete
  //-------------------------------------------------------------------------
  void operator delete( void * p )
  {
    printf( " NewTestBase開放\n" );

    free( p );
  }

  //-------------------------------------------------------------------------
  //  コンストラクタ
  //-------------------------------------------------------------------------
  NewTestBase()
  {
    printf("  NewTestBase::コンストラクト\n");
  }

  //-------------------------------------------------------------------------
  //  デストラクタ
  //-------------------------------------------------------------------------
  virtual ~NewTestBase()
  {
    printf("  NewTestBase::デストラクト\n");
  }

  //-------------------------------------------------------------------------
  // 実験用〜
  //-------------------------------------------------------------------------
  virtual void move()
  {
    printf("  NewTestBase.a = %d\n", a );
  }

protected :
  int a, b, c ;
};





///////////////////////////////////////////////////////////////////////////////
//
// new 演算子のオーバライド実験用の,派生クラス
//
///////////////////////////////////////////////////////////////////////////////
class NewTestChild : public NewTestBase
{
public :
  //-------------------------------------------------------------------------
  //  new
  //-------------------------------------------------------------------------
  void * operator new( size_t size )
  {
    printf( "	NewTestChildのサイズ:%d\n", sizeof(NewTestChild) );
    printf( "	確保サイズ:%d\n", size );

    return malloc( size );
  }

  //-------------------------------------------------------------------------
  //  delete
  //-------------------------------------------------------------------------
  void operator delete( void * p )
  {
    printf( "	NewTestChild開放\n" );
    free( p );
  }


  //-------------------------------------------------------------------------
  //  コンストラクタ
  //-------------------------------------------------------------------------
  NewTestChild()
  {
    printf("	NewTestChild::コンストラクタ\n");
  }


  //-------------------------------------------------------------------------
  //  デストラクタ
  //-------------------------------------------------------------------------
  virtual ~NewTestChild()
  {
    printf("	NewTestChild::デストラクタ\n");
  }


  //-------------------------------------------------------------------------
  //  テスト〜
  //-------------------------------------------------------------------------
  virtual void move()
  {
    printf("    NewTestChild.c = %d\n", c );
  }

private :
  int c, d, e ;
};



///////////////////////////////////////////////////////////////////////////////
//
//	Main
//
///////////////////////////////////////////////////////////////////////////////
int main()
{
  // もともとのクラスで実験
  printf("-- 派生元クラス --\n");

  NewTestBase * ptest = new NewTestBase();

  ptest->move();

  delete ptest ;



  // 派生クラスで実験
  printf("\n-- 派生クラス --\n");

  ptest = new NewTestChild();

  ptest->move();

  delete ptest ;


  getchar();
  return 0 ;
}
…で,これ↓が実行結果.
-- 派生元クラス --
 NewTestBaseサイズ:16
 確保サイズ:16
  NewTestBase::コンストラクト
  NewTestBase.a = -842150451
  NewTestBase::デストラクト
 NewTestBase開放

-- 派生クラス --
   NewTestChildのサイズ:28
   確保サイズ:28
  NewTestBase::コンストラクト
    NewTestChild::コンストラクタ
    NewTestChild.c = -842150451
    NewTestChild::デストラクタ
  NewTestBase::デストラクト
   NewTestChild開放
ふむふむ.ちゃんとコンストラクタも呼ばれてるし,期待通りの動作してるな.
しかし,クラスを派生する度に new と delete オーバライドするのメンドイなぁ.
それに,うっかりオーバライド忘れたらどうなるんやろ?
恐ろしいバグでも発生しなければいいけど…

というわけで,実験的に派生クラス「NewTestChile」の operator new と operator delete をコメントアウトして実行した結果がこれ↓
-- 派生元クラス --
 NewTestBaseサイズ:16
 確保サイズ:16
  NewTestBase::コンストラクト
  NewTestBase.a = -842150451
  NewTestBase::デストラクト
 NewTestBase開放

-- 派生クラス --
 NewTestBaseサイズ:16
 確保サイズ:28                ← ちゃんと確保されてる!?
  NewTestBase::コンストラクト
    NewTestChild::コンストラクタ
    NewTestChild.c = -842150451
    NewTestChild::デストラクタ
  NewTestBase::デストラクト
 NewTestBase開放
あ,あれ? 派生元の new と delete がちゃんと呼ばれる!?
仮想関数でもないのに?!
(てか,new を virtual にできなかったけど…)
ひょっとして,new とか delete ってデフォルトで仮想関数!?
しかも,NewTestChild の確保サイズも正しい?!
…ということは,基本クラスでオーバライドしておけば, 派生クラスでは何もしなくてもいいってこと!?
うあぁ〜,スッテキー♥

これは,結構イケそうだな♪


メモリプールみたいなの作る

さてさて,せっかく new,delete をオーバライドできることがわかったから, new,delete するときにタスクスペースからメモリを確保することにしよぅ〜.
タスクサイズは256byte固定,タスクの最大数も256個にします.
んー,「FreeTask」では,削除フラグを立てるだけの方がいいかも…
で,あとで「CheckAndFreeTask」みたいな感じでフラグをチェックして削除する…
というような,ガベージコレクションみたいなシステムにしておいたほうがいいな.多分…
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>



#define TASK_SIZE 256
#define TASK_NUM  256

///////////////////////////////////////////////////////////////////////////////
//
//	タスク用メモリクラス
//	
///////////////////////////////////////////////////////////////////////////////
class TaskMemory
{
public :

  //-------------------------------------------------------------------------
  //  コンストラクタ
  //-------------------------------------------------------------------------
  TaskMemory()
  {
    // ホントはコンストラクタで確保したくないけど…
    m_pHeap = (BYTE*)malloc( TASK_SIZE * TASK_NUM );

    memset( m_HeapIndex, 0, sizeof(BYTE)*TASK_NUM );


    printf("タスクサイズ:%d\n", TASK_SIZE );
    printf("最大タスク数:%d\n", TASK_NUM );
    printf("ヒープの先頭アドレス:0x%x\n", m_pHeap );
  }


  //-------------------------------------------------------------------------
  //  デストラクタ
  //-------------------------------------------------------------------------
  ~TaskMemory()
  {
    free( m_pHeap );
  }


  //-------------------------------------------------------------------------
  //  空きタスク領域を得る
  //
  //	[戻り値]
  //	NULL     : 空き領域なし
  //	それ以外 : タスク領域のポインタ
  //
  //  ここは現在かなりヘボい実装をしてるので,
  //  うまく改良して高速化するべし!
  //-------------------------------------------------------------------------
  void * MallocTask()
  {  
    for (int i=0 ; i < TASK_NUM ; ++i )
    {
      if ( 0 == m_HeapIndex[ i ] ) 
      {	
        printf( "タスクインデクス[%d], アドレス[0x%x]\n", 
                   i, m_pHeap + (i * TASK_SIZE) );

        m_HeapIndex[ i ] = 1 ;

        return ( m_pHeap + (i * TASK_SIZE) ) ; 
      }
    }

    return NULL ;
  }


  //-------------------------------------------------------------------------
  //  タスク領域を開放する
  //
  //  [引数]
  //  address : 開放(無効化)したいタスク領域のアドレス
  //
  //  delete の実装都合上,インデクスよりもアドレスの方がよかったので.
  //-------------------------------------------------------------------------
  void FreeTask( const void * const address )
  {

    int index = ( (BYTE*)address - m_pHeap ) / TASK_SIZE ;


    printf("開放するアドレス[0x%x] → インデクス[%d]\n", address, index );


    if ( index >= 0   &&  index < TASK_NUM )
    {
      m_HeapIndex[ index ] = 0 ;
    }
  }

private :

  BYTE * m_pHeap ;                  // 仮想ヒープ領域
  BYTE m_HeapIndex[ TASK_NUM ] ;    // 0 : 未使用   1 : 使用中

};



// 大域変数にしちゃった…
// クラスなんかにせずに,関数だけのモジュールにすればよかったなぁ.

TaskMemory g_TaskMemory ;



///////////////////////////////////////////////////////////////////////////////
//
//  タスクのベースクラス
//
///////////////////////////////////////////////////////////////////////////////
class TaskBase
{
public  :
  
  //-------------------------------------------------------------------------
  //  new
  //
  //  size に作成するクラスが必要とするメモリサイズが入ってくるので,
  //  タスクサイズを超えないかどうかをチェックしとく.
  //-------------------------------------------------------------------------
  void * operator new ( size_t size )
  {
    if ( size >= TASK_SIZE ) 
    {
      printf("タスクサイズよりも大きなタスクを作成しようとしています\n");  
      getchar();
      return NULL ;
    }

    return g_TaskMemory.MallocTask();
  }

  //-------------------------------------------------------------------------
  //  delete
  //-------------------------------------------------------------------------
  void operator delete ( void * p )
  {
    g_TaskMemory.FreeTask( p );
  }

  //-------------------------------------------------------------------------
  //  コンストラクタ
  //-------------------------------------------------------------------------
  TaskBase()
  {
    priority = rand() % 256 ;
    printf( "  TaskBase::コンストラクタ\n" );
  }

  //-------------------------------------------------------------------------
  //  デストラクタ
  //-------------------------------------------------------------------------
  virtual ~TaskBase()
  {
    printf( "  TaskBase::デストラクタ\n" );
  }


  //-------------------------------------------------------------------------
  //  ちょっと実験用
  //-------------------------------------------------------------------------
  virtual void ShowPriority()
  {
    printf( "  TaskBase::優先度:%d\n", priority );
  }


protected :

  // ちゃんと変数にアクセスできるのかな?
  int task_ID ;  
  int priority ;
};



///////////////////////////////////////////////////////////////////////////////
//
//  タスクA
//
///////////////////////////////////////////////////////////////////////////////
class TaskA : public TaskBase
{
public :

  //-------------------------------------------------------------------------
  //  コンストラクタ
  //-------------------------------------------------------------------------
  TaskA()
  {
    printf("    TaskA::コンストラクタ\n");
  }

  
  //-------------------------------------------------------------------------
  //  デストラクタ
  //-------------------------------------------------------------------------
  virtual ~TaskA()
  {
    printf("    TaskA::デストラクタ\n");
  }


  //-------------------------------------------------------------------------
  //  実験〜
  //-------------------------------------------------------------------------
  virtual void ShowPriority()
  {
    printf("    TaskA::優先度:%d\n", priority );
  }

private :
  int pow ;


  // ためしに,タスクサイズより大きなクラスを作ろうとすると
  // 実行時に警告がでるはず〜
  // BYTE dammy[255]; 
};



///////////////////////////////////////////////////////////////////////////////
//
//  タスクB(さらに継承したらどうなるんじゃろ?)
//
///////////////////////////////////////////////////////////////////////////////
class TaskB : public TaskA
{
public :
  //-------------------------------------------------------------------------
  //  コンストラクタ
  //-------------------------------------------------------------------------
  TaskB()
  {
    printf("      TaskB::コンストラクタ\n");
  }

  
  //-------------------------------------------------------------------------
  //  デストラクタ
  //-------------------------------------------------------------------------
  virtual ~TaskB()
  {
    printf("      TaskB::デストラクタ\n");
  }


  //-------------------------------------------------------------------------
  //  実験〜
  //-------------------------------------------------------------------------
  virtual void ShowPriority()
  {
    printf("      TaskB::優先度:%d\n", priority );
  }
};


///////////////////////////////////////////////////////////////////////////////
//
//  Main
//
///////////////////////////////////////////////////////////////////////////////
int main()
{
  // 実験
  TaskBase * pTask1 = new TaskBase();
  TaskBase * pTask2 = new TaskA();

  pTask1->ShowPriority();
  pTask2->ShowPriority();

  delete pTask1 ;
  delete pTask2 ;

  
  // 継承実験
  pTask1 = new TaskB();

  pTask1->ShowPriority();

  delete pTask1 ;


  getchar();
  return 0 ;
}
そして,実行結果がこれ↓
タスクサイズ:256
最大タスク数:256
ヒープの先頭アドレス:0x540068
タスクインデクス[0], アドレス[0x540068]
  TaskBase::コンストラクタ
タスクインデクス[1], アドレス[0x540168]
  TaskBase::コンストラクタ
    TaskA::コンストラクタ
  TaskBase::優先度:41
    TaskA::優先度:35
  TaskBase::デストラクタ
開放するアドレス[0x540068] → インデクス[0]
    TaskA::デストラクタ
  TaskBase::デストラクタ
開放するアドレス[0x540168] → インデクス[1]
タスクインデクス[0], アドレス[0x540068]
  TaskBase::コンストラクタ
    TaskA::コンストラクタ
      TaskB::コンストラクタ
      TaskB::優先度:190
      TaskB::デストラクタ
    TaskA::デストラクタ
  TaskBase::デストラクタ
開放するアドレス[0x540068] → インデクス[0]
…なんか,わかりづらいけど,ちゃんと動いてるような気もするなぁ.
今度は,実際にゲームつくりながらタスクシステムの問題点とか考えていこ〜っと♪

…タスク間同士でデータを受けわたすときって,どうすんだ?
プロセス間通信みたく,こぅ,メッセージでやり取りとかするのかな〜?
うぅ〜ん,慣れるまでは難しそうだ…(TДT)




<< Back to Diary...