ホーム < ゲームつくろー! < DirectX技術編 < アニメーションの根っこ:アニメーションコントローラをガンガン使う!
その37 アニメーションの根っこ:アニメーションコントーラをガンガン使う!
Direct3DXで固定アニメーション及びスキンメッシュアニメーションを行う時に使うのが「アニメーションコントローラ(ID3DXAnimationController)」です。通常、これはXファイルをD3DXLoadMeshHierarchyFromX関数で読み込むと自動的に取得できるインターフェイスです。兎角面倒なアニメーションを殆ど自動的に行ってくれるのですから大変ありがたいインターフェイスなのですが、その使い方の情報はあまり広まっていない気がします。その理由はDirectX9のマイナーバージョンアップにおいて大幅なインターフェイス変更があったこと、スキンメッシュアニメーションを実装する事自体がえらい大変であることなどが関係しているかと思います。理由はどうあれ、情報が無い事はプログラマにとって辛いものです。
この章ではID3DXAnimationControllerインターフェイスの使い方について、みっちりと見ていくことにしましょう。
@ ID3DXAnimationControllerの仕組み
ID3DXAnimationControllerインターフェイスの説明は、本家のマニュアル内には皆無です。日本語版マニュアルは前バージョンのままでして今や全く使えませんし(怒)、英語版も仕組みの理解を前提とした説明をしています。しかし、私達が知りたいのはまずその「仕組み」です。では、早速このインターフェイスの仕組み(仕様)について見ていく事にしましょう。
アニメーションコントローラはアニメーションのミキサ(合成)をするのが仕事です。DirectXでの「アニメーション(動き)」の本質は、キーフレームと姿勢行列の対の羅列です。これはXファイル内のAnimationSetテンプレート内を見ると一目瞭然です。AnimationSetテンプレートの一部を見てみましょう。
AnimationSetテンプレート内データ例 |
AnimationSet AnimationSet_Walk1 { Animation Animation0 { { Frame1_RightFoot_Layer1 } AnimationKey { 4; 21; 0;16;1.000000,0.000000,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.000000,0.000000,1.000000,0.000000,0.000000,0.000000,0.000000,1.000000;;, 240;16;1.000000,0.000000,0.000000,0.000000,0.000000,0.988186,-0.153262,0.000000,0.000000,0.153262,0.988186,0.000000,0.000000,0.011814,0.153262,1.000000;;, 480;16;1.000000,0.000000,0.000000,0.000000,0.000000,0.946379,-0.323059,0.000000,0.000000,0.323059,0.946379,0.000000,0.000000,0.053621,0.323059,1.000000;;, 720;16;1.000000,0.000000,0.000000,0.000000,0.000000,0.880709,-0.473658,0.000000,0.000000,0.473658,0.880709,0.000000,0.000000,0.119291,0.473658,1.000000;;, 960;16;1.000000,0.000000,0.000000,0.000000,0.000000,0.816501,-0.577345,0.000000,0.000000,0.577345,0.816501,0.000000,0.000000,0.183499,0.577345,1.000000;;, 1200;16;1.000000,0.000000,0.000000,0.000000,0.000000,0.788011,-0.615661,0.000000,0.000000,0.615661,0.788011,0.000000,0.000000,0.211989,0.615661,1.000000;;, 1440;16;1.000000,0.000000,0.000000,0.000000,0.000000,0.820192,-0.572089,0.000000,0.000000,0.572089,0.820192,0.000000,0.000000,0.179808,0.572089,1.000000;;, 1680;16;1.000000,0.000000,0.000000,0.000000,0.000000,0.889672,-0.456601,0.000000,0.000000,0.456601,0.889672,0.000000,0.000000,0.110328,0.456601,1.000000;;, 1920;16;1.000000,0.000000,0.000000,0.000000,0.000000,0.955320,-0.295575,0.000000,0.000000,0.295575,0.955320,0.000000,0.000000,0.044680,0.295575,1.000000;;, 2160;16;1.000000, ... ... |
この例では、AnimationSet_Walk1というアニメーションセットがあることがわかります。この中にはさらに「Animation0」というフレーム(Frame1_RightFoot_Layer1)を動かすためのデータの塊があります。データはAnimationKeyにまとめられていまして、最初の「4」がデータの表現方法(行列)、次の21がキーフレームの数を表します。その下からが姿勢を表す行列の塊です。1行が1つのキーフレームと行列の対を表します。細かく見ますと、最初の「0」がフレーム番号、次の16がその後に続くデータ(行列)の要素数、「1.0....」以降がフレーム0での姿勢を表す行列です。だいたい分かるかと思います。アニメーションセット1つで、キャラクタのある1つの動作を最初から終わりまで完全に記述しています。
アニメーションコントローラは上のようなAnimationSetテンプレート単位でアニメーションを保持します。AnimationSetが複数あれば、その分だけアニメーションセットを保持します。「歩く」「走る」「飛ぶ」「剣を振る」という単位でアニメーションを保持しているというイメージです。
D3DXLoadMeshHierarchyFromX関数でID3DXAnimationControllerインターフェイスを取得した段階で、一番最後のAnimationSetテンプレートのデータがプリセットされます。このアニメーションセットのIDは0番となり、以下最後から2番目が1、3番目が2というように下のアニメーションセットからIDが振られます。この仕様、個人的には「え〜」なのですが、仕様なので仕方ありません。
さて、アニメーションコントローラは動作させるアニメーションセットを置くための「Track(トラック)」を沢山持っています。読み込んだ段階では、Track0番にID0番のアニメーションが置かれています。読み込んだアニメーションは「ストック」されている状態です。それをTrackに置く事で、初めてアニメーションの実行準備が整います。1つのトラックには1つのアニメーションセットを置く事ができます。アニメーションセットを取り替えた場合、前のアニメーションは即座に終了し、新しいアニメーションがすぐに始まります。
先ほどTrackは複数あると申しました。アニメーションコントローラはTrack0とTrack1に別のアニメーションをそれぞれ置くと、両者を合成して出力します。例えば、立ちポーズのアニメーションセットをTrack0に、歩行のアニメーションセットをTrack1に置くと、両方がミックスされます。合成で重要なのが「Weight(重み)」です。これは各トラックのアニメーションの混合比率を表します。例えば、歩き始めは立ちポーズの姿勢の方が強いはずなので、立ちポーズのアニメーションのウェイトを大きくします。時間が進むと徐々に立ちから歩きへのウェイトが強くなり、最終的には立ちポーズの要素は消えてしまいます。これはウェイトの数値を立ちから歩きへ徐々にシフトしていく事で実現できます。合成の必要が無くなったトラックは無効化することもできます。
これで、アニメーションセットの基本動作の大雑把な説明は終わりです。では、さらに細かくて大切な確信に迫っていきましょう。
A やけに秘密裏なトラックスピードの秘密
アニメーションコントローラで一番実体不明・使い始めの第一歩で確実に躓く肝が「トラックスピード」です。簡単に言うとトラックに設定したアニメーションのスピードを調節する数値なのですが、情報が無さ過ぎて何のことやらさっぱりなんです。とりあえず、その設定方法から見ていきます。トラックスピードを設定するには、ID3DXAnimationController::SetTrackSpeedメソッドを用います:
ID3DXAnimationController::SetTrackSpeedメソッド HRESULT SetTrackSpeed(
UINT Track,
FLOAT Speed
);
Trackにはスピードを設定したいトラック番号を指定します。
Speedにはトラックスピードを設定します。
トラックスピードというのは秒ではありません。フレーム数でもありません。アニメーションを相対的に速くしたり遅くしたりするというなんとも曖昧な数値なんです。しかもこれについての説明文はマニュアルに見当たりません。これを紐解くには「アニメーションが何秒で終わるのか」について知る必要があります。
例えば、60フレームで1ループが終了するアニメーションを作ったとしましょう。これはデフォルトスピード1.0において何秒で終了するのでしょうか?実はこのフレーム数と秒の関係もマニュアルのどこにも書かれていないんです(怒×2)。そこで実際に色々とテストしてみまして、その秘密裏な値がわかりました。XファイルをD3DXLoadMeshHierarchyFromX関数で読み込んだ場合、ID3DXAnimationControllerに読み込まれるアニメーションセットは4800フレームを1秒とするようです。これがわかると、諸所の時間計算ができます。
60フレームで1ループするアニメーションは、60/4800=0.0125秒で終了します。ちょっ早ですね(笑)。これを1秒で終了するには、スピードを0.0125倍すれば良いわけです。つまり、
60フレーム/1秒としたアニメーションのスピード調節値の算出 FLOAT AdjustSpeed = 60.0 / 4800;
pAnimationController->SetTrackSpeed( AdjustSpeed );
と設定すると、ちょうど1秒で60フレーム分進むようになります。もう少し種明かしをしますと、実はアニメーションセットを格納しているID3DXAnimationSetインターフェイスには、ID3DXAnimation::GetPeriodメソッドという1ループにかかる時間を取得するメソッドが用意されています。上の例だと0.0125秒はこのメソッドで取得できるんです。ということは、あるアニメーションを自分の指定した時間で終了させるには、次のような実装をすれば良いことになります:
任意のアニメーションを設定時間で1ループさせるトラックスピードの設定 FLOAT LoopTime = 3.0f; // 3秒ループ
FLOAT AdjustSpeed = pAnimationSet->GetPeriod() * LoopTime;
pAnimationController->SetTrackSpeed( AdjustSpeed );
これさえ覚えておけば、アニメーションの時間管理は思いのままです!
B ローカル時間とグローバル時間の秘密
例えば3秒で1ループするアニメーションがあった場合、アニメーションコントローラは任意の時間での姿勢を自動的に算出してくれます。これはキーフレームからの線形補間が使用されます。細かい事は今は保留します。「任意の時間」をもっとしっかりと定義しますと、アニメーションコントローラの中にはアニメーションに関係する「グローバル時間」と「ローカル時間」という2つの時間が存在します。この違いを理解するのもアニメーションコントローラを使いこなすために必要になります。
グローバル時間はアニメーションコントローラ自体が蓄積する時間で、ID3DXAnimationController::AdvanceTimeメソッドによってのみ加算されます。グローバル時間を直に設定するメソッドはありませんが、リセットするメソッドはあります(ID3DXAnimationController::ResetTimeメソッド)。実は、このグローバル時間は姿勢を決定する計算には直接使われません。単に現在どこまで時間が進んでいるかを表すタイマーなんです。
一方、各トラックごとに保持されるローカル時間は、アニメションの姿勢決定に直接関係します。例えば3秒で1ループするアニメーションならば、ローカル時間が3秒経過するごとに最初の姿勢に戻ります。ローカル時間は、トラックが有効である場合に限りAdvanceTimeメソッドによってグローバル時間同様に加算されます。またグローバル時間と違い、アニメーションコントローラにはローカル時間を直接設定するID3DXAnimationController::SetTrackPositionメソッドが用意されています。つまり、AdvanceTimeメソッドが差分時間(相対時間)、SetTrackPositionが絶対時間を設定するわけです。
ここで最重要点があります。ローカル時間の設定値をアニメーションに反映させるには必ずAdvanceTimeメソッドを呼ぶ必要があります。ですから、SetTrackPositionメソッドで絶対時間を変更しても、即座にアニメーションには反映されないわけです。これを知らないとキャラクタが動かずにパニックになります(笑)。トラックに絶対時間を指定した場合、それを反映させるためにはAdvanceTimeメソッドに0を与えます。これ、ポイントです(^-^)
C アニメーションコントローラのコピーの影響
ID3DXAnimationControllerインターフェイスにはCloneAnimationControllerメソッドがあります。名前の通りアニメーションコントローラをコピー(クローン)するメソッドです。アニメーションコントローラはアニメーションセットをストックしています。グローバル時間やトラックごとのローカル時間も保持しています。さらに背後ではフレーム行列(ボーン行列)とも関連しているはずです。で、このコピーがいったい何をコピーしているのかですが、マニュアルにはやっぱりありません。そこで色々調べてみました。
まず、CloneAnimationControllerメソッドによって返されるID3DXAnimationControllerインターフェイスの参照カウンタは1です。これは、インターフェイス自体はディープコピーである事を表しています。コピーされたアニメーションコントローラには、コピー元のアニメーションセットが格納されます。この参照カウンタを調べてみると少なくとも1ではありませんでした。これはアニメーションセットを参照している事になります。次に、コピー元のアニメーションコントローラをある程度進めた段階でコピーをし、直後にコピーインターフェイスからグローバル時間をGetTimeメソッドで取得してみると0.0を返しました。これは「コピーインターフェイスのグローバル時間は独立」であることを示しています。同様にトラックのローカル時間も独立していました。
では、アニメーションコントローラに関連付けられているフレーム構造体はどうなっているのでしょうか?そこで、コピーインターフェイスのAdvanceTimeメソッドに差分時間を与えて見たところ、「コピー元のフレーム行列が変更されてアニメーションされる」ことがわかりました。つまり、フレーム行列はシャローコピー(参照コピー)されているんです。これは、非常に重要な意味をはらんでいます。
コピー内容をまとめてみますと、
項目 コピーor参照 アニメーションコントローラ コピー 登録アニメーションセット 参照 フレーム行列 参照 グローバル時間 コピー(0に初期化) ローカル時間 コピー(0に初期化)
です。ここからアニメーションコントローラ・・・いや、ゲームオブジェクトの使い方が見えてきます。
D3DXLoadMeshHierarchyFromX関数によって取得したフレーム行列(D3DXFRAME構造体)には複数の独立した時間を持つアニメーションコントローラ(ID3DXAnimationControllerインターフェイス)が関連する事になります。よって、1つのゲームオブジェクトが1つの独立したフレーム行列を持つという図式は成立しません。言い換えれば「1つのゲームオブジェクトが自身の姿勢を行列の形で保持できない」事を示しています。変わりに、1つ1つのオブジェクトの姿勢はアニメーションコントローラによる「独立したローカル時間」で表現される事になるんです。ゲームオブジェクトをクラス化する時に、これを知っておかないと設計は多分大失敗してしまいます。この章はアニメーションコントローラのお話しなので、これ以上の詳細なお話しは次章にまわす事にしますが、クラス設計時にこのコピーの実体を反映させておく事が極めて重要なんです!
D アニメーション切り替え上位レベルインターフェイス
ここまで説明してきたように、アニメーションコントローラは大変に高機能なんですが、少し低レベルアクセスな感があります。自由度が高い分設定項目が多くて使いにくいわけです。細かい事をしないのであれば、もう少し高レベルアクセスになると気楽です。そこで、ここではアニメーションコントローラを扱いやすくするための上位レベルインタフェースを作成してしまいましょう。
私達が一番楽に行いたいのはアニメーションの切り替えです。究極的には、現在0番のアニメーションを実行中だとして、「1番に切り替え!」と命令すると自然に1番に切り替えてくれる。そんな仕組みが整うとアニメーションは相当に使いやすくなります。インターフェイスの目指すのはそこです。
では、細かな設計を見ていきましょう。アニメーションを実行し、また切り替えるのに必要な時間の情報は、
・ 動作開始にかかる時間
・ 1ループにかかる時間
くらいです。動作開始にかかる時間とは、切り替える前のアニメーションから今のアニメーションに切り替わるまでにかかるシフト時間の事です。1ループにかかる時間は問題ありませんね。この辺りをうまく操作して、ユーザからその動作を隠蔽するようなインターフェイスの宣言部は、例えば次のようになります:
アニメーション上位レベルインターフェイス interface IHighLevelAnimController
{
public:
// アニメーションコントローラを設定
bool SetAnimationController( ID3DXAnimationController *pAnimCont );
// アニメーションコントローラを取得
bool GetAnimationController( ID3DXAnimationController **ppAnimCont );
// ループ時間を設定
bool SetLoopTime( UINT animID, FLOAT time );
// 動作開始にかかる時間を設定
bool SetShiftTime( UINT animID, FLOAT interval );
// アニメーションを切り替え
bool ChangeAnimation( UINT animID );
// アニメーションを更新
bool AdvanceTime( FLOAT time );
};
実にシンプルですよね。後はこれを実現する実装をするだけです。
実装する前に、1つのアニメーションに必要な情報をまとめた構造体を作っておきます。上のメソッド設定からして、構造体に必要な最低限のメンバは次のようになりそうです。
アニメーション上位レベル構造体 struct HLANIMATION_DESC
{
UINT uiAnimID; // アニメーションID
ID3DXAnimationSet *pAnimSet; // アニメーションセット
FLOAT fLoopTime; // 1ループの時間
FLOAT fTrackSpeed; // トラックスピード調節値
FLOAT fShiftTime; // シフトするのにかかる時間
FLOAT fCurWeightTime; // 現在のウェイト時間
};
uiAnimIDはアニメーションコントローラのアニメーションセットIDに対応した番号です。
pAnimSetはアニメーションセットインターフェイスへのポインタです。IDと対応しています。
fLoopTimeはアニメーションが1回ループするのにかかる時間を保持します。
fTrackSpeedはこの構造体が持つアニメーションセットをトラックに設定する時にfLooptimeでループが終了するように調節するトラックスピードの値です。計算でも求められますが、毎度計算するのも面倒なので保持しておきます。
fShiftTimeはこのアニメーションに切り替えてから切り替えが終了するまでにかかる合成時間です。
fCurWeightTimeは切り替え中の時間です。アニメーションセットのウェイトを計算するのに使われます。
これと似た構造体としてDirect3DにはD3DXTRACK_DESC構造体が用意されているのですが、上はもう少し情報を増やしてこのインターフェイスに特化させています。
○ ループ時間を設定(SetLoopTimeメソッド)
1つのアニメーションのループ時間を設定します。アニメーションセットに登録されているアニメーションのデフォルト時間はID3DXAnimationSet::GetPeriodメソッドで取得する事ができます。SetLoopTimeメソッドでは1ループの実質時間を指定し、内部でトラックに設定するトラックスピードを算出します。トラックスピードは、
[トラックスピード] = [デフォルト時間] / [実質時間]
で計算が出来ます。
SetLoopTimeメソッド bool CHighLevelAnimController::SetLoopTime( UINT animID, FLOAT time )
{
// 指定のアニメーションIDの存在をチェック
if( m_Anim.size() <= animID )
return false;
// トラックスピード調節値を算出
FLOAT DefTime = m_Anim[animID].pAnimSet->GetPeriod();
m_Anim[animID].fLoopTime = time;
m_Anim[animID].fTrackSpeed = DefTime / time;
return true;
}
○ 動作開始にかかる時間を設定(SetShiftTimeメソッド)
アニメーションが切り替わった時に、その動作に完全に移行するまでの時間を設定します。このメソッドではただ単にシフト時間を格納するだけです。
SetShifTimeメソッド bool CHighLevelAnimController::SetShiftTime( UINT animID, FLOAT interval )
{
// 指定のアニメーションIDの存在をチェック
if( m_Anim.size() <= animID )
return false;
// シフト時間を登録
m_Anim[animID].ShiftTime = interval;
return true;
}
○ アニメーションの切り替え(ChangeAnimationメソッド)
現在のアニメーションから引数で指定したアニメーションに切り替える「準備」をします。ここでは動作するメイントラックは常に0番としています。よって、アニメーションを切り替える時には、現在のアニメーションをTrack1に移行し、引数のアニメーションIDに対応するアニメーションセットをトラック0に新規設定します。
ChangeAnimationメソッド bool CHighLevelAnimController::ChangeAnimation( UINT animID )
{
// 指定のアニメーションIDの存在をチェック
if( m_Anim.size() <= animID )
return false;
// 異なるアニメーションであるかをチェック
if( m_CurAnimID == animID )
return false;
// 現在のアニメーションセットの設定値を取得
D3DXTRACK_DESC TD; // トラックの能力
m_AnimCont->GetTrackDesc( 0, &TD );
// 今のアニメーションをトラック1に移行し
// トラックの設定値も移行
m_AnimCont->SetAnimationSet( 1, m_Anim[m_CurAnimID].pAnimSet );
m_AnimCont->SetTrackDesc( 1, &TD );
// 新しいアニメーションセットをトラック0に設定
m_AnimCont->SetAnimationSet( 0, m_Anim[animID].pAnimSet );
// トラック0のスピードの設定
m_AnimCont->SetTrackSpeed( 0, m_Anim[animID].fTrackSpeed );
// トラックの合成を許可
m_AnimCont->SetTrackenable( 0, true );
m_AnimCont->SetTrackenable( 1, true );
// ウェイト時間を初期化
m_Anim[animID].fCurWeightTime = 0.0f;
// 現在のアニメーション番号を切り替え
m_PreAnimID = m_CurAnimID;
m_CurAnimID = animID;
return true;
}
トラックを変更した後にトラックスピードを再設定し、両方のトラックを有効にします。これで合成が行われるわけです。さらにウェイト時間を初期化し、アニメーション時間の更新に備えます。最後に現在のアニメーション番号を切り替えてこのメソッドの役目は終了です。
○ アニメーションを更新(AdvanceTimeメソッド)
現在の設定を元にアニメーションを指定時間分だけ更新します。基本的にはアニメーションコントローラのID3DXAnimationController::AdvanceTimeメソッドを利用します。更新状態は2種類あります。1つは合成の最中、もう1つは合成後の通常アニメーションです。アニメーション切り替えの最中はウェイトを計算して与えます。
ChangeAnimationメソッド bool CHighLevelAnimController::AdvanceTime( FLOAT time )
{
// 合成中か否かを判定
m_Anim[m_CurAnimID].fCurWeight += time;
if( m_Anim[m_CurAnimID].fCurWeight <= m_Anim[m_CurAnimID].fShiftTime )
{
// 合成中。ウェイトを算出
FLOAT Weight = m_Anim[m_CurAnimID].fCurWeight / m_Anim[m_CurAnimID].fShiftTime;
// ウェイトを登録
m_AnimCont->SetTrackWeight( 0, Weight ); // 現在のアニメーション
m_AnimCont->SetTrackWeight( 1, 1 - Weight ); // 前のアニメーション
}
else
{
// 合成終了中。通常アニメーションをするTrack0のウェイトを最大値に
m_AnimCont->SetTrackWeight( 0, 1,0f ); // 現在のアニメーション
m_AnimCont->SetTrackEnable( 1, false ); // 前のアニメーションを無効にする
}
// 時間を更新
m_AnimCont->AdvanceTime( time, NULL );
return true;
}
最初に現在のアニメーションIDのウェイト時間に引数の差分時間を足して、その時間がシフト時間を超えたか否かで合成中か否かを判定します。合成中だった場合、現在のアニメーションのウェイトを算出します。これは単に[現在のウェイと時間]/[シフト時間]です。算出したウェイトを現在のアニメーショントラック(Track0)に、相反するウェイトを前のアニメーショントラック(Track1)に設定して、ID3DXAnimationController::AdvanceTimeメソッドでアニメーション時間を更新します。一方合成がすでに完了している場合は、現在のアニメーションのウェイトを最大値(1.0f)にして、前のアニメーショントラックを無効してしまいます。
細かなエラーチェックや設定されたアニメーションコントローラの参照カウンタ部分は省いていますが、大方の実装はこんな感じです。この位の実装をするだけで、アニメーションの切り替えが究極的に楽になるのですから、アニメーションコントローラ様様です(^-^)
C 線形補間とアニメーション切り替えのタイミング
アニメーションコントローラは、任意のローカル時間での姿勢を自動計算してくれます。この時使われるのが「線形補間」です。2つのフレームの間の姿勢を直線で結んで、特定の割合の値を算出します。これは案外重要な「制約」なんです。3Dモデリングツールで動作をつけている時は、キーフレームを頼りにそのモデリングツール独自の高度な補間方法(スプラインなど)が取られます。Xファイルに出力する時に、キーフレームのみを書き出すようにしてしまうと、Direct3Dに読み込んだ時にその補間方法の違いが影響して物凄くギクシャクとした動きになってしまいます。これをなるべく防ぐには、Xファイルでの出力時にキーフレームだけでなく、その間のフレームもサンプリングして出力するようにします。こういう機能は各3DモデリングツールのXファイルエクスポーターに大抵ついています。
線形補間はアニメーションの「合成」にも使われます。そして、これこそが最大の「難点」にもなります。例えば、走るアニメーションから歩くアニメーションに切り替える時、シフト時間を長く取ると、走っている時の早い歩幅と歩く時のゆったりとした歩幅が合わずに、非常にみすぼらしい合成になってしまいます。正直合成しない方がマシだと思うくらい酷い事になります。それを防ぐには色々と手段があります。例えば、シフト時間を非常に短くします。そうすると、前のアニメーションの影響がすぐになくなるため、変な合成が目立たなくなります。また、前のアニメーションのウェイトを意図的に小さくするのも調節方法として効果的です。最近のゲームをよ〜く観察すると、ある動作から別の動作に移行する時に、その中間姿勢をはさんでいるゲームもありますし、動作がある位置まで戻った段階で切り替えているゲームもあります。どのゲームも違和感を出さないために職人技ともいえる工夫を凝らしているようです。
アニメーションコントローラを使いこなせるようになれば、「ゲームらしさ」が一気に向上します。ゲームパッドと組み合わせてキャラクタを自由に移動させられるようにすると、もう嬉しくてたまらなくなります。この段階まで来て、ようやく3Dゲームの本質を開始できるかもしれません。大変難儀な道ですが、本サイトでできる限りの説明をしておりますので、「目指せ大規模本格ゲーム」に向けて是非ともマスターしてみて下さい!