Skip to content

第11回 操作感の設計と改善

前回: 第10回 タイトル画面と状態遷移の整理 / 次回: 第12回 最終アレンジ・デバッグと完成

今回の授業内容

  • floatの速度と1ピクセル未満の移動量を使い、細かな物理値を設定する。
  • 上昇時と落下時で重力を分け、ジャンプの軌道を調整する。
  • ジャンプボタンを離したタイミングで高さを変える。
  • コヨーテタイムと入力バッファによって、入力を受け付ける時間を広げる。
  • デバッグ表示で速度、接地状態、受付時間を確認する。
  • 用意された設定例から選び、遊びやすい操作感へカスタマイズする。

今回の配布コード

第10回までのゲームへ、操作ミスを吸収するジャンプ支援を追加したコードです。
第01回からのプレイヤーと第07回からの敵は、どちらも小数速度と端数移動を使っています。
ここでは同じ仕組みを詳しく確認し、プレイヤーの操作感の調整へ利用します。
座標とタイル衝突は整数ピクセルのまま維持し、速度と端数だけをfloatで扱います。

第11回のコード一式をダウンロード

配布ファイル一覧
ファイル内容
Source/Game.h物理値、受付時間、入力履歴
Source/Game.cpp可変ジャンプ、入力猶予、端数移動
Source/Common.hint版とfloat版のClamp
Source/SoundManager.h第06回から継続する音響管理
Source/SoundManager.cppBGMとSEの再生・解放
Assets/Images/Player.pngプレイヤー画像
Assets/Images/Enemy.png敵キャラクター画像
Assets/Images/Tile.png足場タイル画像
Assets/Images/Goal.pngゴール画像
Assets/Maps/Stage01.csvステージマップ

配布コードには標準の操作感が設定されています。
可変ジャンプ、コヨーテタイム、入力バッファを順番に確認したあと、調整用の定数だけを変更します。

第10回までの機能を残して操作感を変える

今回変更するのは、主にUpdatePlayerInput()UpdatePlayerPhysics()Game.hの物理パラメーターです。

タイトル、敵AI、BGM、クリア、ゲームオーバーは第10回のまま残します。
操作感を変更したあとも、ゲーム開始から結果画面まで動くかを最後に確認します。

1ピクセル未満の移動を蓄積する

プレイヤーの座標とRectは整数のままなので、0.35ピクセルの移動を直接反映できません。
そこで端数をremainderXremainderYへ蓄積し、整数部分をまとめて座標へ反映します。

Game.cppcpp
player.remainderX += move;

const int pixelMove =
    static_cast<int>(player.remainderX);

player.remainderX -= pixelMove;

例えば0.35を3回加えると合計は1.05です。
このとき1ピクセル移動し、残りの0.05を次のフレームへ持ち越します。

Game.cppcpp
void Game::MoveHorizontal(float move)
{
    player.remainderX += move;
    const int pixelMove =
        static_cast<int>(player.remainderX);
    player.remainderX -= pixelMove;

    if (pixelMove == 0)
    {
        Rect probe = PlayerRect();
        const int probeMove =
            (move > 0.0f) ? 1 :
            (move < 0.0f) ? -1 : 0;

        if (probeMove != 0 &&
            stage.ResolveHorizontal(probe, probeMove))
        {
            player.vx = 0.0f;
            player.remainderX = 0.0f;
        }
        return;
    }

    Rect movedRect = PlayerRect();
    if (stage.ResolveHorizontal(movedRect, pixelMove))
    {
        player.vx = 0.0f;
        player.remainderX = 0.0f;
    }
    player.x = movedRect.x;
}

衝突した場合は速度だけでなく端数も0にします。
端数を残すと、壁から離れた直後に意図しない移動が発生します。

整数移動量が0のフレームも、進行方向を1ピクセルだけ調べます。
これにより、壁やマップ端へ入力し続けたときに速度が一時的に残り、待機と走行が交互に切り替わる振動を防ぎます。

通常のキャラクター速度は、タイルサイズより十分小さく制限します。
1フレームでタイルを通過する高速移動には、Swept判定や線分判定を別途使用します。

ジャンプの軌道

上昇と落下の重力を分ける

同じ重力を使い続けると、上昇と落下が対称に近い軌道になります。
落下時の重力を大きくすると、頂点までは滑らかで、頂点を越えた後は素早く着地する動きになります。

Game.cppcpp
const float currentGravity =
    (player.vy < 0.0f) ? riseGravity : fallGravity;

player.vy += currentGravity;
player.vy =
    Clamp(player.vy, -30.0f, maxFallSpeed);
Game.hcpp
static constexpr float riseGravity = 0.45f;
static constexpr float fallGravity = 0.75f;

ボタンを離したら上昇を弱める

ジャンプ開始後も同じ高さまで跳ぶ仕様では、短いジャンプを使えません。
上昇中にZを離した瞬間だけ、上向き速度を弱めます。

Game.cppcpp
if (jumpReleased && player.vy < 0.0f)
{
    player.vy *= jumpCutMultiplier;
}

短く押すと低く、長く押すと高く跳べるため、足場へ着地する位置を調整しやすくなります。

押し続けている間、毎フレーム速度を掛けない点に注意してください。
「離した瞬間」の検出には、現在と前フレームの入力を使います。

Game.cppcpp
const bool currentZ =
    CheckHitKey(KEY_INPUT_Z) != 0;
const bool jumpReleased =
    !currentZ && previousZ;

previousZ = currentZ;

コヨーテタイム

プレイヤーが足場の端を離れた直後でも、数フレームだけジャンプを許可します。
画面上では接地しているように見えているのに入力が間に合わない問題を軽減できます。

Game.cppcpp
if (player.onGround)
{
    player.coyoteFrames = coyoteFrameLimit;
}
else if (player.coyoteFrames > 0)
{
    player.coyoteFrames--;
}

ジャンプ条件にはonGroundではなく、残り時間を使用します。

Game.cppcpp
if (player.coyoteFrames > 0 &&
    player.jumpBufferFrames > 0)
{
    player.vy = jumpPower;
    player.onGround = false;
    player.coyoteFrames = 0;
    player.jumpBufferFrames = 0;
}

今回の6フレームは、60fpsで約0.1秒です。
値を大きくしすぎると空中ジャンプのように見えるため、デバッグ表示とプレイ結果の両方で確認します。

ジャンプ入力バッファ

着地する少し前にZを押した場合、その入力を数フレーム保持します。
保持中に着地すると、次の更新でジャンプを実行します。

Game.cppcpp
if (jumpPressed)
{
    player.jumpBufferFrames =
        jumpBufferFrameLimit;
}
else if (player.jumpBufferFrames > 0)
{
    player.jumpBufferFrames--;
}

キーを押している間ずっと受付時間を戻すのではなく、押した瞬間だけ最大値へ設定します。
このため、押しっぱなしによる自動連続ジャンプを防げます。

接地確認

小数速度では、重力を加えても1ピクセル移動しないフレームがあります。
衝突したフレームだけonGroundを設定すると、接地状態が毎フレーム安定しません。

プレイヤーの1ピクセル下を調べ、毎フレーム接地状態を更新します。

Game.cppcpp
Rect groundProbe = PlayerRect();
groundProbe.y += 1;

player.onGround =
    stage.IsSolidAtRect(groundProbe);

if (player.onGround && player.vy >= 0.0f)
{
    player.vy = 0;
    player.remainderY = 0;
}

デバッグ表示で受付時間を確認する

Game.cppcpp
DrawFormatString(
    14, 180, GetColor(255, 255, 255),
    L"coyote:%d buffer:%d frame:%d",
    player.coyoteFrames,
    player.jumpBufferFrames,
    PlayerFrameIndex());

次の操作を行い、値と結果を対応させます。

操作確認する内容
足場の端から歩いて落ちるcoyoteが0へ減る
落ちた直後にジャンプする残り時間内ならジャンプする
着地直前にジャンプを押すbufferが残り、着地後にジャンプする
上昇中にジャンプを離すvyの絶対値が小さくなる
ジャンプを押し続ける着地後に自動ジャンプしない

パラメーター調整

パラメーター主に変化する感覚
accel動き始め
frictionキーを離した後の停止
maxSpeed横移動の最高速度
jumpPowerジャンプ開始時の勢い
riseGravity上昇時間と頂点までの軌道
fallGravity落下時間
maxFallSpeed長く落下したときの速度
jumpCutMultiplier短く押したジャンプの高さ
coyoteFrameLimit足場を離れた後の猶予
jumpBufferFrameLimit着地前入力の猶予

一度に複数の値を変更すると、どの変更が結果へ影響したのか分かりにくくなります。

  1. 問題が起きる場所と操作を固定する
  2. 変更する値を1つ選ぶ
  3. 関係する値を1つだけ変更する
  4. 同じ操作を行う
  5. 遊びやすくなったかを確認する

設定例から操作感を選ぶ

最初は標準設定で遊び、次に別の設定例を試します。
すべての数値を正確に入力できない場合は、変更したい行だけを書き換えても構いません。

設定accelfrictionmaxSpeedriseGravityfallGravity
標準0.35f0.25f4.5f0.45f0.75f
軽快0.50f0.40f5.5f0.55f0.90f
ゆったり0.20f0.15f3.5f0.35f0.55f

ジャンプの受付時間は、次の組み合わせから選べます。

設定coyoteFrameLimitjumpBufferFrameLimit
受付なし00
標準66
広め1010

値を大きくしすぎると、足場から離れたあとや着地よりかなり前の入力まで受け付けます。
操作しやすさだけでなく、画面上の動きが不自然に見えない範囲で選びます。

実習

実習1: ジャンプの高さを操作する

  1. Zを短く押した場合と長く押した場合を比較する。
  2. jumpCutMultiplierを1項目だけ変更する。
  3. 低い足場と高い足場の両方で確認する。

実習2: 入力猶予を確認する

  1. コヨーテタイムを一度0にして足場端でジャンプする。
  2. 6へ戻して同じ操作を行う。
  3. 着地直前にジャンプを押し、入力バッファを確認する。
  4. F1で受付時間が減る様子を確認する。

実習3: 操作感を比較する

  1. 設定例から1つ選ぶ。
  2. パラメーターを1つ変更する。
  3. 同じ場所をもう一度プレイする。
  4. 自分が遊びやすい設定を残す。
  5. 時間があれば、別の受講者にも操作してもらう。

今回の到達目標

  • 小数の速度を整数座標へ安全に反映できる。
  • 短押しと長押しでジャンプの高さが変わる。
  • コヨーテタイムと入力バッファの違いを動作から確認できる。
  • デバッグ表示で入力猶予の残り時間を確認できる。
  • 設定例から選び、自分が遊びやすい操作感へ変更できる。
  • タイトル、敵AI、クリア、ゲームオーバーが変更後も動作する。

よくある問題

ジャンプを押し続けると自動で跳び続ける

jumpBufferFramesは、キーを押している間ではなく、押した瞬間だけ最大値へ戻します。
jumpPressedcurrentZ && !previousZになっているか確認します。

足場を離れてから長い時間ジャンプできる

coyoteFrameLimitを標準の6へ戻します。
デバッグ表示のcoyoteが毎フレーム減り、0で受付終了になることを確認します。

着地音が1回の着地で何度も鳴る

現在のonGroundだけでなく、更新前のwasOnGroundと比較します。
falseからtrueへ変わった瞬間だけLandを再生します。

変更後に操作しにくくなった

設定例の標準値へ戻します。
配布されたGame.hと比較し、物理パラメーターの行だけを戻しても構いません。

まとめ

  • 小数速度の端数を蓄積し、移動後Rectの境界補正へ接続しました
  • 上昇時と落下時の重力を分けました
  • ボタンを離したタイミングでジャンプ高度を変えました
  • コヨーテタイムと入力バッファで入力の猶予を作りました
  • 感覚だけでなく、速度とタイマーを確認しながら調整しました