Skip to content

第03回 入力と移動

前回: 第02回 ゲームループと描画 / 次回: 第04回 構造体と矩形

今回の授業内容

  • DXライブラリのキー入力でプレイヤーを動かす
  • 押しっぱなしと押した瞬間の違いを、前フレームの入力と比較して判定する
  • 速度を使って毎フレームの移動量を管理する
  • 画面外へ出ないように座標を制限する
  • 入力処理をInputクラスとして分ける

今回の開始状態

第02回終了時点では、プレイヤーの仮図形が画面下部に表示されています
今回の終了時点では、矢印キーまたはWASDで上下左右に動けるようにします

変更するファイル

ファイル変更内容
Input.h入力管理クラスを追加する
Input.cpp現在と前フレームのキー状態を記録する
Game.hInputと移動速度を追加する
Game.cpp入力更新、移動、画面内制限を追加する

Inputクラスを追加する

SourceInput.hInput.cppを追加します
入力の判定には2種類あります
1つはキーを押している間ずっと反応する判定で、移動のように毎フレーム値を変えたい処理に使います
もう1つはキーを押した瞬間だけ反応する判定で、ショットやメニュー選択のように1回だけ処理したい場面に使います
この授業では前者をIsDown()、後者をIsPressed()という名前で区別します

cpp
class Input
{
public:
    void Update();
    bool IsDown(int key) const;
    bool IsPressed(int key) const;

private:
    char current[256] = {};
    char previous[256] = {};
};

currentpreviousという2つの配列を持つのは、「今押しているか」と「1フレーム前も押していたか」を比較しないと、押した瞬間を判定できないためです
押した瞬間とは、「今回は押している」かつ「前回は押していなかった」という状態のことです

Update()では、前回の入力を保存してから現在の入力を読みます

cpp
void Input::Update()
{
    for (int i = 0; i < 256; ++i)
        previous[i] = current[i];

    GetHitKeyStateAll(current);
}

GetHitKeyStateAll()は、256個のキーそれぞれの状態を1つのchar配列へまとめて書き込むDXライブラリの関数です
押されているキーには0以外の値、押されていないキーには0が入ります
previousへの保存を先に行うのは、currentを新しい値で上書きする前に、直前の状態を確保しておく必要があるためです
この順序を逆にすると、previouscurrentが常に同じ値になり、押した瞬間を判定できなくなります

判定関数は、この2つの配列を比較するだけです

cpp
bool Input::IsDown(int key) const
{
    return current[key] != 0;
}

bool Input::IsPressed(int key) const
{
    return current[key] != 0 && previous[key] == 0;
}

入力で座標を変える

Game.hInputと速度を持ちます

cpp
Input input;
float playerSpeed = 4.0f;

MainLoop()では、Update()の前に入力を更新します

cpp
input.Update();
Update();
Draw();

input.Update()を先に呼ぶのは、Game::Update()の中で今フレームの入力を使うためです
これが逆順だと、Game::Update()は1フレーム古い入力を参照してしまいます

Game::Update()では、まず上下左右の入力から移動方向を作り、最後に速度を掛けます

cpp
float moveX = 0.0f;
float moveY = 0.0f;

if (input.IsDown(KEY_INPUT_LEFT) || input.IsDown(KEY_INPUT_A))
    moveX -= 1.0f;
if (input.IsDown(KEY_INPUT_RIGHT) || input.IsDown(KEY_INPUT_D))
    moveX += 1.0f;
if (input.IsDown(KEY_INPUT_UP) || input.IsDown(KEY_INPUT_W))
    moveY -= 1.0f;
if (input.IsDown(KEY_INPUT_DOWN) || input.IsDown(KEY_INPUT_S))
    moveY += 1.0f;

playerX += static_cast<int>(moveX * playerSpeed);
playerY += static_cast<int>(moveY * playerSpeed);

moveXmoveYは「-1、0、1のどちらへ動きたいか」だけを表す値です
実際の移動量は、この値にplayerSpeedを掛けて求めます
移動方向の計算と移動量の計算を分けておくと、後で速度だけを変えたいときにこの部分を変更せずに済みます

playerXplayerYintなので、moveX * playerSpeedの結果をstatic_cast<int>で整数へ変換してから加算します
この変換は小数点以下を切り捨てるため、速度が遅いとキャラクターがまったく動かなくなる場合があります
座標をfloatで持てばこの問題は起きませんが、ここでは第02回から続くintの座標をそのまま使い、float座標への移行は第04回で扱います

画面外へ出ないようにする

移動後に、座標を画面内へ戻します
プレイヤーの見た目の半分を余白として使います

cpp
if (playerX < 18) playerX = 18;
if (playerX > screenWidth - 18) playerX = screenWidth - 18;
if (playerY < 32) playerY = 32;
if (playerY > screenHeight - 24) playerY = screenHeight - 24;

この処理は、移動を計算したあとに必ず実行します
移動前に制限をかけると、画面端に到達する直前のフレームで移動量が中途半端に打ち切られ、キャラクターの動きが不自然に感じられます

動作確認

  1. 矢印キーとWASDの両方で移動できることを確認する
  2. 画面端でプレイヤーが止まることを確認する
  3. playerSpeed2.0f8.0fへ変えて操作感を比較する
  4. IsPressed()を使うと押した瞬間だけ反応することを短い表示で確認する
  5. 斜め方向へ移動したときの速さを、上下左右への移動と見比べる

よくある問題

  • 入力が反応しない場合は、input.Update()Update()より前に呼んでいるか確認する
  • Input.cppを作っただけでプロジェクトへ追加していないと、リンクエラーになる
  • playerSpeedを小さくしすぎると、static_cast<int>による切り捨てで座標が変化しなくなる
  • 斜め移動が上下左右への移動より速く感じる場合がある。これはmoveXmoveYをどちらも1.0fのまま速度倍しているためで、平面上の実際の移動距離が約1.41倍になるのが原因である。この補正は次回以降でベクトルの正規化として扱う

今回の完成条件

  • Inputクラスで押しっぱなしと押した瞬間を区別できる
  • 押しっぱなしの入力でプレイヤーを移動できる
  • プレイヤーを画面内に制限できる
  • currentpreviousを比較して押した瞬間を判定する仕組みを説明できる

まとめ

今回は、入力を読み取って座標を変化させました
次回は、位置や範囲を構造体でまとめ、当たり判定の準備をします