Skip to content
第19回 地形との衝突解決

2 / 4 横移動後の壁衝突判定と境界補正

このステップの目的

X軸方向へ移動した命中矩形を調べ、右移動時と左移動時で適切な壁タイルの境界へ押し戻すアルゴリズムを実装します。

左右の移動方向に応じた押し戻し境界

プレイヤーが水平方向(X軸)へ移動した直後、命中矩形がブロック(IsSolid == true)と重なった場合、移動方向に応じて以下のように位置を補正します。

  • 右移動中(moveX > 0:
    ブロックの左側から接触したため、**「接触したタイルの左端」**にプレイヤー矩形の右端を合わせます。
  • 左移動中(moveX < 0:
    ブロックの右側から接触したため、**「接触したタイルの右端」**にプレイヤー矩形の左端を合わせます。
Stage.cppcpp
bool Stage::ResolveHorizontal(dxgame::Rect& rect, int moveX) const
{
    if (moveX == 0)
        return false;

    // 移動後の矩形だけを調べるため、1フレームでタイルを越える
    // 極端な速度ではコリジョンを通り抜ける可能性がある。
    // 極端な速度ではコリジョンを通り抜ける可能性がある
    rect.x += static_cast<float>(moveX);
    const int leftTile = PixelToTile(rect.x);
    const int rightTile = PixelToTile(rect.x + rect.width - 0.001f);
    const int topTile = PixelToTile(rect.y);
    const int bottomTile = PixelToTile(rect.y + rect.height - 0.001f);
    bool hit = false;
    float resolvedX = rect.x;

    for (int y = topTile; y <= bottomTile; y++)
    {
        for (int x = leftTile; x <= rightTile; x++)
        {
            if (!IsSolidTile(x, y))
                continue;

            const float candidate = (moveX > 0)
                ? static_cast<float>(x * TileSize) - rect.width
                : static_cast<float>((x + 1) * TileSize);

            if (!hit ||
                (moveX > 0 && candidate < resolvedX) ||
                (moveX < 0 && candidate > resolvedX))
            {
                resolvedX = candidate;
            }
            hit = true;
        }
    }

    rect.x = resolvedX;
    return hit;
}

std::min / std::max を使用する理由

プレイヤーの命中矩形には高さ(30ピクセル)があるため、壁に接触した際、縦に並ぶ複数のブロックに同時に接触することがあります。
複数接触した場合でも、進行方向に対して最も手前にある境界で停止させる必要があります。
右移動時に rect.x = std::min(rect.x, tileLeft - rect.width) とすることで、複数接触した中で最も左側(手前)の壁境界へ補正されます。左移動時の std::max も同様に、最も右側(手前)の壁境界へ補正するために機能します。

衝突時の速度と端数のリセット

ResolveHorizontal()true(衝突あり)を返した場合、Player クラスは速度と端数をリセットします。

Player.cppcpp
void Player::MoveHorizontal(const Stage& stage, float deltaTime)
{
    remainderX_ += velocity_.x * deltaTime;
    const int pixelMove = static_cast<int>(remainderX_);
    remainderX_ -= static_cast<float>(pixelMove);

    if (pixelMove == 0)
    {
        const int probeMove = velocity_.x > 0.0f ? 1
            : velocity_.x < 0.0f ? -1 : 0;
        if (probeMove != 0)
        {
            dxgame::Rect probe = GetHitbox();
            if (stage.ResolveHorizontal(probe, probeMove))
            {
                velocity_.x = 0.0f;
                remainderX_ = 0.0f;
            }
        }
        return;
    }

    dxgame::Rect movedRect = GetHitbox();
    if (stage.ResolveHorizontal(movedRect, pixelMove))
    {
        velocity_.x = 0.0f;
        remainderX_ = 0.0f;
    }

    position_.x = movedRect.GetCenter().x;
}

端数(remainderX_)もクリアする理由

速度 velocity_.x のみ 0 にして端数 remainderX_ を残すと、次のフレームで端数の蓄積値によって再び壁へ微小にめり込み、毎フレーム押し戻しが発生して画面が細かく振動する原因となります。
衝突が検知された軸では、速度と蓄積端数の両方をクリアすることで安定した停止状態を作ります。

チェックリスト
  • 右移動時はタイルの左端へ、左移動時はタイルの右端へ矩形が押し戻されている
  • 複数のブロックに接触した場合に手前の壁に留まるよう std::min/std::max を使っている
  • 壁衝突時にX速度だけでなく端数 remainderX_ も同時にゼロクリアしている
  • 補正された矩形から位置座標 position_.x が正しく復元されている

ゲームプログラミング実践