ホーム < ゲームつくろー! < ゲーム製作技術編 < ゲーム内にキャラクタを本気で「置く」には?
その2 ゲーム内にキャラクタを本気で「置く」には?
ゲーム製作技術編の切り出しは本当に迷いました。冗談抜きで、「その2」と付くお蔵入り記事は10本を越えます(涙)。あれやこれや右往左往し、出てきたのは「置く」という単純で意味深いお話しでした。
ゲーム内には沢山のキャラクタが出現します。それら描画対象となるキャラクタが多分確実に持っている属性は『座標』です。2Dであれ3Dであれ、位置が定まらないと描画のしようがありません。
では、肝心のその位置は誰がどのように決めるのでしょうか?キャラクタは重力を受けるかもしれません。磁石みたいな物に引っ張られるかもしれません。未知の力でワープするかもしれません。もちろん自分で動く事もありますし、プレイヤーが動きの指示を与える事もあります。また障害物に当たるかもしれませんし、もしかすると重力が入ったり切れたりするかもしれません!ゲームは何でもありなんです。そう考えると、たかがキャラクタの位置とは言え、それを決めるというのは実はえらい大変な事なんです。これは、拡張性をきちっと考えた実装を見据える必要があります。
この章では、そんな奥深いキャラクタの位置を決めることをじっくり追求してみます。
@ 位置を決める人がくっついたり離れたり・・・
描画対象となるキャラクタは、間違いなく位置を持ちます。そして、多くのキャラクタは向きも一緒に持つでしょう。これらはキャラクタが持つオリジナルな属性です。もし、そのままそれら属性を変化させなければ、そのキャラクタは未来永劫その位置と向きで描画され続けます:
このキャラクタは未来永劫動きません・・・
さて、動かないキャラクタを眺めていてもちっとも面白くはありません。ゲームキャラクタは動いてなんぼです。キャラが「動く」とは、その座標を変化させて置く事とほぼイコールです。1フレームごとにその位置が変化すれば、人の目にそれは動いているように見えます。ですから、問題は「どうやって位置を変えるか」なんです。
キャラクタを動かす時に一番簡単なのは、キャラクタが向いている方向にずーっと進む事です。いわゆる「等速直線運動」というやつですね。ところが、これは動きのほんの一例に過ぎません。キャラクタは重力によって放物運動をするかもしれませんし、掃除機に吸われて引き寄せられるかもしれません。スプリングでどこかに飛んでいくかもしれません。そしてその間にプレーヤーの操作があるかもしれません。これらの動きにはその場限りのものもあれば、ずっと動かし続けられる物もあります。これは、少なくと1つのキャラクタの位置を決める人は複数いて、入れ替わり立ち代りそれらがキャラクタに関与する事を意味します。
例えば、キャラクタの向いている方向に単純に1単位進める「動かす人(Operator)」がいるとしましょう。この人は、当然動かすキャラクタを知っています:
動かす人は下のオレンジのBOXですよ(^-^;
このキャラクタがある場面に来ると、とある方向から吹く風に流されるとします。これはキャラクタを1フレームごとにどちらかの方向に相対的に移動させる事で再現できます(ちなみに、これを坂の傾斜に置き換えるとキャラクタは坂を転がります(^-^))。ここで2人の「動かす人」が1つのキャラクタに関与する事になります:
2つの動きがあることで、キャラクタはそのミックスされた方向に移動する事になります。もちろん、風が強くなったり弱くなったりすればこの向きも変わります。こうする事でゲームにリアリティと面白さが増すわけです。動く人をどんどん足していくと、非常に複雑な動きにもできます(やり過ぎは駄目ですけど)。そして、必要なくなった動かす人はさっさと退却してもらいます。こうして、キャラクタはその場その場で動的に計算された位置に移動するわけです。
A キャラクタと動かす人を関連付けるのはタスク
話を先へ進めましょう。付け外しが可能な「動かす人」にとって、自分が動かす対象とするキャラクタを知る事が何よりも大切になります。例えば、ある地点にキャラクタが進むと風の影響を受けてうまくまっすぐ進めない。これは明らかにキャラクタに対して風吹き人が「ガチーン」と後付けされています。いったい、この関係を誰が作るのでしょうか?動かす人は知っている人を動かす方法はわかっていますが「誰を動かすか」は自分では大抵判断できないんです。ですから、両者の関係を結ぶもう一人の存在がどうしても必要になってしまうんです。そういう役目をする便利な人は「タスク」をおいて他にいないでしょう。
向いている方向に移動させるタスク、一定の方向にずらすタスク(風吹きタスク)など色々なタスクを場面場面で使ったりはずしたりする事で、キャラクタの動きに変化をつけられます。これは実にすばらしい効果を発揮してくれます。以下のテストプログラムで、それをちょっと体験してみましょう。
C テストプログラム
動きのタスクをつけたりはずしたりすることで、キャラクタの動きに変化を与えられる。そんな感覚を体験するテストプログラムです。この完全版はこちらのサンプルプログラムにあります。まだ基盤が何も無い状態なので、根元から作っていきましょう。
まずキャラクタの位置情報を司るインターフェイスからです。位置情報は現在の座標と進行方向のベクトルがあれば十分でしょう。それらを格納して管理する「位置情報コンポーネント(クラス)」は次のようになります:
位置情報インターフェイス宣言部 // 位置情報インターフェイス
interface IPosInfo
{
public:
virtual void SetPos( float x, float y, float z ) = 0;
virtual void SetDirect( float dx, float dy, float dz ) = 0;
// その他色々と・・・
};
値を設定したり取得したりするメソッドが沢山あるとても単純なインターフェイスです。このインターフェイスを内包して自分の位置と方向を持つのがキャラクタクラスです:
キャラクタクラスインターフェイス宣言部 typedef sp<IPosInfo> SPIPosInfo; // スマートポインタのtypedefです
interface ICharacter
{
protected:
SPIPosInfo m_spPosInfo; // 位置情報コンポーネント
public:
// 位置情報を取得する
SPIPosInfo GetPosInfo(){ return m_spPosInfo;}
// 描画する
virtual bool Draw()=0;
};
位置情報をインターフェイスの形で内包しているので、ICharcter::GetPosInfoメソッドでそのインターフェイスを取得すれば、キャラクタ自身の位置を複数の人で共有できるようになります。この感覚は極めて大切です。もし位置と方向をメンバ変数として持ってしまうと、動かす人は「キャラクタクラス自体」を知る羽目になります。それはきっと知りすぎになります。このクラスは描画メソッドも持ち合わせています。本当は分離したいのですが、テストプログラムなのでこうしておきます。
次に「動かす人」を定義します。動かす人は自分の知っている位置情報を操作する小さなインターフェイスです。位置情報を設定するメソッドと実際に1単位動かすメソッドがあればここでは十分です:
動かす人インターフェイス宣言部 interface IOperator
{
protected:
SPIPosInfo m_Pos; // 位置情報コンポーネント
public:
// 位置情報コンポーネントを設定する
virtual bool SetPosInfo( SPIPosInfo &spPosInfo )
{
if( spPosInfo.GetPtr()==NULL ) return false;
m_Pos = spPosInfo;
return true;
}
// 更新する
virtual bool Update() = 0;
};
今回テストとして、次の3つの動かす人を上のインターフェイスから派生させてみます:
・ キャラクタが現在向いている方向に1単位進めさせるCStraightOperatorクラス
・ 画面内のカーソルの位置に向くようキャラクタの向きを変えさせるCCursorTurnOperatorクラス
・ キャラクタをジャンプさせるCJumpOperatorクラス
すべてを説明するのは冗長になりますので、2つ目のCCursorTurnOperatorクラスの実装だけを見てみる事にします。
CCursorTurnOperatorクラス宣言部 class CCursorTurnOperator : public IOperator
{
protected:
HWND m_hWnd; // 描画先のウィンドウハンドル
public:
// コンストラクタ
CCursorTurnOperator( HWND hWnd ){ m_hWnd = hWnd; }
// カーソルの位置に向かうように更新する
virtual bool Update()
{
if( m_Pos.GetPtr()==NULL) return false;
POINT CursorPos;
D3DXVECTOR3 CurPos;
GetCursorPosition( &CursorPos ); // カーソル位置を取得
m_Pos->GetPos( &CurPos ); // キャラクタ位置
m_Pos->SetDirectX( CursorPos.x-CurPos.x );
m_Pos->SetDirectZ( CursorPos.y-CurPos.z ); // 3Dを意識してます
return true;
}
protected:
// カーソル位置を取得
void GetCursorPosition( POINT *pOut ){
GetCursorPos( pOut );
ScreenToClient(m_hWnd, pOut);
}
};
Updateメソッド内ではキャラクタの現在位置とカーソル位置を取得し、そこからキャラクタが向くべき方向を決定しています。カーソルは2D情報ですが、ここではちょっと3Dを意識しています。キャラクタは通常XZ平面に立ちます。Y軸は高さ方向です。ここではカーソルの座標を(x,z)だとして、キャラクタはその方向を向くように考えています。
カーソル位置の座標はGetCursorPos API関数用います。これで取得できるのはモニターの左上を(0,0)とするスクリーン座標なので、それを描画ウィンドウの座標に変換します。これはScreenToClient API関数を用います。第1引数にウィンドウハンドルが必要になりますので、この動く人の初期化時にウィンドウハンドルを渡しています。
では次に、キャラクタと動く人の関係を結ぶタスクを作ります。タスクの基本クラスは更新メソッドやタスクの状態を取得するメソッドなどを宣言します:
ITaskBaseインターフェイス宣言部 interface ITaskBase
{
protected:
bool m_bUsageFlag; // タスク有効フラグ(trueでない場合タスクがもういらない)
public:
// 更新する
virtual bool Update() = 0;
// タスクの状態をリセットする
virtual void Reset() = 0;
// 現在の状態を取得する
bool CheckUsageFlag(){ return m_bUsageFlag; }
protected:
// 更新条件が揃っているかチェックする
virtual bool AllowUpdate(){ return true; }
};
これは設計の一例です。タスクインターフェイスの設計は人それぞれだと思います。ここではタスクが機能して良いかその更新条件をチェックするAllowUpdateメソッドも設けました。m_bUsageflagはタスク自体の必要性を設定するフラグです。
今回のテストプログラムではキャラクタと動く人を結びつけるIOperationTaskインターフェイスをITaskBaseインターフェイスから派生させます:
IOperationTaskインターフェイス宣言部 class IOperationTask : public ITaskBase
{
protected:
sp<IPosInfo> m_spPos; // 位置情報インターフェイス
sp<IOperator> m_spOpe; // 動かす人インターフェイス
public:
// 参照位置情報を設定する
virtual bool SetPosInfo( sp<IPosInfo> &spPos ) = 0;
// 参照動かす人を設定する
virtual bool SetOperator( sp<IOperator> &spOpe ) = 0;
};
後はこれを実装したCOperationTaskクラスを定義すれば、このタスクが使えるようになります。
最後はシステムです。ここでのシステムの目的はタスクの切り替えをすることです。ちょっと長めですが例えばこうなります:
ITaskSystemインターフェイス宣言部 typedef map< DWORD, sp<ITaskBase> > TASK_MAP;
typedef pair< DWORD, sp<ITaskBase> > TASK_PAIR;
interface ITaskSystem
{
protected:
TASK_MAP m_mapTask; // タスク格納マップ
TASK_MAP m_mapActTask; // 活動中タスク格納マップ
public:
// システムを初期化する
virtual bool Init() = 0;
// システムを更新する
virtual bool Update() = 0;
// タスクのアクティブ・非アクティブを切り替える
virtual bool SwitchTask( DWORD TaskID, bool DoActive=true )
{
// TaskIDの人が活動しているかチェック
TASK_MAP::iterator it = m_mapActTask.find( TaskID );
// TaskIDさんをアクティブにする
if( DoActive && it == m_mapActTask.end() )
{
// TaskIDさんが存在しているかチェック
TASK_MAP::iterator RegIt = m_mapTask.find( TaskID );
if( RegIt != m_mapTask.end() )
{
//-- TaskIDさんを活動させる --//
m_mapActTask.insert( TASK_PAIR( TaskID, RegIt->second ) ); // 活動させる
}
return true;
}
// TaskIDさんを非アクティブにする
if( !DoActive && it != m_mapActTask.end() ){
// マップからの削除作業
m_mapActTask.erase( it );
return true;
}
return false;
}
};
ポイントはタスクのアクティブ・非アクティブを切り替えるSwitchTaskメソッドです。m_mapTaskというマップには活動候補のタスクが格納されています。内外部からの指示によりm_mapTaskにあるタスクが、活動タスクを格納するm_mapAtcTaskマップに移されます。存在しないタスクはここでは考えない事にします。
自分で眺めていてもこのくらいでもうお腹いっぱいになりますね(*_*)。上記のプログラムだけを眺めていてもあまり実感がわかないかもしれません。是非テストプログラムのサンプルを実行して、あ〜なるほどと感じて頂きたいなと思います。サンプルプログラムはこちらで公開致します。
D まとめ
キャラクタを世界に置くためには座標と向きが決まらないといけません。次のフレームで置くべき位置を決定するには、外部からの様々な要因を考慮する必要がでてきます。ですから、キャラクタ自体に自分の位置を計算させるのは多分無理な設計なのだと思います。これを打破すべくあれこれ色々考えていくと、キャラクタを動かす人が別にいて、さらに両者の関係を作り管理するタスクの存在が必要になる事がわかりました。複数のタスクが1つの位置情報オブジェクトに影響を与える事により、それを持つキャラクタに様々な動きをつけることができます。その様子をサンプルプログラムで示しました
さて、「1つのオブジェクトを複数の人が操作する」。この感覚は位置だけに留まりません。ありとあらゆるオブジェクトについて言えるのではないでしょうか。となると、「オブジェクトの関連を作るタスク」をもっともっと掘り下げる必要が出てきます。このタスクもまた非常に奥深いテーマです。次の章からは「タスク」についてじっくり検討してみましょう。