Appearance
第17回 加速度、摩擦、移動の端数
3 / 4 1ピクセル未満の移動を持ち越す
このステップの目的
1フレームあたりの移動量が1ピクセル未満になる微小な移動量を切り捨てずに蓄積し、低速時でも移動が消失しない端数持ち越し処理を実装します。
毎フレーム整数へ丸めることで起きる「移動の消失」
これまでのfloat座標もゲームの位置を表す方法として使えます。この作品では、この後のタイルとの位置補正に合わせて移動量を整数ピクセルにそろえます。位置を保持する型はfloatのままにします。
しかし、浮動小数点の移動量を毎フレーム整数に丸める処理を行うと、低速移動時に問題が発生します。
例えば、プレイヤーの移動速度が毎秒30ピクセルであるとします。
60FPSで動作している場合、1フレームあたりの移動量は次のようになります。
$$\text{1フレームの移動量} = 30 \times \frac{1}{60} = 0.5 \text{ ピクセル}$$
この 0.5 を毎フレーム整数型(int)へキャストして切り捨てると、毎フレーム 0 ピクセルとなり、キーを入力していてもプレイヤーが移動しなくなります。
端数(Remainder)を次回フレームへ持ち越すアルゴリズム
この移動の消失を防ぐために、**端数蓄積(Remainder)**という手法を用います。
1フレームあたりの移動量を端数変数 remainderX_ に蓄積し、**「1ピクセル以上の整数になった分だけを取り出して実移動量とし、残った小数は次のフレームへ繰り越す」**という処理を行います。
Player.hcpp
private:
float remainderX_ = 0.0f; // 1ピクセル未満の移動量の端数保持Player.cppcpp
// 3. 今フレームの移動量を端数プールに蓄積
remainderX_ += velocity_.x * deltaTime;
// 4. 蓄積された値から、1ピクセル以上の整数部分だけを取り出す
const int moveX = static_cast<int>(remainderX_);
// 5. 取り出した整数分を端数プールから差し引く(小数がプールに残る)
remainderX_ -= static_cast<float>(moveX);
// 6. 確定した整数の移動量をプレイヤーの座標に加算する
position_.x += static_cast<float>(moveX);動作の具体例(0.5ピクセルずつ移動する場合)
- フレーム1: 端数に
0.5が加算される。整数部は0なので移動しない。端数残量0.5。 - フレーム2: 端数にさらに
0.5が加算されて1.0になる。整数部1を取り出して 1ピクセル移動する。端数残量0.0。 - フレーム3: 再び
0.5が加算される。移動量0。端数残量0.5。 - フレーム4: 再び
1.0になり、1ピクセル移動する。
このように、2フレームに1回の間隔で1ピクセルずつ移動するため、低速移動であっても移動量が消滅せず、移動が継続します。
また、負の方向(左移動)でも正しく機能させるために、この範囲ではゼロ方向へ小数部分を取り除くintへの変換を使用します($-1.7$ なら整数部 $-1$、端数残量 $-0.7$ となります)。
::: important 衝突時の端数リセット
第19回で壁や床との衝突判定を実装する際、壁に衝突して停止した場合は、速度 velocity_ だけでなく、端数 remainderX_ も同時に 0.0f にリセットする必要があります。端数が残ったままだと、次のフレームで壁方向へ1ピクセル進もうとしてしまうためです。
:::
チェックリスト
- 低速移動時に小数の切り捨てによって移動が消滅する理由を説明できる
-
remainderX_に小数を蓄積し、intへの変換で整数部分だけを取り出す仕組みを理解している - 左移動(負の数値)であっても端数が正しく次回へ持ち越されることを理解している
- 壁衝突時に速度だけでなく端数もリセットすべき理由を説明できる