GAME
ベクトルによる移動処理(上級編)


HomeProgramming TipsGame Tips[GAME006]

 ダウンロード : ベクトル移動サンプル2(2008/01/26) 

※ 本内容は中級編に引き続いて記載されています。
※ もし、いきなりこちらに来た場合は中級編をご確認いただいた後に
※ 本ページをご覧いただきますようお願いいたします。

さて、中級編で解説したコーナーリング処理のままだとスピードが遅いほど回転半径が小さくなる事に気がついたでしょうか。
ベクトルの世界では単純な計算では意図しない動作になる事が多いので注意が必要です。

速度が遅いと回転半径が小さい ベクトルの長さを半分にしてもまだ少しズレる 円弧の膨らみ分だけ足りない

図Aは速度の違いによる回転半径の違いを表しています。
半分の長さで同じ角度だけ曲がるわけでするから
2倍以上の内側に回り込むのも当たり前というわけです。
では、速度が半分なら回転させる角度も半分にしたらどうなるでしょうか(図B)。

ところがまだちょっとズレていますね。図Cを見ていただいたほうが分かりやすいです。
極端なコーナーリングで描いてみました(本当は角度も問題になるのですがここでは割愛します)。
緑の矢印は赤の矢印のちょうど半分の長さです。

長さを半分にしただけでは円弧の膨らみの分だけ長さが足りないのですね。
実際には図Bの考え方でプログラミングしても殆ど問題は生じません。
そのため、計算量を端折るためこの程度の近似計算でも良いかと私は考えます。

ゲームプログラミングで最も重要なのはレスポンスです。
ユーザーはプログラムを楽しむわけではなくゲームそのものを楽しむのです。
近似的に見た目の事象を同じに出来ればそれで問題解決としています。
故に、このサイトでは、ベクトルによる移動処理をわざわざ Game Tips として紹介しています。

※ 数学的興味を満たすというのであれば、もちろん完璧を求めて計算しても楽しいですね。
※ このあたりは各々の取り組み方や考え方に依存します。私はこう考えているという事で参考まで…。

もっとも鉄道のようにレールの軌道上を走る場合は大問題となります。
図Cの動きを実現するためには、円周上のポイントをどこまで進んだかという
アルゴリズムの変更を行った方が簡単かと思います。
私なら計算が面倒なので直線とエルミート曲線で線路を作った後に、
移動はニュートン法で近似値を見つけて移動させてしまいます。
実行速度という点ではやや不利ですけどね(汗)。

さて図Bの処理について具体的に説明します。
サンプルプログラムも図Bの考え方に沿って作成されています。
この考え方では、単位速度あたりの回転角度を決め、
移動速度に応じて回転角度を変化させる必要があります。
例えば、移動速度の単位ベクトルあたりの回転角度をθとした場合、
移動速度vから実際のコーナーリング角度を v・θ として求めます。単純な乗算ですね。

しかし、回転角度を行列で保持している場合は厄介です。
行列にスカラを単純に乗算しても回転しません。
私もまだまだ勉強不足なので何なんですが、この程度であれば速度と角度を別々に保持するのが良さそうです(汗)。
まあベクトルだと大きさがゼロになると方向情報も失われてしまいますので…。


    TVector2    g_vec2MyPos;            // 現在位置
    float       g_fDirec;               // 移動方向
    float       g_fSpeed;               // 移動速度

    const float c_fUnitDir   =  0.01f;  // 単位回転角度
    const float c_fMinSpeed  =  1.5f;   // 最低移動速度
    const float c_fMaxSpeed  = 30.0f;   // 最高移動速度
    const float c_fStopSpeed =  6.0f;   // 停止時回転速度


単位角度に移動速度を乗算するため、移動速度がゼロだと回転しなくなってしまいます。
※ それはそれで現実社会の自動車とかでは正しいのですが…(汗)。
この対策として、停止時の回転速度用に c_fStopSpeed を設定してあります。
このような条件下で、移動速度が変化しても回転半径が変化しない移動を実現するには、
以下のようなプログラムを記述します。


    // 回転方向を求める(単位角度)
    float fRotate = 0.0f
        + (::GetKeyState(VK_RIGHT) < 0 ? c_fUnitDir : 0.0f)  // 右回転?
        - (::GetKeyState(VK_LEFT)  < 0 ? c_fUnitDir : 0.0f); // 左回転?

    // 回転角を移動速度で確定する
    g_fDirec += fRotate * (g_fSpeed > 0.0f ? g_fSpeed : c_fStopSpeed);

    // 移動する
    TVector2 vec2Direc = GetUnitVector2(g_fDirec);
    g_vec2MyPos = AddVec2(g_vec2MyPos, MultiVec2Scalar(vec2Direc, g_fSpeed));


最高移動速度が決まっているのなら、速度情報を正規化して保持しても良いかと思います。
正規化とは 0.0〜1.0 の数値で全てを表す表現方法です(意訳有り細かい点は気にしないでください)。
この方法だと浮動小数点の誤差を少なくできるようです(未検証)。
まあ私的には、その程度の誤差が出たからといってどうとなるモノでもないのですが、
リアルタイム系ゲームのリプレイを実装する場合に、僅かな誤差が問題となるケースが希にあります。

さて、上級と銘打ちながら数学的には全然上級でない(汗)のですが、
理由として私自身が未だ勉強中という事があります(爆)。
大切なのはベクトルや行列に対して難しそうだという先入観を無くす事と、
それを何に使えるのかという考え方を鍛える事です。


時々、ベクトルや行列を数学的に先に理解しようとする人がいます。
それでは何をすればいいのかさっぱり分かりません。そのため結局何も覚えられません。
DirectX Tips の左右判断ではベクトルの内積計算の性質を利用してます。
何をしたいかが先にあり、そのために何をすればよいのかを学ぶのが大切ではないかと思います。

さて、行列を使わないままというのも上級としては寂しいので、
自機をワイヤフレームっぽくして回転拡大縮小表示させてみますか。
まずは何を表示するのか考えます。サンプルですのでシンプルな形状で考えてみました。

ワイヤで出来たシンプル宇宙船このこの宇宙船は4つの頂点から出来ています。
基点をコックピットや重心位置近辺に取る事が多いです。
このそれぞれの頂点をテーブル化します。
今回は最後は元の位置に戻ります。
もし、戻らないデータなどと混在させるならばもう少し工夫が必要です。
なお、右側に向いているのは、数学的に 0度が右を向いているためです。


    // モデルデータ情報定義
    const int       c_nNumMyShip = 4;
    const TVector2  c_vec2MyShip[c_nNumMyShip] = {
        {  9,  0 },
        { -5, -5 },
        { -3,  0 },
        { -5,  5 },
    };


この座標系は自機の中心を基点とした位置ベクトルとなっています。
これをローカル座標系といいます。
2D や 3D に関わらず物体を回転などの動きを付けたい場合は、
その動かしたい単位毎に位置情報を保持します。

人間の頭で例えれば、世界の中心から腰が誰だけズレているかというローカル座標があります。
その腰を基点として胴体がどこにあるかというローカル座標を計算します。
そして胴体を基点として首がどこにあるかというローカル座標を計算します。
このように次々と連鎖的に計算していく事で最終的に表示される位置が求まります。
3D モデリングツールなどではこれをボーン(骨)などと呼んでいたりします。

3D なんかですと面情報も別に保持しているのですが、
今回は 2D のワイヤフレームですので頂点だけとなります。
この頂点を基点を軸にして、形状が壊れないように全て同じように拡大縮小回転させることで表現します。
ワイヤフレームだと分かりやすいですね。なにしろワイヤがそのままベクトルですから。

拡大縮小回転とデータを変更するわけですから、そのために配列変数領域も確保しておきます。


    TVector2    g_vec2MyShip[c_nNumMyShip];     // 自機の頂点情報


複数のベクトル計算を一気に行う場合は、行列演算が便利です。
行列演算する場合、計算する順序は大切です。
移動して回転と、回転して移動では、結果が大きく異なってしまいます。
数学的にいうと同じ大きさの行列では交換法則は成り立ちません。
このあたりがややこしく感じる部分でもあります。
例えるなら、真っ直ぐ10歩進んでから右に向くのと、右に向いてから真っ直ぐ10歩進むのでは
最終的な位置が全然違うという事です。

スクリーン座標というワールド座標があります。今回は回転させてから平行移動が良さそうです。
下記のサンプルではついでに拡大もさせてみました。
回転行列を作成時に向きをマイナスしているのは、方向を逆転させているためです。
確か右手系と左手系の違いによる回転方向の違いだったと思いますが、
この辺りになると私もちょっと妖しいので深く突っ込まないでください(爆)。


// -------------------------------------------------------------------
// 自分の表示形状を計算する
// -------------------------------------------------------------------
void MakeMyShipModel()
{
    // 自機の頂点演算を行う
    float    fScale    = BASE_SCALE;
    TVector2 vec2Pos   = g_vec2MyPos;
    TMatrix2 mtxRotate = Matrix2Rotation(-g_fMoveDirec);
    TVector2 vec2Temp;
    int i;
    for (i = 0; i < c_nNumMyShip; i++) {

        // 自機を回転させる
        vec2Temp = CalcVec2TransformCoord(c_vec2MyShip[i], mtxRotate);

        // 自機を拡大縮小させる
        vec2Temp = MultiVec2Scalar(vec2Temp, fScale);

        // 自機を移動させる
        g_vec2MyShip[i] = AddVec2(vec2Temp, vec2Pos);
    }
}


まず、基本形状頂点テーブル c_vec2MyShip からデータを取り出しつつ、
先に計算済みの回転行列を乗算します。これにより頂点が回転します。
次に頂点を拡大縮小処理を行った後で自機の移動処理をします。
この最後の処理はワールド座標系に変換している事になります。
回転と拡大縮小は順番を変更しても正しく動作しますが、
ワールド座標系変換だけは最後に行わなければ異常動作となります。
おそらくとんでもない位置に頂点がぶっ飛んで何も表示されないのではないかと思います。

今回のサンプルではたったの4頂点ですので速度的には問題は生じません。
しかし、これが何百万頂点ともなるとどうでしょうか。
この処理のループの中には三角関数が入っていません。
回転行列を先に作成してしまえば、あとはひたすら乗算や加算処理を行うだけで済みますので高速です。
これも行列を使用するメリットと言えます。

頂点の数が多くなってもプログラムの処理自体はほとんど変わりません。
そのため一度表示プログラムを完成させてしまえば、
後はデータ次第で様々なモノを表示させる事が可能となります。

どうでしょう、皆さんもベクトルや行列に親しんでみませんか?

< 初級編に戻る  < 中級編に戻る 



 Copyright 2008 VALGUS. All Rights Reserved.