ホーム < ゲームつくろー! < DirectX技術編 < パラパラアニメを管理しよう

その31 パラパラアニメを管理しよう


 3Dゲームが当たり前のようになっている昨今においても、2Dが台頭する場面はいくらでもあります。特に光や煙、画面に出る文字などでは必須の技術です。一昔前まで2Dというと、画面(サーフェイス)に直接ピクセルを打っていましたが、DirectX8や9になって2D専門のDirectDrawコンポーネントが無くなり、板ポリゴン+テクスチャを操作する方法に変わりました。

 1枚の板ポリゴンに対して、1枚のテクスチャを貼り付けた状態で表示するのは非常に簡単です(DirectX技術編その2「座標変換済みポリゴンで2D板ポリゴンを描画」参照)。しかし、アニメーションを行う場合、テクスチャを連続的に切り替える必要があります。さて、その時のテクスチャの管理、切り替えのタイミングなどはどう管理したら良いのでしょうか?

 この章では、ゲームで絶対に必要な「パラパラアニメ」を再現するための方法について考えていくことにしましょう。



@ アニメーションをするには何が必要か?

 板ポリゴンにテクスチャを切り替えて貼り付ける2Dアニメーションには、どういう要素が必要なのでしょうか?まず、板ポリゴン自体が必要なのはわかります。テクスチャも必要ですが、これは複数枚扱える事を前提としなければなりません。そしてパラパラアニメをするわけですから「テクスチャの描画順序」が非常に重要です。この辺りまではすぐに思いつきますね。

 テクスチャの描画順序が仮にあったとして、1枚のテクスチャは何秒表示するべきでしょうか?これは、ユーザが完全に決めるべきことです。一般に、1枚1枚の絵の描画時間を管理する事は稀ですが、設定できるようにすれば柔軟になります。つまり、「1枚のテクスチャの描画時間」という要素も必要です。ただ、実際に設定してみると「早すぎた、遅すぎた」という事もあります。ですから、「全体の描画速度を調節する変数」があってしかるべきでしょう。

 動作に関する設定として、「特定の絵を描画できる」というのは非常にありがたい動作です。例えばスコアの表示は0〜9までの数字のどれかが表示できれば実現できます。これは一般化すれば「特定の絵からアニメーションできる」という意味になります。スコアの場合は止まったアニメーションとみなせばよいわけです。これを拡張すると「ある絵から始まって指定の時間だけ進める」もしくは「ある時刻からある時刻までアニメーションを進める」という機能にたどり着きます。

 アニメーションはループしても良いですし、1回動作したら終わりという事もあります。現在の動作をユーザに知らせるためには、状態を伝える「イベントメッセージを出力」しなければなりません。それほど大掛かりにはならないと思いますが、必要な項目です。

 1枚の板ポリゴンに1つのアニメーションというのは、実は案外扱いにくいものです。例えば、ホールド状態であるアニメーションをループし続けて、ユーザが何か操作したらそれに見合うアニメーションに切り替える。そういう切り替えができると、非常に柔軟なアニメーションを再現できます。

 盲点なのが「テクスチャやキーフレームなどの読み込み」です。これが実は凄く重要でして、自由にアニメーションをさせるためにきっちりと規格化する必要があります。その中には板ポリゴン自体の情報も格納する必要があるかもしれませんし、テクスチャの合成方法や切り替えエフェクトなどの付加情報も入れ込むことになるかもしれません。そういうややこしい設定ファイルのお手本となるのは「Xファイル」にみられる「テンプレート指向」でしょう。ここではそれを見習うことにします。ユーザ定義Xファイルの読み込みの練習にもなりますね。

 このように、色々挙げてみますと、たかがパラパラアニメとは言え、やることは膨大にあります。少しずつ設計していきましょう。



A 基本インターフェイスから考えてみる

 @で述べたように、パラパラアニメは実は複雑です。こういう場合、使う側の立場から始めると全容が良く見えるようになります。それは、インターフェイスを決めるという作業に他なりません。ユーザが欲しいインターフェイスは何なのでしょうか?

 パラパラアニメは3D空間の世界にだって使い道があります。もちろんスクリーン座標で使われることもあります。もしかすると、三角形のポリゴンとか、六角形のポリゴンに表示するかもしれませんし、立体的なオブジェクトに貼り付ける事も十分に考えられます。結局のところ「貼り付けるオブジェクトについては固定しない」という事が正解かもしれません。面白いことにパラパラアニメは純粋に「テクスチャをどう扱うか」という部分だけで語れるのです。ですから、ポリゴンの生成に関するインターフェイスについては定義しないことにします。

 純粋にテクスチャの操作という事になりますと、ユーザが欲しい物も純粋に「テクスチャ」という事になります。それを一番簡単に扱うには、多分差分時間を入力値にして、その時刻に対応するテクスチャを得るというものだと思います。そこで、現在のアニメーションの時刻を進めるAdvanceTime関数を設けます。インターフェイスの単純化のため、この関数ではテクスチャは取得しないことにします。実際にテクスチャを取得するのはGetCurrectTexture関数で行うことにしましょう。

 テクスチャを設定するインターフェイスは絶対に必要です。この時順番を指定する必要は多分ありません。もし必要になればそういう機能を後ほど追加できるでしょう。ただ、「どのアニメーションセットに追加するか」という指定はあった方が良いでしょう。ここでは指定のアニメーションセットにテクスチャ及び再生時間を追加するインタフェイスであるAddTextureToAnimationSet関数を設けます。

 アニメーション時間に関しては、少し便利にしておきます。まず単純にあるアニメーションセットのスピードを調節をするSetSpeed関数を設けます。またそれとは別に「1つのアニメーション時間の総再生時間」を設定するSetAnimationTotalTime関数を設けておきます。これにより、厳密に決められた時間通りにアニメーションを終わらせることが出来ます。

 現在のアニメーションの状態ですが、よく考えてみると「再生中」や「停止中」という概念がありませんので、アニメーションの最後に行ったか否かだけが唯一の状態と言えます。これは素直にDidEndAnimation関数で成否判定をするだけにしておきましょう。

 アニメーションの切り替えを行う関数が必要です。これはSetCurAnimationSet関数としておきましょう。アニメーションはループするか1回で終わりかを決められることにします。それはSetLoop関数が担うことにします。

 さてこれで、テクスチャと時間を設定し、スピードを調整しながら指定時間のテクスチャを取得して、アニメーションも切り替えられるインターフェイスであるI2DAnimationControllerインターフェイスができそうです。



B ファイルの読み込みは別クラスで

 I2DAnimationControllerインターフェイスにはファイルからの読み込み関数がありません。これは別のクラスに担当してもらう事にします。というのは、テクスチャを読み込んで登録する作業は煩雑であるためです。また、フォーマットのマイナーチェンジに対応するためもあります。I2DAnimationControllerインターフェイスに対して指定のファイルから情報を読み取りテクスチャ等の設定を行うインターフェイスをI2DAnimationLoaderとしておきましょう。

 このクラスが公開する関数はとりあえず1つ、LoadAnimationFromX関数です。その名の通りXファイルから2Dアニメーションを行うための各種設定を抽出して、引数に渡されたI2DAnimationControllerインターフェイスに設定します。その実装は、派生クラスC2DAnimationLaderにて考えることにしましょう。



C 2Dアニメーションセットクラス

 I2DAnimationControllerクラスは複数のアニメーションセットを扱います。各アニメーションセットは同様の動作を提供しますから、当然クラス化しておいた方が便利です。1つのアニメーションを提供するクラスをI2DAnimationSetインターフェイスとしておきましょう。

 I2DAnimationSetインターフェイスのメンバ関数はI2DAnimationControllerインターフェイスと殆ど一緒です。I2DAnimationContorollerインターフェイスは殆どラッパーになるわけです。よって、しばらくはI2DAnimationSetインターフェイスについて見ていくことにします。



D I2DAnimationSet::AdvanceTime関数

 この関数はアニメーション時間を実際に進めます。

I2DAnimationSet::AdvanceTime関数
bool I2DAnimationSet::AdvanceTime(
   DOUBLE TimeDelta
);

実装クラス(C2DAnimationSetクラス)において、この関数は現在の実時間に引数の時間×単位スピードを足し、それに見合うテクスチャを探して現在のテクスチャを更新します。単位スピードはSetSpeed関数で設定される値です。ここで問題になるのは「テクスチャの検索」です。このクラスで設定されるテクスチャは次のようにその描画時間が付属しています。

テクスチャ名 持続時間(設定時) 実時間(Speed=1/3.0とした場合) 開始時刻 終了時刻
Tex01.jpg 1.0 3.0 0.0 3.0
Tex02.jpg 1.0 3.0 3.0 6.0
Tex03.jpg 2.0 6.0 6.0 12.0
Tex04.jpg 1.0 3.0 12.0 15.0
Tex05.jpg 3.0 9.0 15.0 24.0

上のような状態で、例えば8.0という時刻に相当するテクスチャであるTex03.jpgを見つけるにはどうしたら良いのでしょうか?もちろん、上から検索していけば確実に見つかります。しかし、テクスチャが100枚なんて事はざらでして、その度に線形検索するのは無駄過ぎです。よく見ると、上の表は非常に嬉しい性質を持っています。それは時刻がすでにソートされているのです。そして、引数の時間は差分ですから、現在の時刻からちょっとだけ進めればいいだけなんです。ですから累積持続時間を配列に保持しておき、現在の要素番号から検索を始めれば検索時間は殆どかかりません。

 さて、その検索ですが良く考えて見ますと、再生スピードによって引数の時間の解釈は変わってきます。再生スピードが早ければ、引数に例えば1.0と指定した場合に何枚もテクスチャを飛ばしてしまうかもしれません。内部で実時間を保持してしまえば良いかもしれませんが、その場合にスピードが途中で変化した時に困ってしまいます。ですから「内部では常に設定時の持続時間のスケールで考える」と言うのが得策です。その為にすることは簡単でして、引数の時間を現在の設定スピードで掛け算して標準化するんです。

 具体例で行きましょう。上の表の設定で、現在累積持続時間で1.5だったとします。Tex02.jpgが再生中です。スピードは1/3.0です。AdvamceTime関数に実時間である1.8を渡しました。この値に1/3.0を掛けると0.6です。これが引数の持続時間スケール変換値です。今1.5まで進んでいるので、そこに0.6を加えると2.1となります。これはTex03.jpgに相当しますから、そのテクスチャに更新します。こうすると、途中でスピードがどう変化したとしても対応ができます。

C2DAnimationSet::AdvanceTime関数(実装部)
bool C2DAnimationSet::AdvanceTime(  DOUBLE TimeDelta  )
{
   // テクスチャが設定されていない場合はエラー
   if( m_iMaxTexNumber == 0 || m_dTotalTime <= 0 )
      return false;

   // 引数を現在のスピードで標準化する
   DOUBLE StdDelta = TimeDelta * m_dSpeed;

   // 現在の累積持続時間に加算する
   m_dCurStdTime += StdDelta;

   // 累積時間が末端を過ぎているかチェック
   if( m_dCurStdTime > m_dTotalTime ){
      if( m_LoopFlag = false ){
         // 累積時間を戻す
         int times = (int)(m_dCurStdTime / m_dTotalTime);   // ループ回数を計算
         m_dCurStdTime = m_dCurStdTime - times * m_dTotalTime;   // 時間を巻き戻す
         m_iCirTexNumber = 0;    // テクスチャの番号を初期化
      }
      else
      {
         // ループが終了しているので最後のテクスチャに設定
         m_dCurStdTime = m_dTotalTime;
         m_iCurTexNumber = m_iMaxTexNumber-1;
         m_iFinishState = true;     // 終了状態にする
         return true;
      }
   }

   // 現在の要素番号から該当するテクスチャを検索
   int i;
   for(i = m_iCurTexNumber; i<m_iMaxTexNumber; i++)
   {
      // 現在の累積時間 - 要素番号の累積時間 < 要素番号の持続時間ならば
      // 該当テクスチャに当たる
      // これは絶対に該当がある!
      if( m_dCurStdTime - m_dComTime[i] < m_dTextureTime[i] )
         break;
   }

   // ループカウンタのテクスチャを設定する
   m_iCurTexNumber = i;

   return true;
}


 この機構を一度作ってしまえば、後の実装は結構楽になります。



E I2DAnimationSet::AddTexture関数

 この関数はアニメーションセット内にテクスチャと持続時間を追加します。

I2DAnimationSet::AddTexture関数
bool I2DAnimationSet::AddTexture(
  DOUBLE        Interval,
   TEXTUREPACK*  TexPack
);

第2引数には複数枚のテクスチャの情報を格納するTEXTUREPACK構造体へのポインタを渡します。重要なことですが、テクスチャは複数枚重ねられることがあります。色の付いたテクスチャとマスク用のテクスチャなどです。ですから、1つのテクスチャ番号には複数のテクスチャが配属されると考えます。

 TEXTUREPACK構造体は次のように定義しておきましょう。

TEXTUREPACK構造体
struct TEXTUREPACK{
   vector<string> TextureName;
   vector< ComPtr<IDirect3DTexture9*> > Textures;
}

もしテクスチャが作成されていなかったら、実装クラスでは内部でテクスチャを動的に生成してストックするようにします。ComPtrは「COMポインタ」と呼ばれるものでして、これによりテクスチャの保持者及び削除者を意識する必要が無くなります。COMポインタについてはこちらをどうぞ。この構造体の中に持続時間を入れないのは、汎用性を考慮してのことです。

 AddTexture関数内では、他にも持続時間と共に累積持続時間も計算して配列に格納します。これらは特に難しいことは何もありません。

C2DAnimationSet::AddTexture関数(実装部)
bool C2DAnimationSet::AddTexture(
  DOUBLE        Interval,
   TEXTUREPACK*  TexPack
)
{
   // テクスチャパックをコピー
   TEXTUREPACK Copy = *TexPack;  // COMポインタなのでこのシャローコピーは有効です
   
   // 作成されていないテクスチャを検索して作成
   int namesize = Copy.TextureName.size();
   int texsize  = Copy.Textures.size();
   int i;
   for( i=0; i<namesize; i++){
      if( i < texsize ){
         if( Copy.Textures[i] == NULL ){
         // テクスチャを作成してCOMポインタに格納
         Copy.Textures[i] = CreateTexture( Copy.TextureName[i] );
      }
      else
      {
         // テクスチャを作成して配列に追加
        ComPtr< IDirect3DTexture9*> tex( CreateTexture( Copy.TextureName[i] ) );
         Copy.Textures.push_back( tex );
      }
   }

   // 持続時間及び累積持続時間を格納
   m_dTextureTime.push_back( Interval );
   m_dTotalTime += Interval;
   m_dComTime.push_back( m_dTotalTime );

   return true;
}



F I2DAnimationSet::SetTotalAnimationTime関数

 この関数はユーザが指定した時間でパラパラアニメが終わるようにアニメーションの再生スピードを調節します。

I2DAnimationSet:SetTotalAnimationTime関数
bool I2DAnimationSet::SetTotalAnimationTime(
   DOUBLE  TotalAnimTime
);

デフォルトの全体時間はm_dTotalTimeにありますから、後はスピード調節をするだけです。これは、

Speed = m_dTotalTime / TotalAnimTime

で計算されます。

C2DAnimationSet:SetTotalAnimationTime関数(実装部)
bool C2DAnimationSet::SetTotalAnimationTime( DOUBLE TotalAnimTime )
{
   // 引数が0以下であればエラー
   if( TotalAnimTime <= 0 )
      return false;

   // 再生スピードを調節する
   m_dSpeed = m_dTotal / TotalAnimTime;

   return true;
}

簡単ですね(^-^)。

 これでI2DAnimationSetインターフェイス及び実装クラスの主要な部分の説明が終わりました。後は設定関数や取得関数なので取るに足りません。I2DAnimationControllerインターフェイスは、I2DAnimationSetインターフェイスを内部に複数持っているに過ぎません。そして、ユーザインターフェイスをすべてパックするだけですから、特に問題は無いでしょう。完全な実装はサンプルプログラムにおいてヘッダーファイル及び実装ファイルの形で公開致し、その使い方の例をサンプルプログラムとして提供します。

 残された課題はI2DAnimationControllerインターフェイスに外部ファイルから得た情報を格納するI2DAnimationLoderインタフェイスですが、この実装をするためには、Xファイルにおけるカスタムインターフェイスの作成方法をみっちりと学ぶ必要があります。次の章で、それについて説明します。