ホーム < ゲームつくろー! < DirectX技術編 < 親と子の座標位置はクラスでどう設定するべきか?

その40 親と子の座標位置はクラスでどう設定するべきか?


 ゲームの中に必ず存在する「ゲームオブジェクト」。キャラクタや石や建物すべてはゲームオブジェクトであり、ゲームを構成する要素です。それらゲームオブジェクトが必ず持ち合わせているのが「座標」です。これは誰でもすぐに気が付く共通項でして、通常ゲームオブジェクトクラスには個々の位置を表す座標を保持するように実装します。

 さて、このゲームオブジェクトクラスがあたりまえのように持っている「座標」。これ、ちょっと考えると「???」と頭をひねりたくなる代物なんです。例えば、皆さんの机の上にあるペンは、皆さんから見ればすぐそこにありますが、会社や学校の入り口を原点(基準)にすれば全然違う座標値になるはずです。皆さんも学校の入り口も、双方ともまったく動いていなのに、ペンが持つ座標だけはなぜだか変えないといけない。それじゃ、ペンが個人情報として持つ座標って何なんでしょうか?親が持った方がいいんじゃないのか?こう考えると、ゲームオブジェクトが座標値を持つべきかどうかというのも検討しなければならない気までしてしまいます。この、何だか良くわからなくなってきた座標のお話しの鍵を握るのが「空間の親子関係」なんです。

 この章では、ゲームオブジェクトの持つ座標の意味をしっかりと整理して、親子関係のあるオブジェクトをうまく表す座標(姿勢)の持ち方について考えてみます。



@ 世の中に絶対基準は無い!!

 冒頭で述べたペンの位置。例えば皆さんが椅子に座って見た時の位置だとしましょう。皆さんが席を立って別の場所に移動すると、当たり前なのですがペンの座標位置は変わってしまいます。


 机の上にあるペンの位置を動かせば、もちろんペンが持つ座標値は変わります。でも、ペンを動かさずに自分を移動させてもペンの座標値は変えないといけないんです。これは、ペンの位置を皆さん基準で決めているからです。結局、ペンに与える座標値というのは「それを扱う親空間にとって意味がある」ことになり、ペンの絶対的な位置を表しているわけではないんです。

 こうなると「ペンの絶対的な位置が知りたい」と思ってしまうかもしれません。しかし、現実の世の中には絶対的な基準となる座標空間がありません。緯度経度は地球上の位置を表すには便利ですが、宇宙空間の基準にはならないでしょう。ですからペンの絶対座標を決めるには、何は無くとも最初に誰かが絶対空間(座標)を取り決めないといけないんです

 「それじゃペンの位置はどう表すんだ?」となりますが、これは「親空間を基準」として表します。そして、もし誰かの基準によって親空間の絶対位置がわかったら、自分の位置も改めて計算してもらうんです。これにより、めでたくペンの絶対位置も後決めできることになります。ちょっと簡単な例を考えて見ましょう。皆さんを基準にしたとき、ペンの位置が(10,5)にあったとします。紆余曲折の結果、皆さんの絶対位置が(100,30)であることがわかったとしましょう(座標のひねりは無いとします)。すると、ペンの絶対位置はその分だけオフセットすれば良いので(100+10, 5+30) = (110,35)となるわけです。親が分かれば子も分かる。この考え方はゲーム製作に直結します。



A ゲームの世界には「絶対座標」が存在する!

 現実の世界には誰かが取り決めない限り絶対座標はありません。一方、ゲームの世界には物凄い絶対座標の候補があります。それはスクリーン座標です。スクリーンに穿たれる点は、ゲームプログラムの最終到達点です。(123,75)に点を打つと言ったら、それはもう絶対そこに打たれます。この現実には無い「絶対的な取り決め」が身近にあるお陰で、ゲームの世界の座標は現実よりもはるかに扱いやすくなっています。

 3Dゲームが台頭するようになって、絶対座標の概念が少し変わりました。ゲームの中に仮想的な空間を設けて、それを絶対座標にしようと考えるようになったわけです。その空間は「ワールド空間」と呼ばれます。絶対座標であるワールド空間は、自身の基準となる親空間をもはや持ちません。そして、全てのオブジェクトを最終的にワールド空間に位置させる事が目標となりました。これは、非常に画期的でわかりやすい概念です。

 では、ペンクラスの座標値は絶対であるワールド空間を基本空間とすれば良いのでしょうか?これは、きっと違います。キャラクタがペンを扱う時に、ワールド空間を基準にしていたら泣けるほど扱いにくいはずです。皆さんの机の上にあるペンを緯度経度で指定した位置に置くのと一緒です。そんな馬鹿げた事はやってられません。ペンを机のどこかに置きたいのであれば「机空間」を基準にした方が絶対的に楽なんです。そして、ペンの絶対座標はあとから机空間から再計算すれば確実に求まるわけです。つまり、ゲーム内でオブジェクトを扱う時にはその基準となる空間内で扱い、絶対位置は後から計算する。この流れが極めて大切なんです。



B クラスに設定する2つの行列

 ゲームオブジェクトをワールド空間に置く事が最終的な目的である事は間違いありません。しかし、ゲームオブジェクトを扱うのは基本空間内の方がはるかに楽です。このことから、ゲームオブジェクトにはワールド空間の位置と、親空間の位置という2つの座標値を置くべきであることが見えてきます。ただ、ゲームオブジェクトは位置だけではなくて回転とスケール変換も含みます。それをひとまとめに表せるのが「座標変換行列」ですから、結局のところゲームオブジェクトクラスはワールド変換行列とローカル姿勢行列の2つを持つのが得策である事がわかります:

ゲームオブジェクトクラス
class CGameObject
{
protected:
   D3DXMATRIX m_WorldMat;   // ワールド空間を基準とした時の姿勢
   D3DXMATRIX m_LocalMat;   // 自身の親空間を基準とした時の姿勢

public:
   void SetLocalMatrix( D3DXMATRIX *pLocalMat );   // 親空間基準の姿勢を設定
   void UpdateWorldMatrix( D3DXMATRIX *pParentWorldMat );  // 親空間のワールド姿勢から自身の姿勢を算出
   void GetWorldMatrix( D3DXMATRIX* pWorldMat);   // ワールド変換行列を取得
};


 m_WorldMatはワールド空間を基準とした絶対姿勢です。描画時にこの値を用いれば、簡単にワールド空間にオブジェクトを置く事が出来ます。
 m_LocalMatは親空間を基準としたローカル姿勢です。オブジェクトを動かしたい時はこの姿勢を直接操作します。

注目するのは「UpdateWorldMatrixメソッド」です。このメソッドは親空間のワールド姿勢行列から自身のワールド姿勢行列を再計算します。このメソッドの実装は次のようになります。

ゲームオブジェクトクラスのワールド変換行列の計算
void CGameObject::UpdateWorldMatrix( D3DXMATRIX *pParentWorldMat )
{
   // 自身のローカル姿勢と引数のワールド変換行列の掛け算
   m_WorldMat = m_LocalMat * (*pParentWorldMat);
};

 この計算の方法は、スキンメッシュアニメーションのボーン操作と全く同じです。親のワールド姿勢行列に自身のローカル姿勢行列を左から掛け算します。「[自身の姿勢]を[ワールド変換]する」という流れとマッチしていますね。この手の行列の計算は、複雑な親子関係を持つオブジェクトには絶対に必要になります。



C 親子階層規定クラス

 上のように考えると、ゲームオブジェクトに明確な親子関係を設定できる仕組みが欲しくなります。と言ってもBで説明したクラスがもうほとんどそうなっているのですが、親子関係を規定する方法が無いことと、全てのオブジェクトが自分の子供のUpdateWorldMatrixメソッドを呼び出さなくてはならないルールの辛さは解消したいわけです。そこで、子オブジェクトは登録制にして、UpdateWorldMatrixメソッドを呼び出したら自分の子供にも全てそれを自動的に伝える仕組みを作ります。やる事は至って簡単です。

 まず子オブジェクトを登録するメソッドを用意します。

ゲームオブジェクトクラス
#include <SmarrtPtr.h>   // スマートポインタ

typedef sp<CGameObject*> SPGameObject   // CGameObjectのスマートポイント化

class CGameObject
{
protected:
   D3DXMATRIX m_WorldMat;   // ワールド空間を基準とした時の姿勢
   D3DXMATRIX m_LocalMat;   // 自身の親空間を基準とした時の姿勢
   list<SPGameObject> m_ChildObjList;   // 子オブジェクトのリスト

public:
   void SetLocalMatrix( D3DXMATRIX *pLocalMat );   // 親空間基準の姿勢を設定
   virtual void UpdateWorldMatrix( D3DXMATRIX *pParentWorldMat );  // 親空間のワールド姿勢から自身の姿勢を算出
   void GetWorldMatrix( D3DXMATRIX* pWorldMat);   // ワールド変換行列を取得
   size_t RegistChildObj( SPGameObject spChildObj );   // 子オブジェクトを登録
};


 RegistChildObjメソッドで自分の子となるオブジェクトを登録します。スマートポインタにしているのは、登録先のオブジェクトの存在を保証するためです(スマートポインタについて詳しくはこちらをご覧下さい)。そしてUpdateWorldMatrixメソッドを次のように書き換えます:

ゲームオブジェクトクラスのワールド変換行列の計算
void CGameObject::UpdateWorldMatrix( D3DXMATRIX *pParentWorldMat )
{
   // 自身のローカル姿勢と引数のワールド変換行列の掛け算
   m_WorldMat = m_LocalMat * (*pParentWorldMat);

   // 子オブジェクトに自身のワールド座標を伝える
   list<SPGameObject>::iterator it;
   for(it=m_ChildObjList.begin(); it!=m_ChildObjList.end(); it++)
      (*it)->UpdateWorldMatrix( &m_WorldMat );

};


 これで、親がワールド変換行列を計算すると、登録した子にも自動的に連鎖していきます。このメソッドを呼ぶタイミングは、子が自分自身のローカル位置を決めた後です。よって、大抵は最後の方での呼び出しになります。



 上のような実装をすることで、オブジェクトに自由な親子関係を結ばせる事ができるようになります。どのような複雑な親子関係でも、大親分の号令一発で全てのワールド変換行列が算出されるわけですから、描画時に何も迷うことなく姿勢決定ができます。親子関係を取りやめたい場合などは、上の実装に取り除くメソッドを追加することで対処できます。また、親を別にしたい場合などはオブジェクトを譲渡(UnderTake)するメソッドを追加します。そういう一連の仕組みを最初にビシッと作っておくと、ゲーム製作は非常にシステマチックにできるようになってきます。

  子オブジェクトのローカル位置を更新する時に、ワールド空間内のオブジェクトの座標を利用したい場合が結構あります(ターゲットの位置など)。その場合は、先にその対象となるオブジェクトのワールド座標位置を計算しておく必要があります。この呼び出しタイミングの調節は、きっと親空間が管理できるはずです。そう考えると、実は上の実装はUpdateの連鎖の一環としてまとめてしまった方が良いかもしれませんね。あ、そんな気もしてきました(笑)