28:ゲームパッドによる入力

 最近のゲームパッドには、パッドで押されたボタンをキーボードのキー入力としてOSに通知する機能が備わっていますが、だからといって、「このゲームはキーボード専用なのでゲームパッドを使いたい方はそういうソフトを使ってください」というのもなんだかみっともないです。そこで今回はDirectInputによるゲームパッド入力の受け付けを行うプログラムを組むことにします。
 キーボードであれば、どのようなタイプであれ、入力されるデータにそれほど違いはありませんが、ゲームパッドは搭載されているボタンの数ひとつとっても様々な種類があります。そのため、ゲームパッドを入力機器と使用するためにはDirectInputから通知された複数のデバイス情報をプログラム側で選別しなくてはいけません。

 この複数のデバイスを取得するにはIDirectInput8::EnumDevices()を使います。この関数が実行されると、取得したデバイス情報が第2引数で指定した関数で、作成可能なデバイスが次々と呼び出されるので、その中からゲームに使用したい機器を選択することになります。
/* 省略 */

// デバイス列挙関数に渡すデータを格納する構造体
struct enumdata
{
    LPDIRECTINPUT8 pInput;                // デバイスを作成するためのインターフェイス
    LPDIRECTINPUTDEVICE8 *ppPadDevice;    // 使用するデバイスを格納するポインタのポインタ
};

class CInput : public CGameObject
{
private:
    /* 省略 */

    LPDIRECTINPUTDEVICE8 pPadDevice;
    DIJOYSTATE2 paddata, lastpaddata;

    // クラス内で記述するシステムから呼び出される(コールバック)関数はstatic属性にしなくてはならない
    static BOOL CALLBACK EnumJoypad(const DIDEVICEINSTANCE* pInstance, LPVOID pContext);
    /* 省略 */
};
/* 省略 */

void CInput::Init()
{
    if(pInput == NULL){
        // インターフェイスの取得
        HRESULT hr;
        hr = DirectInput8Create(
            GetHInstance(),            // ソフトのインスタンスハンドル
            DIRECTINPUT_VERSION,    // DirectInputのバージョン
            IID_IDirectInput8,        // 取得するインターフェイスのタイプ
            (LPVOID*)&pInput,        // インターフェイスの格納先
            NULL                    // COM集成の制御オブジェクト(使わないのでNULL)
            );

        /* 省略 */

        // ジョイパッドの作成
        
        // コールバック関数に転送したいデータを格納
        enumdata ed;
        ed.pInput = pInput;
        ed.ppPadDevice = &pPadDevice;
        
        pInput->EnumDevices(
            DI8DEVCLASS_GAMECTRL,    // ゲームコントローラーが対象
            EnumJoypad,                // 列挙する関数
            &ed,                    // 列挙関数に渡したいデータはここに入れる
            DIEDFL_ATTACHEDONLY        // インストール・接続済みのデバイスのみ取得
            );

        pPadDevice->SetCooperativeLevel(GetHWnd(),
            DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);
        
        // ゲームパッドの入力情報はDIJOYSTATE2に格納されるので
        // データフォーマットにはc_dfDIJoystick2を指定
        hr = pPadDevice->SetDataFormat(&c_dfDIJoystick2);
        if(FAILED(hr)) RELEASE(pPadDevice);
    }
}

BOOL CALLBACK CInput::EnumJoypad(const DIDEVICEINSTANCE* pInstance, LPVOID pContext)
{
    enumdata *ed = (enumdata*)pContext;

    // このプログラムは単一機器のみ接続されていることが前提。
    // ゲームパッドが複数接続されているときに、特定の機器のみを使いたいときは
    // それ以外のデバイスが呼び出されたときに弾くようにするとよい
    /*
    if(_tcscmp(pInstance->tszProductName, _T("TNK Controler")) != 0){
        return DIENUM_CONTINUE;
    }
    */

    HRESULT hr;
    hr = ed->pInput->CreateDevice
        (pInstance->guidInstance, ed->ppPadDevice, NULL);
    if(FAILED(hr)) return DIENUM_CONTINUE;    // デバイスが作成できないので列挙を続ける

    // 希望するデバイスが作成できたので列挙を終了する
    return DIENUM_STOP;
}
 ゲームパッドの入力情報を取得する方法は、取得するデータ型が配列ではなく専用の構造体であることと、一度IDirectInputDevice8::Poll()で対象の機器へ応答を伺わなくてはいけないことをのぞいては、キーボードでのやり方と同一です。
void CInput::Exec()
{
    if(pKeyDevice){
        /* 省略 */
    }

    if(pPadDevice){
        // ジョイパッドデータの取得
        pPadDevice->Poll();

        // 入力の受付開始
        pPadDevice->Acquire();

        memcpy(&lastpaddata, &paddata, sizeof(DIJOYSTATE2));
        pPadDevice->GetDeviceState(sizeof(DIJOYSTATE2), &paddata);
    }
}
 押されたボタンの情報はDIJOYSTATE2構造体に格納されているので、必要に応じて対象のボタンが押されているかを取得する関数を作成すればプログラムはほぼ完成です。ボタンの押下状態の取得に関してはほとんどキーボードでの関数と同じですが、方向キーは少し特殊な取得方法となっています。詳しくはサンプルコードをご覧ください。
#pragma once

/* 省略 */

// パッドの方向キー用
#define PP_UP 0x1
#define PP_RIGHT 0x2
#define PP_DOWN 0x4
#define PP_LEFT 0x8

/* 省略 */

class CInput : public CGameObject
{
private:
    /* 省略 */

    LPDIRECTINPUTDEVICE8 pPadDevice;
    DIJOYSTATE2 paddata, lastpaddata;

    static BOOL CALLBACK EnumJoypad(const DIDEVICEINSTANCE* pInstance, LPVOID pContext);
public:
    /* 省略 */

    BYTE GetPovPosition();            // 十字キーの位置を取得
    BOOL IsButtonDown(int pos);        // ボタンが押され続けているか
    BOOL IsButtonPressed(int pos);    // ボタンが押された瞬間か
    BOOL IsButtonReleased(int pos);    // ボタンが放された瞬間か

    /* 省略 */
};
/* 省略 */

BYTE CInput::GetPovPosition()
{
    // paddata.rgdwPOV[0]に押された方向が角度×100という整数で格納されている
    // 真上が0で時計回りに36000まで範囲がある
    // デジタル入力方式のゲームパッドの場合、45度単位で取得してもまず問題ない
    switch(paddata.rgdwPOV[0]){
        case 0:
            return PP_UP;
        case 4500:
            return PP_UP | PP_RIGHT;
        case 9000:
            return PP_RIGHT;
        case 13500:
            return PP_RIGHT | PP_DOWN;
        case 18000:
            return PP_DOWN;
        case 22500:
            return PP_DOWN | PP_LEFT;
        case 27000:
            return PP_LEFT;
        case 31500:
            return PP_LEFT | PP_UP;
    }

    return result;
}

BOOL CInput::IsButtonDown(int pos)
{
    if(pPadDevice == NULL) return FALSE;
    return paddata.rgbButtons[pos];
}

BOOL CInput::IsButtonPressed(int pos)
{
    if(pPadDevice == NULL) return FALSE;

    if(paddata.rgbButtons[pos] && !lastpaddata.rgbButtons[pos]) return TRUE;
    return FALSE;
}

BOOL CInput::IsButtonReleased(int pos)
{
    if(pPadDevice == NULL) return FALSE;

    if(!paddata.rgbButtons[pos] && lastpaddata.rgbButtons[pos]) return TRUE;
    return FALSE;
}