【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専用:クロスプラットフォーム対応不可

完全版のコードについて

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

完全なソースコードは、以下の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をリンクするだけで、追加のセットアップは不要です。

実装のポイント

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;
        }
    }
}

最初に反応した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;       // 十字キー

取得した値は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;

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

// デッドゾーン適用
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;
}

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

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

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);
}

実際の使い方

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

▼使用例

#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;
}

デバッグモニター

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

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

===============================================================================
                         CONTROLLER DEBUG MONITOR                              
===============================================================================
 Device: Controller Name          ID:0  Axes:6  Buttons:12
-------------------------------------------------------------------------------
 Raw | LX:32767 LY:32767 RX:32767 RY:32767 POV:65535 Btn:0x0000
     | L2:    0 R2:    0
-------------------------------------------------------------------------------
 L Stick | X:  0.00 [------|*------]   Y:  0.00 [------|*------]
 R Stick | X:  0.00 [------|*------]   Y:  0.00 [------|*------]
 Trigger | L2: 0.00 [        ]       R2: 0.00 [        ]
-------------------------------------------------------------------------------
  D-PAD         U                MAIN            X
             L   R                           Y   A
                D                                B
-------------------------------------------------------------------------------
 Shoulder:  L1  L2                                        R2  R1
 Stick   :  L3                                              R3
 System  :  SELECT                                      START
 Extra   :  EX1  EX2
-------------------------------------------------------------------------------
 Event:
===============================================================================
 ESC to exit

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

トリガー判定の実装

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

// 押された瞬間(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軸に統合

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

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()で入力状態を取得
  • スティック値は正規化とデッドゾーン適用が必要
  • ボタンはビットフラグで取得
  • 十字キーは角度で取得
  • 完全なコードはGitHubリポジトリで公開中

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

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

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

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