Seaside Laboratory

Posts

XInput API の使い方

XInput はシンプルな API で、複雑な手順を踏むことなく簡単に入力を取得することができる。COM ようなクラスベースではなく関数ベースのインターフェイスなので Win32 API を使う感覚に近い。

概要

DirectInput はデバイス列挙時に取得した GUID でコントローラーを識別するが、XInput はコントローラーが接続されているポートの番号を使って識別をする。

XInput API では同時にコントローラーを 4 基まで接続できます。XInput 関数は、すべて dwUserIndex パラメーターを必要とします。このパラメーターは設定されている、または問い合わされているコントローラーを識別するために渡されます。この ID の範囲は 0 ~ 3 で、XInput によって自動的に設定されます。この番号は、コントローラーが接続されているポートに対応していて、変更できません。

XInput を使うには xinput.lib のリンクと XInput.h のインクルードが必要になる。

入力の取得

入力の取得は XInputGetState を呼び出すだけ。

DWORD XInputGetState(
    DWORD dwUserIndex,    // コントローラーのインデックス
    XINPUT_STATE* pState  // 状態を受け取る XINPUT_STATE 構造体へのポインター
);

以下はマニュアルに記載されているサンプルプログラムを単純化したもの。

XINPUT_STATE state;
ZeroMemory( &state, sizeof(XINPUT_STATE) );

// Simply get the state of the controller from XInput.
DWORD dwResult = XInputGetState( 0, &state );

if( dwResult == ERROR_SUCCESS )
{
    // Controller is connected
}
else
{
    // Controller is not connected
}

呼び出しが成功すると引数の XINPUT_STATE 構造体に入力情報がセットされる。マニュアルには呼び出し前に行っている構造体のクリアについて何も書かれていないが、サンプルに従ってクリアしておいた方が無難なのかもしれない。

typedef struct _XINPUT_STATE {
    DWORD dwPacketNumber;     // 状態パケット番号
    XINPUT_GAMEPAD Gamepad;   // コントローラーの現在の状態
} XINPUT_STATE, *PXINPUT_STATE;

dwPacketNumber は「同じであれば、コントローラーの状態は変化していません。」と書かれていて、これは無駄な更新処理を行わないようにするための補助情報ということだろうか。高フレームレートのゲームでは入力が変化しないフレームの方が圧倒的に多くなるのでそのための仕組みなのかもしれない。

入力の判定に必要な情報は Gamepad メンバーに格納されている。

typedef struct _XINPUT_GAMEPAD {
    WORD wButtons;        // デジタルボタンのビットマスク
    BYTE bLeftTrigger;    // 左トリガーアナログコントロール
    BYTE bRightTrigger;   // 右トリガーアナログコントロール
    SHORT sThumbLX;       // 左スティックの X 軸
    SHORT sThumbLY;       // 左スティックの Y 軸
    SHORT sThumbRX;       // 右スティックの X 軸
    SHORT sThumbRY;       // 右スティックの Y 軸
} XINPUT_GAMEPAD, *PXINPUT_GAMEPAD;

ボタン

ボタンの情報はビットフラグとして格納されているので、ボタンと対応する定数でマスクすれば押されているのか判定することができる。

if ( Gamepad.wButtons & XINPUT_GAMEPAD_A )
{
    // A ボタンが押された
}

トリガー

トリガーはボタンとは違い多段階のアナログ値なので入力の有無は閾値を超えたかどうかで判定する。

定数 XINPUT_GAMEPAD_TRIGGER_THRESHOLD をしきい値として使用することができます。この値を使用すると、bLeftTrigger および bRightTrigger がその値より大きい場合のみ、ボタンが押されたと認識されるようになります。

if ( Gamepad.bLeftTrigger > XINPUT_GAMEPAD_TRIGGER_THRESHOLD )
{
    // 左トリガーが押された
}

スティック

スティックは車のハンドルと同じように遊び (デッドゾーン) があるので、入力量を測る前にデッドゾーンを超えているのか判定する必要がある。

デッドゾーンは、アナログスティックに触っていないときや、ステックが中央にある場合でも、コントローラーが報告する「動作」値です。

デッドゾーンをまったくフィルターしない XInput を使用するゲームは、満足のいくゲームプレイ体験を提供できません。コントローラーによっては、特別に感度が高いものもあるため、デッドゾーンはユニットごとに変わることがあります。さまざまなシステムでいくつかの Xbox 360 コントローラーを使ってゲームをテストすることをお勧めします。

デッドゾーンを自分で管理する方法もあるが閾値が定数として用意されているのでそれらを使うのが無難。

#define XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE  7849
#define XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE 8689

左と右で値が異なるのは先程の「デッドゾーンはユニットごとに変わる」ということか。

軸の向きがわからないと条件式を書くことができないので報告される値について理解しておく必要がある。

スティック軸の各メンバーは、スティックの位置を表す符号付きの -32768 ~ 32768 の値です。値が 0 の場合は、スティックがその軸の中心にあることを表します。値が負の場合は、やや下側または左側にあることを表します。値が正の場合は、やや上側または右側にあることを表します。

SHORT 型に 32768 を入れることはできないので英語版のマニュアルを確認したところ、

Each of the thumbstick axis members is a signed value between -32768 and 32767 describing the position of the thumbstick.

やはり間違っていた。

以下はデッドゾーン内の入力を 0 に丸めるサンプルプログラム。

// Zero value if thumbsticks are within the dead zone
if( (state.Gamepad.sThumbLX <  XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE &&
     state.Gamepad.sThumbLX > -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE) &&
    (state.Gamepad.sThumbLY <  XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE &&
     state.Gamepad.sThumbLY > -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE) )
{
   state.Gamepad.sThumbLX = 0;
   state.Gamepad.sThumbLY = 0;
}

「押された」という状態は「デッドゾーンに入っていない」状態なので上記条件を反転させる。

if ( Gamepad.sThumbLX <= -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE )
{
    // スティックの左が押された
}

デッドゾーンを超えた瞬間に成立するので最速で反応することになる。

スティック入力を方向パッド入力に変換

Retro-Bit 製のサターンパッドは方向パッドが左スティックとして実装されているので、wButtons を読むだけでは入力を認識することができない。そこで、wButtons を読む前にスティックの入力を方向パッドに変換して対応を行う。

// スティックの入力を方向パッドフラグに変換
WORD ThumbToDPad( SHORT sThumbX, SHORT sThumbY, SHORT sDeadZone )
{
    WORD wButtons = 0;

    if      ( sThumbY >=  sDeadZone )
    {
        wButtons |= XINPUT_GAMEPAD_DPAD_UP;
    }
    else if ( sThumbY <= -sDeadZone )
    {
        wButtons |= XINPUT_GAMEPAD_DPAD_DOWN;
    }

    if      ( sThumbX <= -sDeadZone )
    {
        wButtons |= XINPUT_GAMEPAD_DPAD_LEFT;
    }
    else if ( sThumbX >=  sDeadZone )
    {
        wButtons |= XINPUT_GAMEPAD_DPAD_RIGHT;
    }

    return wButtons;
}

関数を用意したら以下の要領でスティックからの入力を変換して wButtons にマージする。

// 左スティックからの入力を方向パッドに変換
Gamepad.wButtons |= ThumbToDPad( Gamepad.sThumbLX, Gamepad.sThumbLY, XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE );

XInputEnable

サンプルプログラム等では XInputGetState の他に XInputEnable という API が使われていることがある。

void XInputEnable(
  BOOL enable
);

enable が FALSE の場合、XInput は XInputGetState に対してニュートラルデータ (押下されたボタンなし、軸を中央に配置、トリガーは 0) を送信します。XInputSetState 呼び出しは登録されますが、デバイスには送信されません。FALSE 以外の任意の値を送信すると、読み込み/書き込み機能は標準に戻ります。

enable を FALSE にすると入力がニュートラルとして報告される点と、

この関数は、アプリケーションが (WM_ACTIVATEAPP などを通じて) フォーカスを取得または失ったときに呼び出されることを意図しています。この関数を使用すると、XInput が無効になっている場合は常にニュートラルデータが報告されるため、アプリケーションの XInput クエリループを変更する必要はありません。

という解説から、コントローラーの状態を意識することなく入力を取得するための API と思われる。

例えばプログラムが XInputGetState の呼び出しに成功したときだけ入力情報を更新するような構造だった場合、呼び出しが失敗したときは前回の入力情報が残ったままになり、最後に何らかの入力がされていると「押され続けた」状態として判断されてしまう。そのため、失敗したときは入力情報をクリアして「入力なし」の状態にしておかなければならないが、XInputEnable を使えばこの処理が不要になる、ということなのだろう。

XInputEnable を使う場合、ウィンドウプロシージャなどに以下のよう切り替え処理を入れることになるが、

case WM_ACTIVATEAPP:
{
    //
    // XInputEnable is implemented by XInput 1.3 and 1.4, but not 9.1.0
    //

    if( wParam == TRUE )
    {
        // App is now active, so re-enable XInput
        XInputEnable( TRUE );
    }
    else
    {
        // App is now inactive, so disable XInput to prevent
        // user input from effecting application and to
        // disable rumble.
        XInputEnable( FALSE );
    }

    break;
}

この切り替え処理を入れる手間と入力情報クリアの手間が釣り合っていないので使う必要性があるのか疑問を感じる。

ただ、バイブレーションを使う場合は、

FALSE を渡すと、現在再生中のバイブレーションエフェクトが停止します。この状態では、XInputSetState の呼び出しは登録されますが、デバイスには渡されません。
TRUE を渡すと、XInputSetState に送信された最後のバイブレーション要求が(0 であっても)デバイスに渡されます。

最終的に FALSE で無効にしておかないと振動し続けてしまうことがあるので呼び出しは必要とのこと。

XInput のバージョン

他の DirectX コンポーネントと同様に XInput にもバージョンが存在する。以下は「XInput Versions」に記載されいる情報を表としてまとめたもの。

バージョン 配布元
1.1 DirectX SDK April 2006
9.1.0 Windows Vista 以降に含まれる
1.3 DirectX SDK June 2010
1.4 Window 8 以降に含まれる

DirectX のバージョン由来なのか、9.1.0 だけおかしなナンバリングになっている。

Where is the DirectX SDK?」という記事に、

Any project that uses the XInput API and is intended to run on Windows 7 or older versions of Windows need to use either the legacy version (9.1.0) or will need to explicitly include the headers and libraries for this component from the DirectX SDK. The XInput header and XINPUT.LIB that are included in the Windows SDK target only the version (1.4) that ship as part of Windows 8 and later. The same header can be used with XINPUT9_1_0.LIB to use the legacy version, which is included with older versions of Windows. The legacy version of XInput doesn't detect full capabilities or support controller-integrated audio, so if support for these features is required, you must use the DirectX SDK version (1.3).

XInput API を使用し、Windows 7 またはそれ以前のバージョンの Windows で実行するプロジェクトは、レガシーバージョン (9.1.0) を使用するか、DirectX SDK からこのコンポーネントのヘッダーとライブラリを明示的に含める必要があります。Windows SDK に含まれている XInput ヘッダーと XINPUT.LIB は、Windows 8 以降の一部として出荷されているバージョン (1.4) のみを対象としています。XINPUT9_1_0.LIB と同じヘッダーを使用することで、古いバージョンの Windows に含まれているレガシーバージョンを使用できます。XInput のレガシーバージョンは、すべての機能を検出したり、コントローラー内蔵オーディオをサポートしていないため、これらの機能のサポートが必要な場合は、DirectX SDK バージョン (1.3) を使用する必要があります。

と書かれているように対象 OS によってバージョンを使い分ける必要がある。

リンクするライブラリによって使われる DLL が切り替わるので、Windows 8 以降が対象なら XInput.lib (1.4)、古い Windows も対象に含めるなら Xinput9_1_0.lib (9.1.0) を使用する。

DirectInput との併用

XInput デバイスだけでなく DirectInput デバイスもサポートする場合、XInput 単体では発生しない諸問題に対処しなければならないので、それらを踏まえた上でサポートをするのか判断する必要がある。

デバイスの重複

XInput デバイスは DirectInput デバイスとしても使えるユーザーフレンドリーな仕様なので、XInput と DirectInput を併用した場合、単一の Xbox コントローラーが両 API から認識され重複してしまう。Xbox コントローラーは XInput で管理するのが適切なので、DirectInput 側で列挙されたものは取り除かなければならない。

リファレンスに DirectInput 上で XInput デバイスを識別するためのサンプルプログラムはあるものの、識別は簡単にいかないようでそこそこ複雑な 100 行程のコードをプログラムに組み込む必要がある。

DirectInput で "IsXInputDevice" というメソッドを提供しないあたり、なるべく DirectInput を使わせたくないというマイクロソフトの方針が感じ取れる。

ボタンの配置

XInput はボタンの数や配置を仕様として明確に決めているが、DirectInput には明確な仕様がないのでゲーム内でボタン設定を行わないとまともに操作することができない。

特に格闘ゲームにおいては各自使い慣れたコントローラーを繋いで遊ぶというスタイルが増えてきているので、プレイヤーが入れ替わる度にボタン設定が必要になる状況は可能な限り避けておきたいところ。

コントローラーの認識

XInput はデバイスを接続ポート番号で管理しているので、コントローラーをゲーム起動後に接続しても認識することができるが、DirectInput はゲーム起動時に列挙したデバイスから COM オブジェクトを生成する仕組みなので、後から接続されたコントローラーを認識することができない。

UI

Steam Blog でユーザーが使っているコントローラーの統計情報が公開されたが、

Xbox コントローラーが標準的なコントローラであるという基本的な事実はデータから一目瞭然です。これまでに Steam に接続された Xbox 360 または Xbox One のコントローラーの数は 4,000 万にも上り、全コントローラの 64% に相当します。

Xbox 系コントローラーのシェアは過半数を超えているので DirectInput をサポートする必要性が低く、

ビルトインのサポートが圧倒的に XInput となったために、様々なタイトルを同じ操作でプレイするのに Xbox コントローラーが適するという結果となりました。

Xbox コントローラー以外のデバイスが混在すると、画面上に表示されるボタン名といった UI を統一できないのでゲームの操作性が低下してしまう。