アットウィキロゴ

エンティティシステム


■ プレイヤーがミサイルを発射する例

ENTITY* FireProjectile ( char* className, ENTITY* entshooter, ENTITIY* parent )
{
   ENTITY* entProj;                                            // 発射物エンティティ
   VECTOR3 pos;                                                // 射手/発射物の位置
   VECTOR3 dir;                                                // 射手の方向ベクトル
   VECTOR3 velocity;                                           // 射手の速度ベクトル
   
   
   // 『parent』のシーングラフの子として発射物を作成
   entProj = EntCreate(className, parent, 0, 0);
   if ( entProj)
   {
       // 射手から位置・方向・速度を取得
       EntSendMessage( entShooter, EM_GETPOS, (int)&pos, 0 );
       EntSendMessage( entShooter, EM_GETDIR, (int)&dir, 0 );
       EntSendMessage( entShooter, EM_GETVELOCITY, (int)&vel, 0 );
       
       // 新しい発射物の設定
       EntSendMessage( entProj, EM_SETPOS,      (int)&pos, 0 );
       EntSendMessage( entProj, EM_SETDIR,      (int)&dir, 0 );
       EntSendMessage( entProj, EM_SETVELOCITY, (int)&vel, 0 );
       EntSendMessage( entProj, EM_SETOWNER,    (int)&entShooter, 0 );
       EntSendMessage( entProj, EM_START,       0, 0);
       
       return (entProj);
   }
}

※ C++の実装では、(int)のキャストは正しい型付きパラメータを引数とする仮想関数により消える。


■ 全てがエンティティ

全てをメッセージで制御可能なエンティティとして考えること!
発射物も、地形も、ゲームスクリプト(アルゴリズム)といったもの全てを統一して考える。

弾丸、地形、スクリプトエンティティの様々なメッセージへの応答の仕方


                        弾丸       地形       スクリプト

更新 処理 無視 処理

描画 処理 処理 無視

衝突チェック 処理 無視 無視

衝突テストへの応答 無視 処理 無視



■ システムの要素

  • エンティティメッセージ: エンティティの通信の仕方を定義する
  • エンティティコード : エンティティクラスを実装するコードとデータ
  • クラスリスト : 登録されたエンティティクラスのリストの保守
  • エンティティマネージャ: エンティティの作成、エンティティツリーの管理、
                         1つあるいは複数のエンティティへのメッセージ送信のサポート


■ エンティティメッセージ

エンティティは必要でなかったり、理解できないメッセージを安全に無視できなければならない。

typedef int (ENT_PROC) ( ENTITY* ent, EM message, int var1, int var2 );


メッセージリストの例

この列挙型に1つ値を加えるだけで、新しいメッセージを追加できる。

enum EEntMessageTag
{
   // クラス処理
   EM_CLSINIT,                         // クラスの初期化 var1=char* データパス
   EM_CLSFREE,                         // クラスの解放
   EM_CLSNAME,                         // クラス名をvar1にコピー
   
   // 作成と破棄
   EM_CREAT,                           // エンティティの作成
   EM_START,                           // エンティティを『ON』にする
   EM_SHUTDOWN,                        // エンティティを上品に破棄
   EM_DESTOROY,                        // エンティティを即座に破棄
   
   // 標準アクション
   EM_UPDATE,                          // var1 = int 経過秒数
   EM_DRAW,                            // _normal_ レンダリング
   
   // データアクセス
   EM_SETPOS,                          // var1 = VECTOR3* 位置
   EM_GETPOS,                          // VECTOR3 位置をvar1にコピー
   
   ...
};


■ エンティティコード(Cベース)


struct missileTag
{
   char        name[ENT_NAME_LEN]; // 名前
   VECTOR3     velocity;           // 速度
   VECTOR3     position;           // ワールド空間位置
   VECTOR3     forceAccum;         // 力の累算器
   MATRIX4x4     matModel;         // 姿勢行列
   int         engineRunning;      // TRUE: エンジンON
   
   // 他に必要なメンバがあれば以下に追加する
};


static MODLE mdlMissile;
static SOUND launchSount;
static float thrust = 10000.0f;

int MissileProc( ENTITY* entity, EM message, int var1, int var2)
{

   // コンテナからエンティティのクラス データを取得
   MISSILE* e = ( (MISSILE*)entity->data );
   
   
   // ミサイルエンティティに関連する全てのメッセージを処理
   switch(message)
   {
       // クラス操作
       case EM_CLSNAME : strcpy((char*)var1, className); return(TRUE);
       case EM_CLSINIT : return( ClsInit((char*)var1) );
       case EM_CLSFREE : return( ClsFree() );

       // 作成と破棄
       case EM_CREATE  : return( Create(entity) );
       case EM_SHUTDOWN: return( Shutdown(THISENT) );
       case EM_DESTROY : return( Destroy(entity, THISENT) );
       case EM_START   : return( Start(THISENT) );

       // 標準アクション
       case EM_UPDATE  : return( Update(THISENT, INT2FLOAT(var1)) );
       case EM_DRAW    : return( Draw(THISENT) );

       // データアクセス
       case EM_SETNAME : strncpy(THISENT->name, (char*)var1, ENT_NAME_LEN); return(TRUE);
       case EM_GETNAME : strcpy((char*)var1, THISENT->name); return(TRUE);
       case EM_SETPOS  : V3Copy(&THISENT->position, (VECTOR3*)var1); return(TRUE);
       case EM_SETVEL  : V3Copy(&THISENT->velocity, (VECTOR3*)var1); return(TRUE);
       case EM_SETDIR  : return( SetDirection(THISENT, (VECTOR3*)var1) );
       
       // Win32のDefWindowProcと同様の粗野な形の継承をサポートする。
       default         : return( DefEntityProc(entity, message, var1, var2) );
   }
   return(TRUE);
}

■ クラスリスト

外側の世界に対してはWin32のウィンドウクラスと同様にエンティティクラスはメッセージ処理関数だけを公開する。
テキスト名でクラスを作成して参照できるように、クラスリストはこれらのメッセージハンドラのリンクリストや
ベクトルを保持する。


struct entityClass
{
   char name[64];              // 一意なクラス名
   ENT_PROC* clsProc           // クラスのメッセージ ハンドラ
   struct entityClass* next    // リスト内の次のクラス
};


登録はEntCreateClass関数で行う。

1) エントリをクラスリストに追加し、EM_CLSNAMEメッセージをclsProcに送り、クラスに付随するテキスト名を取り出す。

2) EM_CLSINITメッセージをclsProcに送り、リソースのロードのような、1回限りの初期化を行う機会をクラスに与える。


ゲームで使う様々なクラスの初期化は、次のようなコードで起動時に行えば良い。

// par1: エンティティがデータを見つけるパス
void GameInitClasses( char* dataPath )
{
   EntCreateClass( playerProc, dataPath, 0 );                  // プレイヤーを登録
   EntCreateClass( missileProc, dataPath, 0 );                 // ミサイルを登録
   
   // ... 以下初期化が続く
}

void EntDestoryAllClasses()
{
   // EM_CLS_FREEメッセージを1つ又は全ての登録クラスに送って後始末を楽にする。
}

■ エンティティマネージャ

エンティティマネージャは個々のエンティティの作成と削除を行い、それらを含むリストやツリーを管理する。
エンティティのメッセージ処理関数とプライベートなデータへのポインタを、単純な汎用コンテナ構造で
エンティティを参照する。


struct entityTag
{
   ENT_PRPC*           Proc;                   // メッセージ処理関数
   void*               data;                   // エンティティのカスタムデータ
   int                 guid;                   // ネット同期用の一意なID
   struct entityTag*   parent;                 // 親またはNULL
   struct entityTag*   prevSibling;            // 前の兄弟またはNULL
   struct entityTag*   nextSibling;            // 次の兄弟又はNULL
   struct entityTag*   child;                  // 最初の子
};


<エンティティマネージャの鍵となる操作>



ENTITY* EntCreateEntity( char* className, ENTITY* parent, int var1, int var2 );


1)この関数はclassNameをクラスリストで参照し、見つかったら新しいENTITY構造体を
  作成し、parentの子としてエンティティツリーに付加する。

2)クラスのメッセージ処理関数Procへのポインタを保存し、直ちにvar1,var2パラメータ
  でProcにEM_CREATEメッセージを送る。CREATEハンドラで、エンティティはクラスデータ
  用の領域を確保し、dataでそれを指す。



int EntDestoryEntity( ENTITY* ent );

DESTOROYメッセージに応じて内部で呼び出され、ENTITYコンテナ構造体を開放して、
ツリーから取り出す。実際にエンティティを破棄するには、EM_DESTOYメッセージを
送るだけだ。



int ENtSendMessage( ENTITY* ent, EM message, int var1, int var2 );

与えられたパラメーターでメッセージを個々のエンティティに送る



int EntSendMessageGuid( int guid, EM message, int var1, int var2 );

メッセージをエンティティポインタではなく、guid経由で個々のエンティティに送る。
これはポインタが意味をなさない[[ネットワーク]]同期に役立つ。



void EntSendMessagePre( ENTITY* ent, EM message, int var1, int var2 );

メッセージをエンティティとその全ての子に前順序で送る。メッセージの処理後にFALSE
を返すエンティティがあれば、再帰を中断する。

エンティティツリーがシーングラフや衝突ツリーとしても使えるように、これらは階層
カリングをサポートする。これに似た、中断しない関数も役に立つかもしれない。



拡張案

・エンティティを名前で探したり、エンティティツリーを並び替えたり、節点ごとに
  コールバックを発行するエンティティを反復する機能をサポート。


■ メッセージによるゲームループ

ほとんど全ての『ゲームコード』は、結局エンティティ自身の内部に存在するので、ループは
主としてメッセージ配送者になる。


static void GameProcessInput()
{
  // map input events, then send appropriate events to the entity(ies)
  // currently under human control
  GetInputEvents(&inputEvent);                                 // イベントデータの入力
  MapInputEventsToGameEvents(&inputEvent, &gameEvent);         // マップの衆力
  EntSendMessage(entPlayer, EM_USERINPUT, (int)&gameEvent, 0); // プレイヤー
}



static void GameUpdateWorld()
{
   EntSendMessagePre(entWorld, EM_UPDATE, GetElapsedMs(), 0);
   EntSendMessagePre(entWorld, EM_POSTUPDATE, 0, 0);
   EntCleanup(entWorld);
}


static void GameDraw()
{
   // 全てのエンティティに自分をレンダリングさせ、オーバーレイを行う
   EntSendMessagePre(entWorld, EM_DRAW,        0, 0);
   EntSendMessagePre(entWorld, EM_DRAWSHADOW,  0, 0);
   EntSendMessagePre(entWorld, EM_DRAWOVERLAY, 0, 0);    

   // デバッグモードなら、エンティティの追加情報を描画する
   if (debugMode)
       EntSendMessagePre(entWorld, EM_DRAWDEBUG, 0, 0);

   // 現在選択中のエンティティの編集状態を表示
   if (editMode)
       EntSendMessage(entBeingEdited, EM_DRAWEDIT, 0, 0);        
}
最終更新:2006年11月18日 17:01