ゲームプログラミングでのお話です。
ゲームプログラミングでは、通常キャラクターの速度や座標を小数で保持します。
DirectXでは、これらの値を保持するためのベクトルクラスが定義されています。
D3DXVECTOR3などのクラスです。このクラスはx,y,zのメンバ変数を持ち、
速度や座標を表すことができます。
struct D3DXVECTOR3
{
:
FLOAT x;
FLOAT y;
FLOAT z;
:
}
ここで見てほしいのが、メンバ変数x,y,zの型です。型が「FLOAT」なのです。
(※FLOAT型はここではfloat型と同義と考えてください)
なぜdouble型ではなく、FLOAT型が使われているのか?理由は2つあります。(もっとあるかも)
- CPUではdouble型の方が計算速度が速いが、GPUではFLOAT型の方が速い
- double型が8byteでFLOAT型が4byteなのでメモリが少なくてすむ
というわけで、普段プログラミングする際には、変数定義・式の計算はdouble型で行い、
GPUに渡すときには上記構造体に合わせてFLOAT型にキャストして使えばよいでしょう。
FLOAT型で計算を行うと結果がずれる例
もともとこの記事を書こうと思ったのは、自作ゲームのジャンプ処理で、
動きにところどころにおかしなところがあったからです。
うまく口で説明できないのですが、たまに座標が飛ぶというか、原因を探るのに苦労しました。
FLOAT型とdouble型では表現できる幅に違いがあります。
型 | 指数部 | 仮数部 |
FLOAT | 8bit | 23bit |
double | 11bit | 52bit |
FLOAT型は有効桁数が10進数で6.92桁なく、同じくらいの大きさの値の引き算が
行われると有効桁が少なくなるため、誤差が大きく出ることになります。
ゲームのように連続して加算を繰り返す場合は、誤差の積み重ねで座標が大きく
ずれてしまうことがあるため、計算は精度の高いdoubleで行った方がよいでしょう。
ジャンプの計算式
ジャンプの座標は二次関数的に変化させます。
高校の頃に習った「y = -(x-a)^2 + b」という式を使えばいいですね。
ここではx軸を時間として、ジャンプ開始の時刻を原点にしているので、
aおよびbの値を調整して、原点を通る放物線になるようにします。
図にするとこんな感じになります。
↓にソースコードを載せていますが、
要はソースコード内の変数xがx軸、変数g_lfCurJumpHeightがy軸の値になります。
これで放物線を描くジャンプが行えるというわけです。
// 400msで1回ジャンプするときのソースコードサンプル double g_lfCurJumpHeight; // 今回フレームのジャンプの高さ double g_lfPrevJumpHeight; // 前回フレームのジャンプの高さ : // 400msで1回のジャンプが完了するため40.0で割ると、[0,10)の実数値が返る double a = 5.0; double x = (dwCurrentTime - dwStartJumpTime) / 40.0; // 前回ジャンプ時の高さは保存しておく g_lfPrevJumpHeight = g_lfCurJumpHeight; // 原点を通り、x=2aのときy=0となる放物線を描く二次関数 g_lfCurJumpHeight = -(x-a)*(x-a) + a*a;
実際のゲームの処理では、衝突判定の関係で直接座標を操作せず、
(g_lfCurJumpHeight - g_lfPrevJumpHeight)で算出される値を速度として持たせます。
毎フレーム速度を設定して、衝突判定で壁にめりこまないことなどが確定したら、
速度を座標に加算して、座標をアップデートします。(ゲームでは一般的な手法です)
これらの浮動小数の計算過程で結果にずれが発生します。
FLOAT型とdouble型で、それぞれジャンプ時のy座標をプロットした図が以下となります。
FLOAT型の方はところどころ値が大きくずれています。
double型の方はずれが少なくなっています。
なんだこの程度のずれか、と思う方もいるかもしれませんが、
実際にキャラクターの動きを見るとかなり違和感があるのです。
というわけで、CPUで小数計算を行うときはdoubleを使いましょう。