Skip to content
第17回 加速度、摩擦、移動の端数

4 / 4 操作感を数値から比較する

このステップの目的

加速度、最高速度、摩擦の3つのパラメータを個別に変更し、プレイヤーの立ち上がり、最高速度、および停止距離に与える影響を比較・検証します。

3つのパラメータが移動に与える影響

GameTuning.h に定義されている3つの数値を変更することで、操作感を調整できます。

GameTuning.hcpp
namespace dxgame::platformer::tuning::player
{
    // 水平方向の最高速度(ピクセル/秒)
    inline constexpr float MaxSpeed = 240.0f;

    // 水平方向の加速度(ピクセル/秒^2)
    inline constexpr float Acceleration = 1260.0f;

    // 水平方向の摩擦減速度(ピクセル/秒^2)
    inline constexpr float Friction = 1500.0f;
}

これらのパラメータは、それぞれ以下の役割を持ちます。

パラメータ小さくした場合大きくした場合設計上の役割
Acceleration(加速度)最高速度に達するまで時間がかかる素早く最高速度に達する入力への応答性の調整
MaxSpeed(最高速度)全体の移動速度が低い全体の移動速度が高いゲームの進行テンポの調整
Friction(摩擦)キーを離しても長く滑るキーを離すと素早く停止する操作の制動距離の調整

1箇所ずつの比較とデバッグ表示による数値確認

パラメータを調整する際は、**「一度に複数の数値を同時に変更しない」**という原則を守ります。
AccelerationFriction を同時に変更すると、操作感の変化がどちらの数値によるものか判別が困難になります。

デバッグ表示への速度出力

プレイヤーの現在速度を数値で確認するため、F1 のデバッグ表示に velocity_ の値を出力します。

Game.cppcpp
if (debugVisible_)
{
    const Vector2 velocity = player_.GetVelocity();
    DrawFormatString(14, 104, GetColor(255, 255, 255),
        "Velocity: X=%.1f, Y=%.1f", velocity.x, velocity.y);
}

動作確認の手順

プロジェクトをビルドして実行し、以下の項目を順に確認します。

  1. 加速の確認:
    • 左右キーを押した際、速度が 0 から最高速度(240.0)まで連続して増加することを確認します。
  2. 減速停止の確認:
    • 最高速度で移動している状態でキーを離し、速度が徐々に落ちて 0 で静止することを確認します。
  3. 切り返しの確認:
    • 右移動中に左キーを入力した際、減速して速度が 0 を通過し、反対方向へ加速することを確認します。
  4. 低速時の微小移動の確認:
    • キーを短く入力した際、端数蓄積によって数ピクセル移動できることを確認します。
チェックリスト
  • 加速度、最高速度、摩擦の3つのパラメータの役割の違いを説明できる
  • キー入力開始から最高速度まで滑らかに加速することを確認できる
  • キーを離した際、摩擦によって0を通り越さずに停止することを確認できる
  • 左右の切り返しがスムーズに行われることを確認できる

まとめと次回への引き継ぎ

第17回では、物理運動の基礎である加速度、摩擦、そして微小移動を保持する端数蓄積処理を実装しました。これで水平方向の滑らかな移動挙動が整いました。

次回(第18回)は、垂直方向(Y軸)の運動として「重力、ジャンプ、着地」を実装します。重力加速度を適用して落下させ、足元の床タイルを検知して着地(位置補正)させることで、ジャンプアクションの基本構造を構築します。

参考実装

今回追加・変更するファイルの一例です。宣言や補助処理を確認したいときに開いてください。自分のコードへ必要な変更を反映し、既存の調整値や素材をそのまま上書きしないようにします。

Source/Platformer/Game.cpp
Source/Platformer/Game.cppcpp
#include <Windows.h>
#include <string>

#include "DxLib.h"

#include "Config/ApplicationConfig.h"
#include "Platformer/Game.h"
#include "Platformer/Tuning/GameTuning.h"

namespace dxgame::platformer
{
    bool Game::Initialize()
    {
        if (!images_.LoadAll())
            return false;

        constexpr wchar_t StagePath[] = L"Assets/Maps/Stage01.csv";
        if (!stage_.Initialize(StagePath))
        {
            const std::wstring message = std::wstring(StagePath) + L"\n" +
                stage_.GetLastLoadError();
            MessageBoxW(nullptr, message.c_str(), L"Map Load Error", MB_OK);
            return false;
        }

        ResetStage();
        return true;
    }

    void Game::MainLoop()
    {
        // 小数分の待ち時間を次フレームへ持ち越す
        double nextFrame = GetNowCount();

        while (ProcessMessage() == 0)
        {
            input_.Update();
            if (input_.IsHeld(Key::Escape))
                break;

            Update(dxgame::config::DeltaTime);

            ClearDrawScreen();
            Draw();
            ScreenFlip();

            nextFrame += dxgame::config::FrameMilliseconds;
            const int waitMilliseconds =
                static_cast<int>(nextFrame - GetNowCount());

            if (waitMilliseconds > 0)
                WaitTimer(waitMilliseconds);
            else
                nextFrame = GetNowCount();
        }
    }

    void Game::Update(float deltaTime)
    {
        UpdateDebugToggle();
        player_.Update(input_, deltaTime);
        camera_.Update(player_.GetPosition(), stage_.GetWorldWidth());
    }

    void Game::Draw() const
    {
        DrawBackground();
        stage_.Draw(camera_.GetX(), images_);
        player_.Draw(camera_, images_);
        DrawHud();
        DrawDebug();
    }

    void Game::DrawBackground() const
    {
        images_.Draw(ImageId::Background, { 0.0f, 0.0f,
            static_cast<float>(dxgame::config::ScreenWidth),
            static_cast<float>(dxgame::config::ScreenHeight) });
    }

    void Game::DrawHud() const
    {
        const int textColor = GetColor(255, 255, 255);
        DrawString(12, 10, L"LEFT/RIGHT: MOVE", textColor);
        DrawString(12, 30, L"Z: JUMP", textColor);
        DrawString(12, 50, L"F1: DEBUG    ESC: QUIT", textColor);
    }

    void Game::DrawDebug() const
    {
        if (!debugVisible_)
            return;

        const dxgame::Rect playerDrawRect =
            camera_.WorldToScreen(player_.GetDrawRect());
        DrawBox(static_cast<int>(playerDrawRect.x),
            static_cast<int>(playerDrawRect.y),
            static_cast<int>(playerDrawRect.x + playerDrawRect.width),
            static_cast<int>(playerDrawRect.y + playerDrawRect.height),
            GetColor(80, 240, 255), FALSE);

        const dxgame::Rect playerRect =
            camera_.WorldToScreen(player_.GetHitbox());
        DrawBox(static_cast<int>(playerRect.x), static_cast<int>(playerRect.y),
            static_cast<int>(playerRect.x + playerRect.width),
            static_cast<int>(playerRect.y + playerRect.height),
            GetColor(255, 80, 80), FALSE);

        const dxgame::Rect goalDrawRect =
            camera_.WorldToScreen(stage_.GetGoalDrawRect());
        DrawBox(static_cast<int>(goalDrawRect.x),
            static_cast<int>(goalDrawRect.y),
            static_cast<int>(goalDrawRect.x + goalDrawRect.width),
            static_cast<int>(goalDrawRect.y + goalDrawRect.height),
            GetColor(80, 240, 255), FALSE);

        const dxgame::Rect goalRect =
            camera_.WorldToScreen(stage_.GetGoalTrigger());
        DrawBox(static_cast<int>(goalRect.x), static_cast<int>(goalRect.y),
            static_cast<int>(goalRect.x + goalRect.width),
            static_cast<int>(goalRect.y + goalRect.height),
            GetColor(255, 220, 40), FALSE);

        DrawBox(8, 76, 300, 178, GetColor(0, 0, 0), TRUE);
        const int textColor = GetColor(255, 255, 255);
        const dxgame::Vector2& position = player_.GetPosition();
        const dxgame::Vector2& velocity = player_.GetVelocity();
        DrawFormatString(14, 84, textColor,
            L"pos: %.1f, %.1f", position.x, position.y);
        DrawFormatString(14, 104, textColor,
            L"velocity: %.2f, %.2f", velocity.x, velocity.y);
        DrawFormatString(14, 124, textColor, L"camera: %d",
            camera_.GetX());
        DrawFormatString(14, 144, textColor, L"csv: %d",
            stage_.IsLoadedFromCsv() ? 1 : 0);
        DrawString(14, 164,
            L"cyan: draw  red: player  yellow: goal", textColor);
    }

    void Game::UpdateDebugToggle()
    {
        if (input_.IsPressed(Key::Debug))
            debugVisible_ = !debugVisible_;
    }

    void Game::ResetStage()
    {
        player_.Reset(stage_.GetPlayerSpawn());
        camera_.Update(player_.GetPosition(), stage_.GetWorldWidth());
    }

}
Source/Platformer/Player.cpp
Source/Platformer/Player.cppcpp
#include <algorithm>

#include "Platformer/Camera.h"
#include "Platformer/ImageManager.h"
#include "Platformer/Input.h"
#include "Platformer/Player.h"
#include "Platformer/Tuning/GameTuning.h"

namespace dxgame::platformer
{
    void Player::Reset(const dxgame::Vector2& position)
    {
        position_ = position;
        velocity_ = {};
        remainderX_ = 0.0f;
    }

    void Player::Update(const Input& input, float deltaTime)
    {
        const bool moveLeft = input.IsHeld(Key::Left);
        const bool moveRight = input.IsHeld(Key::Right);

        if (moveLeft && !moveRight)
        {
            velocity_.x -= tuning::player::Acceleration * deltaTime;
        }
        else if (moveRight && !moveLeft)
        {
            velocity_.x += tuning::player::Acceleration * deltaTime;
        }
        else if (velocity_.x > 0.0f)
        {
            velocity_.x = std::max(0.0f,
                velocity_.x - tuning::player::Friction * deltaTime);
        }
        else if (velocity_.x < 0.0f)
        {
            velocity_.x = std::min(0.0f,
                velocity_.x + tuning::player::Friction * deltaTime);
        }

        velocity_.x = std::clamp(velocity_.x,
            -tuning::player::MaxSpeed, tuning::player::MaxSpeed);

        remainderX_ += velocity_.x * deltaTime;
        const int pixelMove = static_cast<int>(remainderX_);
        remainderX_ -= static_cast<float>(pixelMove);
        position_.x += static_cast<float>(pixelMove);
    }

    void Player::Draw(const Camera& camera, const ImageManager& images) const
    {
        images.Draw(ImageId::Player,
            camera.WorldToScreen(GetDrawRect()));
    }

    dxgame::Rect Player::GetHitbox() const
    {
        return tuning::player::Hitbox.MovedBy(position_);
    }

    dxgame::Rect Player::GetDrawRect() const
    {
        return tuning::player::DrawRect.MovedBy(position_);
    }

    const dxgame::Vector2& Player::GetPosition() const
    {
        return position_;
    }

    const dxgame::Vector2& Player::GetVelocity() const
    {
        return velocity_;
    }
}
Source/Platformer/Player.h
Source/Platformer/Player.hcpp
#pragma once

#include "DxGame/Rect.h"
#include "DxGame/Vector2.h"

namespace dxgame::platformer
{
    class Camera;
    class ImageManager;
    class Input;

    // プレイヤーの位置、横速度、移動端数を管理する
    class Player
    {
    public:
        void Reset(const dxgame::Vector2& position);
        void Update(const Input& input, float deltaTime);
        void Draw(const Camera& camera, const ImageManager& images) const;

        dxgame::Rect GetHitbox() const;
        dxgame::Rect GetDrawRect() const;
        const dxgame::Vector2& GetPosition() const;
        const dxgame::Vector2& GetVelocity() const;

    private:
        dxgame::Vector2 position_ = {};
        dxgame::Vector2 velocity_ = {};
        float remainderX_ = 0.0f;
    };
}
Source/Platformer/Tuning/GameTuning.h
Source/Platformer/Tuning/GameTuning.hcpp
#pragma once

#include "Config/ApplicationConfig.h"
#include "DxGame/Rect.h"

namespace dxgame::platformer::tuning
{
    // 距離はピクセル、時間は秒、速度はピクセル/秒、
    // 加速度と重力はピクセル/秒²を基本単位とする。
    // 加速度と重力はピクセル/秒²を基本単位とする
    namespace player
    {
        // 中心座標を基準にした表示範囲と地形判定範囲
        // 画像本来の大きさとは独立して調整する
        inline constexpr Rect DrawRect =
            Rect::Centered(32.0f, 48.0f).MovedBy({ 0.0f, -8.0f });
        inline constexpr Rect Hitbox = Rect::Centered(24.0f, 30.0f);

        // 左右入力中の加速度、最大速度、無入力時の減速度
        // 速度の単位はピクセル/秒、加減速度はピクセル/秒²
        inline constexpr float Acceleration = 1260.0f;
        inline constexpr float MaxSpeed = 270.0f;
        inline constexpr float Friction = 900.0f;

    }

    namespace stage
    {
        // タイル画像の元サイズとは独立した、ワールド上の1マスの大きさ
        inline constexpr int TileSize = 32;

    }

    namespace camera
    {
        // 追従対象を画面内のどのX座標へ置くか
        inline constexpr float TargetScreenX =
            dxgame::config::ScreenWidth * 0.5f;
    }

    namespace enemy
    {
        // 敵の中心座標を基準にした表示範囲と命中範囲
        inline constexpr Rect DrawRect = Rect::Centered(32.0f, 32.0f);
        inline constexpr Rect Hitbox = Rect::Centered(28.0f, 28.0f);
    }

    namespace goal
    {
        // ゴール画像の表示範囲と、クリア成立に使う範囲
        inline constexpr Rect DrawRect = Rect::Centered(32.0f, 32.0f);
        inline constexpr Rect Trigger = Rect::Centered(20.0f, 28.0f);
    }
}

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