【C++】キーボード入力を取得する方法

サムネイル画像 C++

今回は、C++で「ゲーム用のキーボード入力を取得する方法」について解説していきます。

「キーを1回押しただけなのに、ずっと反応し続けてしまう…」
「押した瞬間だけ処理したいのに、どう書けばいいかわからない」
「WM_KEYDOWNだけでいいの?それともGetKeyState?」

こんな疑問はありませんか?

ゲーム開発のキーボード入力には、「押している間」「押した瞬間」「離した瞬間」の3種類の判定が必要です。
この記事では、自主制作で使っているキーボードモジュールのコードをもとに、仕組みから実装手順まで丁寧に解説していきます。

この記事を読み終えると、ゲームで使えるキーボード入力をしっかり実装できるようになると思いますので、ぜひ最後まで読んでいただけると嬉しいです。

このキーボードモジュールとは?

今回紹介するキーボードモジュールは、Windowsメッセージ(WM_KEYDOWN / WM_KEYUP)ベースで動く入力管理クラスです。

主な特徴は以下の通りです。

  • Press / Trigger / Release の3種判定を標準装備
  • ✅ 左右の Shift / Ctrl / Alt を区別して取得できる
  • ✅ ビットフィールドで 256キー分の状態をわずか32バイトで管理
  • ✅ シングルトンパターンでどこからでも Keyboard::Instance() でアクセスできる

📁 完全版のコードについて

完全版のコードはGitHubで公開しています。

👉 https://github.com/it-kashiros/KeyBoard

ダウンロードは、GitHubページの 「Code」→「Download ZIP」 からZIPで取得してください。

ファイル構成は以下の3ファイルです。

keyboard.h    … キークラスの定義・Keyboardクラスの宣言
keyboard.cpp  … Keyboardクラスの実装
main.cpp      … 使用例(テストプログラム)

自分のプロジェクトに組み込む場合は、keyboard.hkeyboard.cpp をコピーして使ってください。

Press / Trigger / Release の違い

ゲーム開発でキーボード入力を扱うとき、この3種類の判定を使い分けることがとても重要です。

  • IsPressed:キーを押している間ずっと true(移動・射撃の連射など)
  • IsTrigger:キーを押した瞬間だけ true(ジャンプ・決定ボタンなど)
  • IsRelease:キーを離した瞬間だけ true(チャージ攻撃の解放など)

たとえば、ジャンプを IsPressed で実装してしまうと、キーを1回押しただけで押している間ずっとジャンプが発動し続けます
このような「1回の操作に対して1回だけ処理したい」場合は、IsTrigger を使うのが正解です。

if文の使い分けについては、if文とswitch文の使い分けの記事も参考にしてみてください。

使い方の基本(4ステップ)

ステップ① keyboard.h をインクルードする

keyboard.h をインクルードするだけで、Key 列挙体と Keyboard クラスが使えるようになります。

▼main.cpp


#include <windows.h>
#include "keyboard.h"

ステップ② WndProc に ProcessMessage を追加する

Windowsメッセージをキーボードモジュールに転送します。
WM_KEYDOWN / WM_KEYUP / WM_SYSKEYDOWN / WM_SYSKEYUP / WM_ACTIVATEAPP の5つを渡してください。

▼main.cpp


LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
    switch (message) {
    case WM_KEYDOWN:
    case WM_KEYUP:
    case WM_SYSKEYDOWN:
    case WM_SYSKEYUP:
    case WM_ACTIVATEAPP:
        // キーボードモジュールにメッセージを転送
        Keyboard::Instance().ProcessMessage(message, wParam, lParam);
        break;

    case WM_DESTROY:
        PostQuitMessage(0);
        break;

    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

WM_ACTIVATEAPP を渡しているのがポイントで、Alt+Tabでウィンドウを切り替えたときにキーが押しっぱなしになるバグを防いでいます。

ステップ③ Initialize() と Update() を呼ぶ

起動時に Initialize()、ゲームループの先頭で毎フレーム Update() を呼びます。
Update() を忘れると Trigger / Release がまったく機能しません。

▼main.cpp


int main() {
    // ...(ウィンドウ作成など)

    // ① 初期化(1回だけ)
    Keyboard::Instance().Initialize();

    MSG msg = {};
    bool isRunning = true;

    while (isRunning) {
        // メッセージ処理
        while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
            if (msg.message == WM_QUIT) { isRunning = false; break; }
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }

        // ② 毎フレーム呼ぶ(Trigger/Release判定に必須)
        Keyboard::Instance().Update();

        // ③ ここでキー判定を行う
        // ...

        Sleep(16);
    }
    return 0;
}

ステップ④ IsPressed / IsTrigger / IsRelease で判定する

Update() の後に、目的に応じた判定関数を使います。

▼main.cpp


Keyboard& kb = Keyboard::Instance();

// 押している間ずっと移動(IsPressed)
if (kb.IsPressed(Key::Right)) {
    playerX += 5.0f;
}
if (kb.IsPressed(Key::Left)) {
    playerX -= 5.0f;
}

// 押した瞬間だけジャンプ(IsTrigger)
if (kb.IsTrigger(Key::Space)) {
    Jump();
}

// 離した瞬間にチャージ解放(IsRelease)
if (kb.IsRelease(Key::Z)) {
    FireCharged();
}

// ESCで終了
if (kb.IsTrigger(Key::Escape)) {
    isRunning = false;
}

実行結果(コンソール出力例)

============ KEYBOARD TEST =============
 ESC:Exit | Focus popup window to input
========================================
PRESS: A W                              
TRIG:  W                                
REL:   -                                
========================================
Shift[ ][ ] Ctrl[ ][ ] Alt[ ][ ]
Arrows: [ ][ ][<][ ]  Any:[Y]

仕組みを少しだけ覗いてみよう

このモジュールが Trigger / Release を判定できる理由は、「現在フレームの状態」と「前フレームの状態」を両方保持しているからです。

▼keyboard.cpp(Update の実装)


void Keyboard::Update() {
    // 現在の状態を「前フレームの状態」としてコピーする
    std::memcpy(m_previousState, m_currentState, sizeof(m_currentState));
}

▼keyboard.cpp(Trigger / Release の判定ロジック)


// 押した瞬間:今押されていて、かつ前フレームでは押されていなかった
bool Keyboard::IsTrigger(Key key) const {
    int k = static_cast<int>(key);
    return GetBit(m_currentState, k) && !GetBit(m_previousState, k);
}

// 離した瞬間:今押されていなくて、かつ前フレームでは押されていた
bool Keyboard::IsRelease(Key key) const {
    int k = static_cast<int>(key);
    return !GetBit(m_currentState, k) && GetBit(m_previousState, k);
}

非常にシンプルな仕組みで、毎フレーム前後の状態を比較するだけでTrigger/Releaseが実現できています。

関数を複数ファイルに分けて使う方法が気になる方は、プロトタイプ宣言の記事もあわせて読んでみてください。

【重要】私が実際にゲーム制作でハマった体験談

自主制作の2Dアクションゲームにジャンプを実装したとき、まんまとこの問題にハマりました。

スペースキーでジャンプさせようと思って、こんなコードを書きました。


// ❌ これがバグの原因
if (kb.IsPressed(Key::Space)) {
    Jump();
}

実際に動かすと、スペースキーをちょっと押しただけでジャンプが何十回も連続で発動してしまいます。60fpsで動いているので、キーを押している間ずっと毎フレームJump()が呼ばれていたからです。

原因に気づくまで15分くらいかかりました。IsPressedIsTrigger に変えるだけで一発解決でした。


// ✅ 押した瞬間だけ反応
if (kb.IsTrigger(Key::Space)) {
    Jump();
}

「1回だけ反応させたい」か「押している間ずっと反応させたい」かで、IsTrigger と IsPressed を使い分けるのがゲーム入力の基本です。

よくある失敗例と対処法

  • IsPressedで1回押しを判定しようとしている
    → 押している間ずっとtrueになるため、毎フレーム処理が走ります。1回の操作に1回だけ反応させたいならIsTriggerを使いましょう。

  • Update() を毎フレーム呼んでいない(または呼ぶ場所がズレている)
    Update() がないと前フレームの状態が更新されないため、IsTrigger と IsRelease がまったく機能しません。ゲームループの先頭で必ず呼んでください。

  • WndProc に ProcessMessage を渡し忘れている
    → キーメッセージがモジュールに届かないため、どのキーを押しても反応しません。WndProcの WM_KEYDOWN / WM_KEYUP / WM_SYSKEYDOWN / WM_SYSKEYUP / WM_ACTIVATEAPP の5つを必ず渡しましょう。

注意点

  • このモジュールは Windowsメッセージベース のため、ウィンドウが作成されていないと動作しません
  • WM_ACTIVATEAPP を渡すことで、Alt+Tab後にキーが押しっぱなしになるバグを防いでいます。忘れずに渡しましょう
  • コントローラー入力とあわせて使いたい場合は、XInputやSDL3との併用もできます

コントローラー入力の実装については、XInputでゲームコントローラー入力を取得する方法や、SDL3で全機種のコントローラーに対応する方法の記事もあわせて読んでみてください。

まとめ

  • ゲーム用キーボード入力には Press / Trigger / Release の3種判定 が必要
  • IsPressed:押している間ずっとtrue(移動・連射)
  • IsTrigger:押した瞬間だけtrue(ジャンプ・決定)
  • IsRelease:離した瞬間だけtrue(チャージ解放など)
  • Update() を毎フレーム呼ぶのが必須(忘れるとTrigger/Releaseが動かない)
  • WndProcには 5種類のメッセージ(KEYDOWN/UP/SYSKEYDOWN/UP/ACTIVATEAPP)を渡す
  • コードは GitHub からZIPでダウンロードできます

これでゲーム用キーボード入力の実装はバッチリですね!

ここまで読んでくださり、ありがとうございました。
この記事が皆様の開発の役に立てば幸いです。

関連記事