【C++】WinMMでゲームコントローラー入力を取得する方法を解説

サムネイル画像 C++

今回は、C++で「ゲームコントローラーをWinMM(Windows Multimedia API)で取得する方法」について解説していきます。

「XInputは使えないけど、コントローラー入力を取得したい」「WindowsでDirectInput的なことをしたい」と思ったことはありませんか?

この記事を読み終えると、あなたはWinMMを使ったコントローラー入力の取得方法をマスターできると思いますので、ぜひ最後まで読んでいただけると嬉しいです。

WinMM(Windows Multimedia API)とは?

WinMMは、Windowsに標準で搭載されている古いマルチメディアAPIです。

ジョイスティックやゲームコントローラーの入力を取得できるjoyGetPosEx()などの関数が用意されています。

WinMMの特徴

  • 追加ライブラリ不要:Windowsに標準搭載
  • 幅広いコントローラーに対応:DirectInput互換のコントローラーが使える
  • 軽量:シンプルなAPI
  • 古いAPI:モダンなゲーム開発では非推奨
  • Windows専用:クロスプラットフォーム対応不可

C++の開発環境については、DebugビルドとReleaseビルドの違いも理解しておくと、開発がスムーズに進みます。

完全版のコードについて

この記事では、実装の要点を解説します。

完全なソースコードは、以下のGitHubリポジトリで公開しています。

🔗 https://github.com/it-kashiros/controller_winmm

リポジトリには以下のファイルが含まれています。

  • game_controller.h:コントローラークラスのヘッダー
  • game_controller.cpp:コントローラークラスの実装
  • main.cpp:デバッグ用のサンプルプログラム

Visual Studioで開いて、すぐにビルド・実行できます。

基本的な使い方

WinMMでコントローラー入力を取得する基本的な流れは以下の通りです。

  1. joyGetDevCaps()でデバイス情報を取得
  2. joyGetPosEx()で入力状態を取得
  3. 取得した値を正規化してゲーム用に変換

必要なヘッダーとライブラリ

▼game_controller.h(抜粋)

#pragma once
#include <windows.h>
#include <mmsystem.h>

#pragma comment(lib, "winmm.lib")

winmm.libをリンクするだけで、追加のセットアップは不要です。

C++のヘッダーファイルについては、プロトタイプ宣言とは?の記事でも解説しています。

実装のポイント

1. コントローラーの検出

WinMMでは、コントローラーにID(0~15)が割り当てられます

接続されているコントローラーを見つけるには、順番に試していく必要があります。

▼game_controller.cpp(抜粋)

// 接続中のコントローラーがなければ探索
if (s_workingControllerId == -1) {
    for (int id = 0; id < 16; id++) {
        int testValue = GetGamepadValue(id, 3);
        if (testValue != -1) {
            s_workingControllerId = id;
            UpdateCaps(id);
            break;
        }
    }
}

実行結果の例

Controller found at ID: 0
Device: USB Gamepad

最初に反応したIDを記憶し、以降はそのIDを使い続けます。

2. 入力状態の取得

joyGetPosEx()で、スティック・ボタン・POV(十字キー)の生の値を取得できます。

▼game_controller.cpp(抜粋)

JOYINFOEX ji;
ji.dwSize = sizeof(JOYINFOEX);
ji.dwFlags = JOY_RETURNALL;

if (joyGetPosEx(id, &ji) != JOYERR_NOERROR)
    return -1;

// 各軸の値を取得
int leftX = ji.dwXpos;      // 左スティックX
int leftY = ji.dwYpos;      // 左スティックY
int rightX = ji.dwRpos;     // 右スティックX
int rightY = ji.dwUpos;     // 右スティックY
int buttons = ji.dwButtons; // ボタンのビットフラグ
int pov = ji.dwPOV;         // 十字キー

実行結果の例

leftX: 32767
leftY: 32767
buttons: 0x0001

取得した値は0~65535の範囲なので、ゲーム用に正規化する必要があります。

3. スティック値の正規化

スティックの値を-1.0~1.0の範囲に変換します。

▼game_controller.cpp(抜粋)

// スティック値を正規化(-1.0~1.0)
s_currentState.leftStickX = (float)(leftX - 32767) / 32767.0f;
s_currentState.leftStickY = (float)(leftY - 32767) / 32767.0f;

実行結果の例

leftStickX: 0.00 (中央)
leftStickY: 0.00 (中央)

さらに、デッドゾーンを適用して、わずかな傾きを無視します。

// デッドゾーン適用
static float ApplyDeadzone(float value, float deadzone = 0.15f) {
    if (fabs(value) < deadzone) return 0.0f;
    
    float sign = (value > 0) ? 1.0f : -1.0f;
    float adjustedValue = (fabs(value) - deadzone) / (1.0f - deadzone);
    return sign * adjustedValue;
}

変数の初期化については、変数の初期化 =0と{0}の違いも参考にしてください。

4. ボタンの判定

ボタンはビットフラグで取得されます。

// ボタンの状態
s_currentState.buttonDown = (buttons & (1 << 0)) != 0;  // ボタン0
s_currentState.buttonRight = (buttons & (1 << 1)) != 0;  // ボタン1
s_currentState.buttonLeft = (buttons & (1 << 2)) != 0;  // ボタン2
s_currentState.buttonUp = (buttons & (1 << 3)) != 0;  // ボタン3

実行結果の例

Button 0: Pressed
Button 1: Released

ビットシフトで各ボタンの状態をチェックできます。

5. 十字キー(POV)の処理

十字キーは角度(0~35900)で取得されます。

if (pov == 65535 || pov == -1) {
    // 押されていない
    s_currentState.dpadUp = false;
    s_currentState.dpadDown = false;
    s_currentState.dpadLeft = false;
    s_currentState.dpadRight = false;
} else {
    // 角度を度に変換(0.01度単位なので100で割る)
    int angle = pov / 100;
    s_currentState.dpadUp = (angle >= 315 || angle <= 45);
    s_currentState.dpadRight = (angle >= 45 && angle <= 135);
    s_currentState.dpadDown = (angle >= 135 && angle <= 225);
    s_currentState.dpadLeft = (angle >= 225 && angle <= 315);
}

実行結果の例

POV angle: 0 (Up pressed)
POV angle: 9000 (Right pressed)

私が実際にこの機能を使った時の体験談

実は、私がチームでゲーム制作をしていた時、Nintendo Switch Proコントローラーが全く認識されないという問題に遭遇しました。

具体的には、joyGetPosEx()が常にJOYERR_UNPLUGGEDを返し、デバッグモニターに「Controller not connected…」と表示され続けていました。

原因と解決策

調査した結果、Proコントローラーは独自のドライバを使っており、WinMMでは直接認識できないことが判明しました。

一方、3COINSの安価なコントローラーやXboxコントローラーは問題なく動作しました。これは、DirectInput互換のドライバを使っているためです。

最終的に、以下の対応をしました。

  • 開発中は3COINSコントローラーやXboxコントローラーを使用
  • プレイヤー向けには「DirectInput対応コントローラー推奨」と明記
  • Proコントローラー対応が必要な場合は、別途SDL2などのライブラリを検討

この経験から、WinMMは万能ではなく、コントローラーの互換性を事前に確認する重要性を学びました。

よくある失敗例

初心者がハマりやすいポイントを3つ紹介します。

失敗例1:デッドゾーンを適用していない

症状:スティックを触っていないのに、キャラクターが勝手に動く

原因:アナログスティックは完全に0にならないため、わずかな値が常に入力される

解決策:デッドゾーン(0.15程度)を適用して、小さな値を無視する

// NG例
float stickX = (leftX - 32767) / 32767.0f;

// OK例
float stickX = ApplyDeadzone((leftX - 32767) / 32767.0f);

失敗例2:ノートオフを検出していない

症状:ボタンを離してもずっと押されっぱなしになる

原因:前フレームの状態を保持していないため、ボタンが離されたことを検出できない

解決策:前フレームの状態を保存し、現在との比較で判定する

// 前フレームの状態を保存
s_prevState = s_currentState;

// トリガー判定
bool pressed = s_currentState.buttonA && !s_prevState.buttonA;

失敗例3:軸の配置を決め打ちしている

症状:特定のコントローラーでL2/R2が正しく動作しない

原因:コントローラーによってZ軸とV軸の扱いが異なる

解決策:デバイスの機能(JOYCAPS)を確認し、動的に対応する

if (s_caps.hasV) {
    // XInputタイプ:Z軸とV軸が別々
    s_currentState.triggerL = (float)triggerZ / 65535.0f;
    s_currentState.triggerR = (float)triggerV / 65535.0f;
} else {
    // DirectInputタイプ:Z軸のみで統合
    // 中央より大きければL2、小さければR2
}

実際の使い方

実装したクラスは、以下のように使います。

▼使用例

#include "game_controller.h"

int main() {
    // 初期化
    GameController::Initialize();
    
    while (true) {
        // 毎フレーム更新
        GameController::Update();
        
        // 接続確認
        if (!GameController::IsConnected()) {
            continue;
        }
        
        // スティックの値を取得
        float leftX = GameController::GetLeftStickX();
        float leftY = GameController::GetLeftStickY();
        
        // ボタンの押下判定
        if (GameController::IsTrigger_ButtonDown()) {
            // Bボタンが押された瞬間
        }
        
        if (GameController::IsPressed_ButtonRight()) {
            // Aボタンが押されている間
        }
    }
    
    // 終了処理
    GameController::Finalize();
    return 0;
}

実行結果の例

Controller connected: USB Gamepad
Left stick: X=0.85, Y=-0.42
Button A pressed!

デバッグモニター

リポジトリのmain.cppには、コントローラーの入力をリアルタイムで表示するデバッグモニターが実装されています。

▼実行すると、以下のような画面が表示されます

スティックの動きやボタンの状態がリアルタイムで視覚化されるため、デバッグに便利です。

トリガー判定の実装

ゲームでよく使う「ボタンが押された瞬間」の判定は、前フレームとの比較で実現できます。

// 押された瞬間(Trigger)
static bool IsTrigger_ButtonDown() {
    return s_currentState.buttonDown && !s_prevState.buttonDown;
}

// 離された瞬間(Release)
static bool IsRelease_ButtonDown() {
    return !s_currentState.buttonDown && s_prevState.buttonDown;
}

// 押されている間(Pressed)
static bool IsPressed_ButtonDown() {
    return s_currentState.buttonDown;
}

この3つのパターンで、あらゆる入力判定に対応できます。

注意点

1. コントローラーによって軸の配置が違う

WinMMはDirectInput互換のコントローラーに対応していますが、メーカーによって軸の割り当てが異なる場合があります。

  • PlayStation系:Z軸とV軸が別々にL2/R2
  • Xbox系:Z軸のみでL2/R2が1軸に統合
  • Nintendo系:WinMMで直接認識できない場合がある

リポジトリのコードでは、両方のパターンに対応しています。

2. XInputコントローラーは非推奨

XboxコントローラーなどのXInput対応デバイスは、WinMMでも動作しますが、XInput APIを使う方が推奨されます。

WinMMは主に、DirectInput互換の古いコントローラーやフライトスティックなどで使います。

3. 振動機能は使えない

WinMMには、コントローラーの振動機能を制御するAPIがありません

振動が必要な場合は、XInputやDirectInputを使う必要があります。

動作確認

リポジトリをダウンロードして、Visual Studioで開いてください。

  1. Visual Studioでプロジェクトを開く
  2. コントローラーをPCに接続
  3. ビルド&実行(F5)
  4. コントローラーを操作して、入力が表示されることを確認

ESCキーで終了できます。

まとめ

WinMMを使ったコントローラー入力取得についてまとめます。

  • WinMM:Windows標準のマルチメディアAPI
  • joyGetPosEx()で入力状態を取得
  • スティック値は正規化とデッドゾーン適用が必要
  • ボタンはビットフラグで取得
  • 十字キーは角度で取得
  • Nintendo Proコントローラーなど、一部のコントローラーは認識できない
  • 完全なコードはGitHubリポジトリで公開中

これでWinMMを使ったコントローラー入力の取得はバッチリですね!

GitHubのコードを参考に、自分のゲームプロジェクトに組み込んでみてください。

ここまで読んでくださり、ありがとうございました。

この記事が皆様の学習に役立てば幸いです。