DirectInput Mapper: シングル システムの複数ユーザーとデバイスのためのプログラミング

C. Shane Evans
Microsoft Corporation

January 2001

要約: 今回の記事では、新しい DirectInput Mapper 機能、それも 1 台のマシンで複数のユーザーがゲームをするときに関係がある機能についてお話ししましょう。

目次

概要
アプリケーション コード
  データ構造体
     アクション マップ
     プレーヤのアクションとデバイス列挙構造体
  アプリケーションの初期化
  アプリケーション ウィンドウ プロセス
  ゲーム ループとユーザー入力の操作
  アプリケーション クリーンアップ
CInputDeviceManager クラス
  データ メンバ
  エラー コード
  初期化: CInputDeviceManager コンストラクタと Create() メソッド
     列挙と割り当て
     デバイスの再割り当て
  クリーンアップ: Cleanup() と CleanupDevices() メソッド
結論

概要

今回の記事は、DirectInput と Microsoft® DirectX® 8.0 における DirectInput の機能に関して理解していただいていることを前提に書かれています。基本的な知識については、SDK ドキュメント (DirectX 8.0 for C++ または DirectX 8.0 for Visual Basic) から始めて、SDK チュートリアル アプリケーションで学ぶとよいでしょう。

DirectX 8.0 では、入力コード開発者のための新しい機能を採用しました。それが DirectInput Mapper です。DirectInput Mapper、略して「Mapper」は、デバイスの種類にまったくとらわれない入力パラダイムへの大きな 1 歩です。Mapper では、ユーザーが実行できるゲーム固有のアクション用語で入力コードを開発できます。Mapper を使えば、ジョイスティック、ゲームパッド、ハンドルなどデバイスの種類ごとに固有のコードを書く必要はありません。さらに、Mapper 利用には、次のような利点があります。

  • キー デバイスが少数であれば、特に指定しなくても正常に機能する。

  • DirectInput にユーザーのアクションを伝えると、デバイスがそれを解釈し、選択されたデバイスにアクションが自動的に割り当てられる。

  • デバイス再構成サービスと、確立されたユーザー設定を「無料」で入手できる。

つまり、入力コードの開発者にとって、Mapper はすぐれたツールです。重要なのは、Microsoft が DirectX 8.0 ではじめてこの種のテクノロジに挑戦したこと、そしてこの初めての挑戦もまだまだ控えめだということです。Action Mapping 自体は強固ですが、強化が必要な領域は、ちょっと挙げただけでもマクロ、シフト ボタン、強制フィードバック連結などがあります。今回の記事は、代表的事例になると思われるマルチ ユーザー ゲームに焦点を当てます。マルチ ユーザーとは、1 台のマシンで実行しているゲームに複数のプレーヤが対話している、コンソール ゲーム モデルのことです。(マルチ ユーザーは、マルチプレーヤとは違います。マルチプレーヤは ネットワーク ゲームと密接な関係があります。) コンソール上で最も代表的なマルチ ユーザー ゲーム シナリオにスポーツ タイトルがあります。これは、それぞれがコントローラを持った複数のプレーヤがチームでプレイするゲームです。コンソール環境には、デバイスの種類の違い がそのまま反映されないという利点があります。コンソール開発者が念頭に描くコントローラは 1 つだけか、あるいは、同じ入力仕様に準拠したライセンス コントローラです。また、コンソール ゲームの操作は、特にシットダウン アンド プレイ型のスポーツ ゲームでいえば、PC ゲーム モデルとして確立されたユーザー設定のコンセプトとの関連が強くありません。ユーザーが常時コントローラを取り替える状態では、確立されたユーザー設定を 把握するのは簡単ではありません。

では、Mapper によってコンソールにおける操作と PC マルチ ユーザーの操作方法とを完全に一致させることができるかというと、そうはいかないのです。しかし、アプリケーションやユーザーのニーズに合ったソリュー ションの設計に必要な情報が得られます。今回の記事で取り上げたアプリケーションである MultiMapper は、DirectX 8.0 SDK に組み込まれています。MultiMapper のサンプルはゲームではなく、入力ループと、ちょっとしたウィンドウ表示を備えたマネージャー クラスです。ゲーム グラフィックスの表示機能やスコアの保存機能もなく、生成するのは単純なユーザー インターフェイスだけです。その目的は、マルチユーザー コンテキストで Mapper 方式の入力パラダイムを展開することです。

本書では、MultiMapper のサンプルを簡単に取り上げ、必要に応じてサンプルからコードやコメントを直接引用してサンプルのデモにおける問題やソリューションを解説します。今回の 記事のほとんどが当初コメントとして入力されたものですが、その多くはさらに詳しいコメントの解説が付け加えられています。コード レビューは、2 つの基本セクションからなります。「アプリケーション コード」では、グローバル データ構造体、アプリケーション初期化、クリーンアップ、ウィンドウ メッセージ、ゲーム ループについて解説します。ただし、重要なコードのほとんどは、CInputDeviceManager というヘルパー クラスに含まれています。「CInputDeviceManager クラス」では、クラスの内部データ メンバ、初期化、クリーンアップ、エラー コード、デバイス列挙、割り当てについて解説します。

アプリケーション コード

アプリケーションは、アクション マップの定義、簡単なユーザー インターフェイスを持つウィンドウの作成と表示、 CInputDeviceManager クラスの作成、ユーザー入力の処理に必要なコードからなります。これらの要素は、サンプル全体でも最も簡単な部分であり、マネージャー クラスは、すべてのデバイス列挙と割り当てを処理します。

アプリケーション コードが実行するタスクの詳細については、以下の項で説明します。

データ構造体
  アクション マップ
  プレーヤのアクションとデバイス列挙構造体
アプリケーションの初期化
アプリケーション ウィンドウ プロセス
ゲーム ループとユーザー入力の操作
アプリケーション クリーンアップ

データ構造体

アクション マップ

アクション マップは、特定のマシンに接続した入力装置を DirectInput において抽象化するときに必要な情報をすべて格納した、Mapper 対応アプリケーションの基礎をなします。アクション マップは、アプリケーション定義の値 (DIACTION 構造体の uAppData メンバ) と、所定のゲームの種類 (これも DIACTION 構造体の dwSemantic) の共通アクションを表す値同士の関連付けを伝達します。

MultiMapper サンプルは、その uAppData 値に以下の定数を使います。この「ゲーム」でユーザーが実行できるアクションに対応した値です。このサンプルはスペース シミュレーター ゲームを想定したものです。したがって、関係のある入力としては、宇宙船の回転、推力制御、武器などがあります。また、ジョイスティックや同等のアナログ コントロールから軸データを受け取るために、軸用の定数も定義されています。

#define INPUT_LEFTRIGHT_AXIS   1L
#define INPUT_UPDOWN_AXIS      2L
#define INPUT_TURNLEFT         3L
#define INPUT_TURNRIGHT        4L
#define INPUT_FORWARDTHRUST    5L
#define INPUT_REVERSETHRUST    6L
#define INPUT_FIREWEAPONS      7L
#define INPUT_ENABLESHIELD     8L
#define INPUT_DISPLAYGAMEMENU  9L
#define INPUT_QUITGAME        10L

アクション マップは DIACTION 構造体の配列です。g_rgGameAction 配列は、DirectInput が実際の入力装置の入力をゲーム アクションにマップするときに必要なグローバル コレクション ゲーム アクションです。最初のカラムは、アプリケーション定義のコードであり、定義済みの乗数に先立つリストに表示されます。これらは、ユーザーが装置のコント ロールを物理的に起動するとゲームの入力ループに出力される定数です。2 番目のカラムは、アプリケーション定義のセマンティックにマップする物理的なアクションです。たとえば、以下の配列において、ユーザーがキーボード上の左 矢印キーを押すと、入力ループは INPUT_TURNLEFT の入力コードを受け取ります。最後のカラムは、DirectInput で構成ダイアログ ボックスを表示するためのテキスト文字列です。

#define NUMBER_OF_SEMANTICS 17

DIACTION g_rgGameAction[NUMBER_OF_SEMANTICS] =
{
    // ジャンルに沿って DirectInput が次善に定義する
    // デバイス入力 (ジョイスティックなど)。 
    // このアプリケーションのジャンルはシミュレータ。
    { INPUT_LEFTRIGHT_AXIS,  DIAXIS_SPACESIM_LATERAL,    0, _T("Turn"), },
    { INPUT_UPDOWN_AXIS,     DIAXIS_SPACESIM_MOVE,       0, _T("Move"), },
    { INPUT_FIREWEAPONS,     DIBUTTON_SPACESIM_FIRE,     0, _T("Shoot"), },
    { INPUT_ENABLESHIELD,    DIBUTTON_SPACESIM_GEAR,     0, _T("Enable shields"), },
    { INPUT_DISPLAYGAMEMENU, DIBUTTON_SPACESIM_DISPLAY,  0, _T("Display"), },

    // キーボード入力マッピング
    { INPUT_TURNLEFT,        DIKEYBOARD_LEFT,    0, _T("Turn left"), },
    { INPUT_TURNRIGHT,       DIKEYBOARD_RIGHT,   0, _T("Turn right"), },
    { INPUT_FORWARDTHRUST,   DIKEYBOARD_UP,      0, _T("Forward thrust"), },
    { INPUT_REVERSETHRUST,   DIKEYBOARD_DOWN,    0, _T("Reverse thrust"), },
    { INPUT_FIREWEAPONS,     DIKEYBOARD_F,       0, _T("Shoot"), },
    { INPUT_ENABLESHIELD,    DIKEYBOARD_S,       0, _T("Enable shields"), },
    { INPUT_DISPLAYGAMEMENU, DIKEYBOARD_D, DIA_APPFIXED, _T("Display"), },
    { INPUT_QUITGAME,        DIKEYBOARD_ESCAPE,  DIA_APPFIXED, _T("Quit Game"), },

    // マウス入力マッピング
    { INPUT_LEFTRIGHT_AXIS,  DIMOUSE_XAXIS,      0, _T("Turn"), },
    { INPUT_UPDOWN_AXIS,     DIMOUSE_YAXIS,      0, _T("Move"), },
    { INPUT_FIREWEAPONS,     DIMOUSE_BUTTON0,    0, _T("Shoot"), },
    { INPUT_ENABLESHIELD,    DIMOUSE_BUTTON1,    0, _T("Enable shields"), },
};

プレーヤのアクションとデバイス列挙構造体

PLAYERDATA 構造体には、ゲームにおける 1 人のプレーヤに関するゲーム状態の全データを格納します。簡単なビジュアル フィードバックを表示する以外にこのデータには使い道がありませんが、このような構造体では、ゲームで使用するデータの近似値が格納されます。 MultiMapper では、各プレーヤに 1 つずつ割り当てたこのような構造体を配列にしたものを利用して全プレーヤの状態を追跡します。WM_CREATE メッセージを受け取ると、ウィンドウ プロシージャで配列が初期化されます。

typedef struct _PLAYERDATA {
    BOOL  bTurningRight;
    BOOL  bReverseThrust;
    BOOL  bTurningLeft;
    BOOL  bForwardThrust;
    BOOL  bFiringWeapons;
    BOOL  bEnableShields;
    BOOL  bDisplayingMenu;
    DWORD dwLRAxisData;
    DWORD dwUDAxisData;
} PLAYERDATA, *LPPLAYERDATA;

ENUMDATA 構造体は、 EnumDevicesBySemantics コールバック メソッドに必要な情報を伝えます。ENUMDATA 構造体はさまざまな場所で使用され、マネージャー クラスの内部リストにデバイスを追加したり、アクション マップを構築、設定し、列挙のステータス (デバイスの有無) を戻します。構造体には、マネージャー クラスまでのポインタ、マネージャー クラスによる自身の初期化の方法を示すフラグ、アプリケーション呼び出しのためのウィンドウ ハンドルを格納します。その他のメンバはごく単純で、ゲームのプレーヤの数、すべてのプレーヤの名前に対応するマルチ sz 文字列でのポインタ、およびプレーヤのアクション フォーマットまでのポインタとなっています。最後に、この構造体は、BOOL フィールドの bRet を持ち、デバイスがプレーヤに割り当て済みであるかどうかを示す戻り値をここに格納します。

typedef struct _ENUMDATA {
    CInputDeviceManager* pDevMan;
    DWORD                dwCreateFlags;
    HWND                 hWnd;
    DWORD                dwPlayerNum;
    TCHAR*               pszPlayerName;
    LPDIACTIONFORMAT     lpdiaf;
    BOOL                 bRet;
} ENUMDATA, *LPENUMDATA;

アプリケーションの初期化

アプリケーションを開始すると、 WinMain 関数も他の Windows アプリケーションと同様に開始します。WinMain 関数は WNDCLASS 構造体を初期化し、最小限の ユーザー インターフェイス (UI) を表示する簡単なポップアップウィンドウを作成します。ウィンドウができて、表示されると、コードはモデル ダイアログを呼び出し、ゲームと対話するプレーヤの数をユーザーに照会します。プレーヤ数が決まるまで何も特別な処理は実行されず、WinMain はアプリケーションの CreateInputStuff 関数を呼び出します。

CreateInputStuff はデフォルトの初期化フラグで DirectInputヘルパー クラスを作成します (これについては、「CInputDeviceManager コンストラクタと Create()メソッド」参照)。これでヘルパー クラスに渡る DIACTIONFORMAT 構造体は、必要なデバイス タイプを指定します (現在の種類内で) 。種類は文書と dinput.h ヘッダ ファイルに定義します。DIACTIONFORMAT 構造体は、ゲーム アクション配列の指定にも使用します。

HRESULT CreateInputStuff( HWND hWnd, DWORD dwNumUsers )
{
    HRESULT hr;

    // このアプリケーションに適切な入力デバイスのアクション フォーマットをセットアップ
    DIACTIONFORMAT diaf;
    ZeroMemory( &diaf, sizeof(DIACTIONFORMAT) );
    diaf.dwSize        = sizeof(DIACTIONFORMAT);
    diaf.dwActionSize  = sizeof(DIACTION);
    diaf.dwDataSize    = NUMBER_OF_SEMANTICS * sizeof(DWORD);
    diaf.dwNumActions  = NUMBER_OF_SEMANTICS;
    diaf.guidActionMap = g_AppGuid;
    diaf.dwGenre       = DIVIRTUAL_SPACESIM;
    diaf.rgoAction     = g_rgGameAction;
    diaf.dwBufferSize  = BUFFER_SIZE;
    diaf.lAxisMin      = -100;
    diaf.lAxisMax      = 100;
    _tcscpy( diaf.tszActionMap, _T("MultiMapper Sample Application") );

    // 新しい入力デバイスマネージャを作成 
    g_pInputDeviceManager = new CInputDeviceManager();
    if( !g_pInputDeviceManager )
    {
        return E_OUTOFMEMORY;
    }

    hr = g_pInputDeviceManager->Create( hWnd, &dwNumUsers, &diaf, 
                                        APP_DICREATE_DEFAULT );

    if( FAILED(hr) )
    {
        TCHAR msg[MAX_PATH];
        int iRet;

このアプリケーションにおけるデバイスの「所有権」は、特定ユーザー向けのデバイス列挙の際に、DIEBSFL_RECENTDEVICE フラグの有無でわかります。このフラグがある場合、デバイスが最近そのユーザーによって使用されたことを表しており、MultiMapper は、同じユーザーが再びそのデバイスを使用すると想定します。

このアプリケーションにおけるデバイスの「所有権」は、特定ユーザー向けのデバイス列挙の際に、DIEBSFL_RECENTDEVICE フラグの有無でわかります。このフラグがある場合、デバイスが最近そのユーザーによって使用されたことを表しており、MultiMapper は、同じユーザーが再びそのデバイスを使用すると想定します。 論理上、ゲームに関るプレーヤの中で 1 人のユーザーだけが多くのデバイスを占有する可能性があります。その場合、MultiMapper は、デフォルト操作を無効にしてデフォルト構成を持つデバイスを各ユーザーに提供するフラグで、マネージャー クラスを再初期化します。初期化フラグについては、「初期化: CInputDeviceManager コンストラクタと Create()メソッド」を参照してください。

        switch(hr)
        {
        case E_APPERR_DEVICESTAKEN:
            sprintf( msg, 
                 _T("利用可能なデバイスより多いユーザーを入力しました、" \
                 "あるいはあるユーザーが多すぎるデバイスを要求しているかも
                 しれません。\n\n" \
                 "Yes をクリックして各ユーザーにデフォルトデバイスを与えてください、" \
                 "No をクリックするとアプリケーションを閉じます"));

            iRet = MessageBox( hWnd, msg, _T("Devices Are Taken"), 
                               MB_YESNO | MB_ICONEXCLAMATION );

            if(iRet == IDYES)
            {
                g_pInputDeviceManager->Cleanup();
                g_pInputDeviceManager->Create( hWnd, &dwNumUsers, 
                                               &diaf, APP_DICREATE_FORCEINIT );
            }
            else
                return E_FAIL;
            
            break;

もう 1 つのエラーは、マシンに接続されたデバイス数よりも多くのユーザーがプレイに参加した場合に発生します。この場合は、プレーヤ数が自動的に減少し、ヘルパー クラスが再初期化されて、ゲームのプレイが可能になります。

        case E_APPERR_TOOMANYUSERS:
            DWORD dwNumDevices = g_pInputDeviceManager->GetNumDevices();
            dwNumUsers = dwNumDevices;
            TCHAR* str = _T( "あなたが入力したユーカーの数に対して、" \
                             "十分なデバイスがシステムに装着されていません。\n\n" \
                             "ユーザーの数が自動的に変更され、" \
                             "%i (一ステムで有効なデバイスの数) となります。");

            sprintf( msg, str, dwNumDevices);
            MessageBox( hWnd, msg, _T("Too Many Users"), 
                        MB_OK | MB_ICONEXCLAMATION );

            g_pInputDeviceManager->Cleanup();
            g_pInputDeviceManager->Create( hWnd, &dwNumUsers, 
                                           &diaf, APP_DICREATE_DEFAULT );
            break;
        }
    }

    return S_OK;
}

強固なアプリケーションによって、キーボードの様々な部分で各プレーヤのコントロールが分割されることがありますが、MultiMapper サンプルではわかりやすさを優先するという主旨で、このアプローチは取り上げていません。

アプリケーション ウィンドウ プロセス

MultiMapper のためのウィンドウ プロセスはごく簡単であり、操作するのは、WM_PAINTWM_CHAR、および WM_DESTROY の 3 つのメッセージだけです。サンプルでは WM_PAINT を操作して、現在選択されているコントロール デバイスにおけるアクションに対応して最小限のビジュアル フィードバックを表示します。ユーザー インターフェイスの残りの部分は、今回の記事の後の方で解説する PaintPlayerStatus 関数で描画します。

        case WM_PAINT:
        {
            // ユーザーへメッセージを出力
            CHAR* str;
            HDC hDC = GetDC( hWnd );
            
            SetTextColor( hDC, RGB(255,255,255) );
            SetBkColor( hDC, RGB(0,0,0) );
            SetBkMode( hDC, OPAQUE );
            
            str = _T("Name");
            TextOut( hDC, 0, 0, str, lstrlen(str) );
            str = _T("Turn");
            TextOut( hDC, COL_SPACING*1, 0, str, lstrlen(str) );
            str = _T("Thrust");
            TextOut( hDC, COL_SPACING*2, 0, str, lstrlen(str) );
            str = _T("Weapon");
            TextOut( hDC, COL_SPACING*3, 0, str, lstrlen(str) );
            str = _T("Shield");
            TextOut( hDC, COL_SPACING*4, 0, str, lstrlen(str) );

            str = _T("Looking for game input... press Escape to exit.");
            TextOut( hDC, 0, 140, str, lstrlen(str) );

            str = _T("Press D to display input device settings.");
            TextOut( hDC, 0, 158, str, lstrlen(str) );

            ReleaseDC( hWnd, hDC );
            break;
        }

MultiMapper は、グローバルな、つまりプレーヤに依存しない、キーストロークを採用し、どのユーザーでもアプリケーションを終了したり、構成 UI を表示できるようになっています。特定プレーヤを対象にキーボードが構成される場合と、そうでない場合があるので、アクション マップでグローバル アクションを操作すると、誰もキーボードを使用していない場合に問題になる可能性が多少あります。したがって、グローバル アクションは、プレーヤ割り当てに無関係にキーボードアクション マップに埋め込まず、MultiMapper ではグローバル キーストロークを WM_CHAR メッセージとして操作します。この場合、アクション マップより Windows メッセージを使用した方が簡単です。

        case WM_CHAR:
            if( VK_ESCAPE == (TCHAR)wParam )
                SendMessage( hWnd, WM_DESTROY, 0, 0 );
            else if ( 0x64 == (TCHAR)wParam ) // 0x64 == 'D'
                InvokeDefaultUI(hWnd);
            break;

ユーザーがエスケープ キーを押すか、または標準 Windows UI を操作してウィンドウを閉じると、 WM_DESTROY メッセージがアプリケーションに送信されます。これを受け取ったコードは、アプリケーションのためにクリーンアップ コードを呼び出します。この詳細については、「アプリケーション クリーンアップ」を参照してください。

        case WM_DESTROY:
            Cleanup();
            PostQuitMessage( 0 );
            break;

ゲーム ループとユーザー入力の操作

GameLoop 関数は、アプリケーションを対象に適当に命名された入力ループです。この関数を開始すると、 CInputDeviceManager::GetNumUsers メソッドを呼び出されて、ゲームをプレイするユーザー数が取り出されます。そして、 CInputDeviceManager::GetDevicesForPlayer メソッドが呼び出されてすべてのプレーヤの間で操作が繰り返され、現在各プレーヤに割り当てられているデバイスを表す IDirectInputDevice8 インターフェイス ポインタの配列が取り出されます。

BOOL GameLoop( HWND hWnd )
{
    BOOL bRet;
    DIDEVICEOBJECTDATA didObjData;
    ZeroMemory( &didObjData, sizeof(didObjData) );
    static PLAYERDATA pd[MAX_USERS];    // 各プレーヤの状態
    LPDIRECTINPUTDEVICE8* pdidDevices;  
    
    DWORD dwNumPlayers = g_pInputDeviceManager->GetNumUsers();
            
    // 全てのプレーヤに対し全てのデバイスでループしゲーム入力をチェック。
    for( DWORD i=0; i < dwNumPlayers; i++ )
    {
        // 入力デバイスのリストへのアクセスを取得。
        g_pInputDeviceManager->GetDevicesForPlayer( i, &pdidDevices );

ユーザーのデバイス配列が取り出されたら、GameLoopParsePlayerInput 関数を呼び出して、前回入力ループが呼び出された後にそのユーザーを対象にバッファされたアクションを呼び出します。ParsePlayerInput コードの詳細については、この章の後の方で解説します。

        bRet = ParsePlayerInput(hWnd, pdidDevices, &pd[i]);
        if( !bRet )
        {
            return bRet;
        }

プレーヤの入力状態が PLAYERDATA 構造体にコンパイルされると、 GameLoop 関数によって論理的な錯綜は取り除かれます。たとえば、プレーヤが同時に左右に移動できるようにしても意味はあまりありません。このような錯綜はゲーム論 理によって異なり、DirectInput 意味マッピングに依存するものではありません。アプリケーションでは、別の方法でこれを実装してもかまいません。

        if( pd[i].bTurningLeft && pd[i].bTurningRight )
        {
            pd[i].bTurningLeft = pd[i].bTurningRight = FALSE;
        }
        if( pd[i].bForwardThrust && pd[i].bReverseThrust )
        {
            pd[i].bForwardThrust = pd[i].bReverseThrust = FALSE;
        }

この時点で、サンプルでは、ゲーム プレーヤの状態が把握されました。サンプルから PaintPlayerStatus 関数が呼び出され、プレーヤの状態がペイントされます。実際のゲームでは、これは場面の簡単な更新呼び出しになるでしょう。

        PaintPlayerStatus( hWnd, i, pd[i] );

先に述べたように、 ParsePlayerInput メソッドは、ユーザーが所有するすべてのデバイスから、バッファ アクションを取り出し、それらのアクションをプレーヤ状態構造体 PLAYERDATA に構文解析します。データは、プレーヤが実行しているゲーム中アクションのスナップショットを表します。この関数は、ゲーム アクションの検索と記憶用のパラメータを受け取ります。また、デバイス コントロールがこの目的にマップされたときに構成ユーザー インターフェイスを表示するためのウィンドウ ハンドルを受け取ります。これにより、同じタスクのグローバル キーストロークが増えます。ゲームによっては現在のデバイス構成を表示するためのコントロールを厳密に組み込む必要があるからです。

BOOL ParsePlayerInput
(HWND hWnd, LPDIRECTINPUTDEVICE8* ppdidDevices, LPPLAYERDATA ppd)
{
    HRESULT hr;
    DWORD   dwItems;
    DIDEVICEOBJECTDATA adod[BUFFER_SIZE];

    DWORD   dwDevice = 0;

この関数は、現在のユーザーに割り当てられたすべてのデバイスをループします。各デバイスにおける最初のアクションは、 IDirectInputDevice8::Poll メソッドに対する呼び出しです。デバイスの多くが USB なので、Poll メソッドは技術的にはオプションであり、USB は割り込み駆動バスです。ただし、 Poll メソッドは基本的に割り込み駆動デバイスでは空命令です。したがって、MultiMapper サンプルでは、どのデバイスでもデータ読み取りの前にはポーリングが実行され、またこれによってパフォーマンスが犠牲になることはありません。デバイスの取り込みに失敗して Poll メソッドが失敗する可能性があり、その場合は、このサンプルではただちにデバイスの再取得が試みられ、制御を呼び出し関数に戻します。その場合、入力ルー プの繰り返しのために、このデバイスのデータが失われます。たとえば、アクティブ アプリケーションからユーザーがタブ移動する場合などがそうですが、その場合の損失が重大な問題になることはめったにありません。

    while ( ppdidDevices[dwDevice] )
    {
        // 現在の状態を読み込むためにデバイスをポーリング 
        if( FAILED(ppdidDevices[dwDevice]->Poll() ) )  
        {
            // DirectInput は入力ストリームが妨害された事を教える。
            // 我々はポーリング中の状態をトラックできないので、
            // 行われなければならないリセットは無い。
            // 我々は再取得し再度トライする。
            hr = ppdidDevices[dwDevice]->Acquire();
            if( DIERR_INPUTLOST  == hr) 
            {
                hr = ppdidDevices[dwDevice]->Acquire();
            }

            // hr は DIERR_OTHERAPPHASPRIO 等のエラーの場合がある。
            // アプリケーションが最小化のときや変更中のとき発生する、
            // その場合後でもう一度トライ。
            return TRUE; 
        }

デバイスに対するポーリングが終了したので、コードは IDirectInputDevice8::GetDeviceData メソッドを呼び出してデバイスからバッファ アクションを呼び出します。アクションは、 DIDEVICEOBJECTDATA 構造体の uAppData の新しいメンバに格納されます。uAppData メンバにより入力の構文解析はごく簡単になりました。DirectInput では、先の IDirectInputDevice::SetActionMap に対する呼び出しで得られたアプリケーション定義の値に uAppData メンバを設定します。1 つの DIDEVICEOBJECTINSTANCE 構造体では、uAppData メンバがゲームで発生するアクションを表します。DIDEVICEOBJECTINSTANCE 構造体の配列により、入力ループの最後の繰返し後にユーザーが実行したゲーム アクションを集合的に記述します。

GetDeviceData が戻ると、アプリケーションは簡単な切り替え文でアクション配列の間をループし、どのアクションが実行されるかを判定します。各ゲーム アクションは、ユーザーの PLAYERDATA 構造体の対応するメンバに状態データを配置するケース文によって補足されます。これは、ゲームのイベントに呼応してビジュアルなフィードバックを表示するときに PaintPlayerStatus が使用します。

        dwItems = BUFFER_SIZE;
        hr = ppdidDevices[dwDevice]->GetDeviceData( sizeof(DIDEVICEOBJECTDATA),
                                                    adod, &dwItems, 0 );
        if( SUCCEEDED(hr) )
        {           
            // アクションを取得。入力イベントの数は dwItems に保存され、
            // 全てのイベントは "adod" 配列に保存される。
            // 各イベントは uAppData に保存される型を持り、
            // 実際のデータは dwData に保存される。
            for( DWORD j=0; j<dwItems; j++ )
            {
                // 軸なしのデータは"button pressed" あるいは "button
                // released"として受け取る。入力をそのようにパース。
                BOOL bState = (adod[j].dwData != 0 ) ? TRUE : FALSE;

                switch (adod[j].uAppData)
                {
                case INPUT_LEFTRIGHT_AXIS: // 左右軸データをパース 
                    (*ppd).dwLRAxisData  = adod[j].dwData;
                    (*ppd).bTurningRight = (*ppd).bTurningLeft  = FALSE;
                    if( (int)(*ppd).dwLRAxisData > 0 )
                        (*ppd).bTurningRight = TRUE;
                    else if( (int)(*ppd).dwLRAxisData < 0 )
                        (*ppd).bTurningLeft = TRUE;
                    break;

                case INPUT_UPDOWN_AXIS: // 上下軸データをパース 
                    (*ppd).dwUDAxisData   = adod[j].dwData;
                    (*ppd).bReverseThrust = (*ppd).bForwardThrust = FALSE;

                    if( (int)(*ppd).dwUDAxisData > 0 )
                        (*ppd).bReverseThrust = TRUE;
                    else if( (int)(*ppd).dwUDAxisData < 0 )
                        (*ppd).bForwardThrust = TRUE;
                    break;
                    
                case INPUT_TURNLEFT:        (*ppd).bTurningLeft    = bState; break;
                case INPUT_TURNRIGHT:       (*ppd).bTurningRight   = bState; break;
                case INPUT_FORWARDTHRUST:   (*ppd).bForwardThrust  = bState; break;
                case INPUT_REVERSETHRUST:   (*ppd).bReverseThrust  = bState; break;
                case INPUT_FIREWEAPONS:     (*ppd).bFiringWeapons  = bState; break;
                case INPUT_ENABLESHIELD:    (*ppd).bEnableShields  = bState; break;
                
                case INPUT_QUITGAME:        return FALSE;                
                
                case INPUT_DISPLAYGAMEMENU: 
                    (*ppd).bDisplayingMenu = bState; 
                    InvokeDefaultUI(hWnd);
                    return TRUE;
                }
            }
        }

        // リスト内の次のデバイスに進む
        dwDevice++;
        }

    return TRUE;
}

アプリケーション クリーンアップ

Cleanup 関数は、アプリケーション用のグローバルなクリーンアップ関数です。ウィンドウ プロセスでは WM_DESTROY メッセージを受け取るときに Cleanup を呼び出します。この関数は、マネージャー クラスの内部クリーンアップ メソッド (すべての DirectInput オブジェクトを削除) を呼び出して、マネージャー クラスそのものを削除します。

VOID Cleanup()
{
    if( g_pInputDeviceManager )
    {
        g_pInputDeviceManager->Cleanup();
        delete g_pInputDeviceManager;
        g_pInputDeviceManager = NULL;
    }
}

CInputDeviceManager クラス

MultiMapper サンプルでは、その重要な処理のほとんどが、 CInputDeviceManager クラス内で実行されます。このクラスは、アプリケーションから提供されたアクション マップを管理し、デバイス列挙を実行し、デバイス割り当てと再割り当てを処理します。これらのタスクの詳細については、以下の各項を参照してください。

データ メンバ
エラー コード
初期化: CInputDeviceManager コンストラクタと Create() メソッド
  列挙と割り当て
  デバイスの再割り当て
クリーンアップ: Cleanup() メソッドと CleanupDevices() メソッド

データ メンバ

CInputDeviceManager クラスは、いくつかのプライベート データ メンバを利用して、さまざまな DirectInput 関連のタスクを実行します。このクラスには、システムの入力装置のために IDirectInputDevice8 インターフェイスを生み出す IDirectInput8 インターフェイスが 1 つ格納されます。デバイス インターフェイスは二次元配列に格納されます。この場合、各列はユーザーに割り当てられたデバイスの一次元配列です。

このクラスには、現在のプレーヤ数とプレーヤ名の配列を追跡するためのデータ メンバを格納します (マルチ sz フォーマットで格納)。また、クライアント ウィンドウ ハンドル、アクション フォーマット構造体、クラス初期化の際に使用する作成フラグ、最後の列挙でシステムにアタッチしたすべての入力装置の数を格納するためのデータ メンバがあります。

    LPDIRECTINPUT8       m_pDI;
    LPDIRECTINPUTDEVICE8 m_ppdidDevices[MAX_USERS][MAX_DEVICES];
    DWORD                m_dwNumUsers;
    TCHAR*               m_szUserNames;
    HWND                 m_hWnd;
    DIACTIONFORMAT       m_diaf;
    DWORD                m_dwCreateFlags;
    DWORD                m_dwTotalDevices;

エラー コード

CInputDeviceManager クラスでは、2 つのカスタム COM エラー コードを使用します。COM 規則によると、これらは、それぞれエラー コード重大度とファシリティを表す SEVERITY_ERROR と FACILITY_ITF を使用する MAKE_HRESULT マクロで定義します。

列挙時にデフォルトで、クラスは recent というマークされたデバイスを現在のユーザーに割り当てます。場合によっては、1 人のプレーヤがデバイスの所有権を占有して他のユーザーがプレイできなくなることもあります。その場合は、クラスは E_APPERR_DEVICESTAKEN エラー コードを戻します。

#define E_APPERR_DEVICESTAKEN MAKE_HRESULT(SEVERITY_ERROR,FACILITY_ITF,998) 

プレーヤ数が、システムの現在のデバイス数を越えてると、E_APPERR_TOOMANYUSERS がマネージャー クラスから戻ります。たとえば、キーボードとマウスが 1 基ずつしかない 1 台のマシンでプレーヤ 4 人のゲームを要求すると、このコードが戻ります。

#define E_APPERR_TOOMANYUSERS MAKE_HRESULT(SEVERITY_ERROR,FACILITY_ITF,999)

初期化: CInputDeviceManagerコンストラクタと Create() メソッド

CInputDeviceManager クラスの初期化は、2 段階で実行されます。第 1 段階では、クラスのコンストラクタがクラスの内部データ メンバをゼロ初期化する間に、プレーヤ名の静的なリストが設定されます。

CInputDeviceManager::CInputDeviceManager()
{
    m_pDI                  = NULL;
    m_dwCreateFlags        = 0;
    m_dwTotalDevices       = 0;

    // 複数プレーヤの名前をパックして使う。
    m_szUserNames  = _T("Player 1\0Player 2\0Player 3\0Player 4\0\0");

    ZeroMemory( m_ppdidDevices, sizeof(m_ppdidDevices) );
}

初期化の第 2 段階は、クライアントが CInputDeviceManager::Create メソッドを呼び出すときに実行されます。このメソッドは DirectInput を作成し、プレーヤごとにデバイスを列挙します。デバイス割り当ては列挙コールバックで実行されます。デバイス列挙の呼び出し前に、このコードは、コール バック関数に必要な情報を伝える ENUMDATA 構造体を準備し、列挙とデバイス割り当てプロセスの成功と失敗を示す戻り値を受け取ります。

HRESULT CInputDeviceManager::Create( HWND hWnd, DWORD* lpdwNumUsers, 
                                     DIACTIONFORMAT* pdiaf, DWORD dwFlags )
{
    HRESULT hr;

    // 内部使用のため引数をコピー
    m_hWnd          = hWnd;
    memcpy( &m_diaf, pdiaf, sizeof(DIACTIONFORMAT) );
    m_dwNumUsers    = *lpdwNumUsers;
    m_dwCreateFlags = dwFlags;

    // メイン DirectInput オブジェクトを作成
    hr = DirectInput8Create( GetModuleHandle(NULL), DIRECTINPUT_VERSION, 
                             IID_IDirectInput8, (VOID**)&m_pDI, NULL );
    if( FAILED(hr) )
    {
        return E_FAIL;
    }

    TCHAR* pszNameScan = m_szUserNames;
    for(DWORD dwPlay = 0; dwPlay < m_dwNumUsers; dwPlay++)
    {
        ENUMDATA ed;
        ZeroMemory( &ed, sizeof(ed) );
        ed.pDevMan = this;
        ed.dwPlayerNum = dwPlay;
        ed.pszPlayerName = pszNameScan;
        ed.bRet = FALSE; // デバイスが見つかれば TRUE 。
        ed.lpdiaf = &m_diaf;
        ed.hWnd = m_hWnd;
        ed.dwCreateFlags = m_dwCreateFlags;

クラスの初期化の方法は、2 通りあります。これは、 Create メソッドに渡る dwFlags パラメータで制御します。デフォルト モードでは、クラスは最近使用したデバイスを適切なユーザーに割り当てます。これはユーザー名に基づいたプロセスです。

        if( m_dwCreateFlags == APP_DICREATE_DEFAULT)
        {
            // このユーザーに "最適" なデバイスを列挙
            hr = m_pDI->EnumDevicesBySemantics
                        ( pszNameScan, &m_diaf, 
                        EnumSuitableDevicesCB, 
                        (LPVOID)&ed, 
                        DIEDBSFL_THISUSER | DIEDBSFL_AVAILABLEDEVICES);
        }

このデフォルト方式では、1 人のユーザーが多くのデバイスを占有してゲームと対話できるプレーヤの数が制限される傾向があります。その場合は、APP_DICREATE_FORCEINIT 作成フラグでクラスを再初期化します。 Create が APP_DICREATE_FORCEINIT で呼び出されると、クラスでは 1 つのデバイスを各ユーザーに割り当て、過去のユーザー設定の傾向からデフォルト構成を生成します。このタスクはデバイスの再列挙で実行します。ただし、 DIEDBSFL_THISUSER フラグは省略します。これは、デバイス所有権のいかんに関係なく DirectInput でデバイス列挙を実行するためです。

        else // m_dwCreateFlags == APP_DICREATE_FORCEINIT
        {
            // 任意のユーザーに利用可能なデバイスを列挙
            hr = m_pDI->EnumDevicesBySemantics
                        ( NULL, &m_diaf, 
                        EnumSuitableDevicesCB, 
                        (LPVOID)&ed, DIEDBSFL_AVAILABLEDEVICES);
        }

        if( FAILED(hr))
        {
            return hr;
        }

列挙の後に、ENUMDATA 構造体の bRet メンバに格納される戻り値は、デバイス割り当て操作の成功と失敗を表します。値がゼロの場合、ユーザーに対するデバイス割り当てに失敗したことを表しま す。システムに接続されたデバイスの数が、ゲーム プレーヤの数より少なければ、このメソッドで E_APPERR_TOOMANYUSERS が戻ります。そうでなければ、ユーザーの 1 人が所有するデバイス数が多すぎて、プレーヤ間で公平にデバイスを割り当てることができない可能性が高くなります。その場合、メソッドは以下を戻します。

        if(!ed.bRet)
        {
            if(m_dwTotalDevices < m_dwNumUsers)
                return E_APPERR_TOOMANYUSERS;
            else
                return E_APPERR_DEVICESTAKEN;
        }

列挙と割り当て

MultiMapper では 2 通りの方法で IDirectInput8::EnumDevicesBySemantics を利用します。デフォルトでは、ユーザー設定に基づいてデバイスの列挙と割り当てが行われます。そして強制的初期化では、どのユーザーにも適したデバイスの列挙が行われます。この 2 つの方法の違いは、EnumDevicesBySemantics に渡る DIEDBSFL_THISUSER フラグの有無で区別します。クラス定義 EnumDevicesBySemanticsCB コールバック メソッドは、どちらの方法のコールバックでも受け取ります。数ある標準パラメータの中でコールバックが受け取るのは pContext です。これは、今回のサンプルではアプリケーション定義 ENUMDATA 構造体に対するポインタとして出てきています。この構造体は、デバイス割り当て呼び出し側へのデバイス割り当ての成功と失敗を表すときの重要な役割を果たします。ENUMDATA.pszPlayerName メンバもここでは重要です。ユーザー列挙が実行されるコールバックとの通信をするからです。

マネージャー クラスでは、プレーヤごとにすべてのデバイスを列挙するため、DirectInput はスタートアップ時に頻繁にメソッドを呼び出します (#_of_calls = #_of_players * installed_devices-#_of_owned_devices)。参考のため、メソッド定義の最初の部分を続けます。

データグラム出力関連のコードの多くは、この関数から取り除かれました。

BOOL CALLBACK CInputDeviceManager::EnumSuitableDevicesCB( 
      LPCDIDEVICEINSTANCE  pdidi,
      LPDIRECTINPUTDEVICE8 pdidDevice, 
      DWORD  dwFlags,
      DWORD  dwRemainingDevices,
      LPVOID pContext )
{
    HRESULT hr;
    ENUMDATA ed = *(LPENUMDATA)pContext;
    DWORD dwBuildFlags;    

    // 後で割り当てるために最新ではない/新しい デバイスを保持するテンポラリ配列。
    // リンク リストの方がより良い選択のはずだが、便宜上、単純な配列を使う。
    static LPDIRECTINPUTDEVICE8 lprgDevTemp[MAX_DEVICES]; 
    static DIDEVICEINSTANCE     didTemp[MAX_DEVICES];
    static int iIndex = 0;

DI8DEVTYPE_DEVICECTRL 型のデバイスは、音声通信など特殊目的用であり、通常はハイプライオリティ ゲーム アクション向きではありません。今回のサンプルでは、これらのタイプのデバイスを対象にしていないので、削除され、次のデバイスで列挙が続行します。

    if( DI8DEVTYPE_DEVICECTRL == GET_DIDEVICE_TYPE(pdidi->dwDevType) )
    {
        return DIENUM_CONTINUE;     
    }

このメソッドは、今回のプレーヤと同じプレーヤに対するゲームの過去の呼び出しでプレーヤが最後に使用したデバイスを割り当てます。 DIEDBS_RECENTDEVICE によってマークされたデバイスの前回の所有者がこのユーザーなので、今回もそのユーザーに必要だとみなされるわけです。こうして、このサンプルでは、現在 列挙が実行されているユーザーにデバイスを割り当てます。まず、MultiMapper は、 IDirectInputDevice8::SetCooperativeLevel メソッドを呼び出して、デバイスへの排他的なフォアグラウンド アクセスを要求します。連携レベルはアクション マップの設定に無関係なので、アプリケーションでは提携レベルを他の場所に設定してもかまいません。MultiMapper は、 IDirectInputDevice8::BuildActionMap を呼び出して、ユーザーのアクション マップを構築し、次に IDirectInputDevice8::SetActionMap を呼び出して、そのデバイスまでのアクション マップを設定します。SetActionMap コールでは、デバイスの所有権を効果的にユーザーに割り当てます。デバイスを割り当てた後、DIEDBSFL_THISUSER フラグで EnumDevicesBySemantics を呼び出すと、他のユーザーが所有するデバイスは列挙されません (ユーザー名文字列の簡単な比較にもとづきます) 。

呼び出し側が EnumDevicesBySemantics メソッドを呼び出し、DIEDSBFL_THISUSER フラグを渡さないとデバイスには DIEDBS_RECENTDEVICE フラグによるマークが付かないので注意してください。

    if( dwFlags & DIEDBS_RECENTDEVICE )
    {
        // 協調レベルの設定。
        hr = pdidDevice->SetCooperativeLevel( ed.hWnd, 
                                             DISCL_EXCLUSIVE|DISCL_FOREGROUND );
        if( FAILED(hr) )
        {
            return DIENUM_CONTINUE;     
        }

        // このプレーヤのアクションマップを設定し、
        // DirectInput の有効デバイス内部リストから削除する。
        dwBuildFlags = (ed.dwCreateFlags == APP_DICREATE_DEFAULT) ? 
                        DIDBAM_DEFAULT : DIDBAM_HWDEFAULTS;
        pdidDevice->BuildActionMap( ed.lpdiaf, ed.pszPlayerName, dwBuildFlags );
        hr = pdidDevice->SetActionMap
                         ( ed.lpdiaf, ed.pszPlayerName, DIDSAM_DEFAULT ); 

        // デバイスをデバイスマネージャの内部リストに追加する。
        ed.pDevMan->AddDeviceForPlayer( ed.dwPlayerNum, pdidi, pdidDevice );

        // BuildActionMap が失敗するなら、SetActionMap も失敗する。
        // ここでは便宜上シングル エラー チェックを使う。
        // このデバイスのコントロールが不適切なとき失敗する、
        // しかし他のデバイスは OK かもしれないので、列挙を続ける。
        if( FAILED(hr) )
        {
            return DIENUM_CONTINUE;
        }
        
        // ENUMDATA 構造体に戻り値をセットし、
        // 成功を指示して列挙を停止。
        ((LPENUMDATA)pContext)->bRet = TRUE;
    }

DIEDBS_NEWDEVICE フラグや DIEDBS_MAPPEDPRI1 フラグでマークされたデバイスは、このユーザーに対する最近のデバイスが見つからない時のイベントで使用されるフラット リストに追加されます。これらのデバイスは、そのユーザーにとっては新規デバイスであるか、ゲームに重要なアクションを割り当て可能であることを表しま す。現在のプレーヤに最新のデバイスが見つからない場合は、これらのフラグによりマークがあるデバイスはどれも使用される可能性があります。

    else if( dwFlags & ( DIEDBS_MAPPEDPRI1 | DIEDBS_NEWDEVICE ) )
    {   // これは最新デバイスでも新しいデバイスでもない、
        // デバイスの内部リストにそれを追加する。
        // 最近使ったデバイスや新しいデバイスを見つけられずに終わったら、
        // このどちらかをトライする。
        lprgDevTemp[iIndex] = pdidDevice;
        didTemp[iIndex]     = *(LPDIDEVICEINSTANCE)pdidi;
        lprgDevTemp[iIndex++]->AddRef();

    }

コールバック関数には、各呼び出しで列挙される残りのデバイスの数が渡ります。この情報を使用すれば、残りのデバイスがいつなくなって、現在のユー ザーに対する新たなコールバックがいつ終了するかを簡単に通知できます。現在のユーザー向けの最新デバイスが見つからない場合、このメソッドはフラット リストの最初のデバイスを取り出し (一般には最適なデバイス)、その他のデバイスは無視します。これはサンプルとしての単純化を意図したものなので、実際のアプリケーションでは、このよう なデバイスについてアクション マップ間で繰り返し実行され、割り当てられたアクションの中でもゲームで最も重要なアクションが選択されます。デフォルト列挙の実行中は、ユーザー設定が 優先します。強制的割り当て列挙では、このサンプルは、ハードウェア デフォルトを採用し、各ユーザーが完全に構成済みのデバイスを受け取ります。

    if( 0 == dwRemainingDevices &&  (((LPENUMDATA)pContext)->bRet != TRUE) ) 
    {
        dwBuildFlags = (ed.dwCreateFlags == APP_DICREATE_DEFAULT) ? 
                       DIDBAM_DEFAULT : DIDBAM_HWDEFAULTS;

        // このプレーヤのアクションマップを削除し、DirectInput の
        // 内部有効デバイス リストからそれを削除する。
        if( lprgDevTemp[0] )
        {
            // デバイスをデバイス マネージャの内部リストに追加する。
            ed.pDevMan->AddDeviceForPlayer
                        ( ed.dwPlayerNum, &didTemp[0], lprgDevTemp[0] );

            lprgDevTemp[0]->BuildActionMap
                            ( ed.lpdiaf, ed.pszPlayerName, dwBuildFlags );
            hr = lprgDevTemp[0]->SetActionMap
                            ( ed.lpdiaf, ed.pszPlayerName, DIDSAM_DEFAULT ); 

            // BuildActionMap が失敗するなら、SetActionMap も失敗する。
            // ここでは便宜上シングル エラー チェックを使う。
            // このデバイスのコントロールが不適切なとき失敗する、
            // しかし他のデバイスは OK かもしれないので、列挙を続ける。
            if( FAILED(hr) )
                return DIENUM_CONTINUE;

            ((LPENUMDATA)pContext)->bRet = TRUE;
        }

        // リストに残っているデバイス全てを削除する。
        for (int i=1 ; iRelease();
            }
        }

        iIndex = 0; // 次のパスのためにカウンタをリセット。
    }

    return DIENUM_CONTINUE;
}

デバイスの再割り当て

DirectInput Mapper デフォルト UI が ConfigureDevices コールから戻ると、必ずデバイスが再割り当てされます。ConfigureDevices の戻りの後で、一部のデバイスの構成が異なっていたり、比較的上位に権限が変更されている可能性があります。同じように、以前は未所有であったデバイスが 今回は誰かによって所有されている可能性もあります。デバイスの再割り当てと再構成は、列挙を利用すれば簡単です。ConfigureDevices コールでは、戻りの前にデバイスが自動的に再割り当てされるので、システムの各デバイスからは、それぞれの DIPROP_USERNAME プロパティの最新のユーザー名が戻ります。

VOID CInputDeviceManager::ReassignDevices()
{
    // マシンの全てのデバイスのフラットな配列。
    LPDIRECTINPUTDEVICE8 prgAllDev[MAX_DEVICES];

    TCHAR szNameDev[MAX_PATH];
    DWORD dwDev;

このメソッドは、マネージャー クラスの内部リスト (所有者と対象デバイス) のフラッシュで開始し、DIEDBSFL_THISUSER フラグは省略した状態で NULLユーザー名で EnumDevicesBySemantics を呼び出します。この項の後の方で出てくる BuildFlatListCB コールバック メソッドは、列挙されたすべてのデバイスを配列に追加するだけです。

    // 各ユーザーのクラス内部配列をクリーンアウト。
    CleanupDevices( APP_CLEANUP_PRESERVEASSIGNMENT );

    // マシンに現在接続されている全てのデバイスの単純なフラット リストを構築。
    // この配列は各ユーザーへのデバイスの再割り当てに使う。
    // 
    // NULL ユーザー名を使い、DIEDBSFL_THISUSER フラグを使わずに全ての
    // デバイスを列挙する。
    ZeroMemory( prgAllDev, sizeof(prgAllDev) );
    m_pDI->EnumDevicesBySemantics( NULL, &m_diaf, BuildFlatListCB, 
                                   prgAllDev, DIEDBSFL_ATTACHEDONLY); 
   

リストができると、コードは配列内のすべてのデバイスをロールして現在のユーザー名を取り出します (DIPROP_USERNAME で)。デバイスにユーザー名がある場合、このメソッドは CInputDeviceManager::AddDeviceForPlayer メソッドを呼び出して、ふさわしいユーザーにそのデバイスを割り当てます。

    DIPROPSTRING dips;
    dips.diph.dwSize       = sizeof(DIPROPSTRING); 
    dips.diph.dwHeaderSize = sizeof(DIPROPHEADER); 
    dips.diph.dwObj        = 0; // デバイス プロパティ
    dips.diph.dwHow        = DIPH_DEVICE; 

    // 今システムに接続されている全てのデバイスを持った配列を取得した。
    // それをループして、テンポラリ配列のプレーヤにそれを割り当てる。
    dwDev = 0;
    while( prgAllDev[dwDev] )
    {
        prgAllDev[dwDev]->GetProperty( DIPROP_USERNAME,
                                       &dips.diph );
        
        // 文字列を Unicode から ANSI に変換。
        WideCharToMultiByte( CP_ACP, 0, dips.wsz, -1,
                             szNameDev, MAX_PATH,NULL, NULL);

        // このデバイスが今誰に割り当てられているかを確認 (DWORD 値で)。
        // そのデバイスが割り当てられていない (i.e. ユーザー名なし) なら、スキップする。
        if( strlen(szNameDev) )
        {
            DWORD dwAssignedTo;
            dwAssignedTo = (DWORD) 
                         ( (byte)szNameDev[strlen(szNameDev)-1] - ((byte)'1') );
            
            DIDEVICEINSTANCE didi;
            ZeroMemory( &didi, sizeof(didi) );
            didi.dwSize = sizeof(didi);
            prgAllDev[dwDev]->GetDeviceInfo( &didi );

            // そのユーザーが再び使用できるデバイスを取得。
            prgAllDev[dwDev]->BuildActionMap( &m_diaf, szNameDev, DIDBAM_DEFAULT );
            prgAllDev[dwDev]->SetActionMap( &m_diaf, szNameDev, DIDSAM_DEFAULT );
            prgAllDev[dwDev]->SetCooperativeLevel
                              ( m_hWnd, DISCL_EXCLUSIVE|DISCL_FOREGROUND );
            
            // 今プレーヤにそれを追加。
            AddDeviceForPlayer( dwAssignedTo, &didi, prgAllDev[dwDev] );
        }

        dwDev++;
    }

もちろん、残りのデバイスに所有者はないのでデバイス プールに戻され、インターフェイス ポインタが解放されます。

    // すべてのデバイスが新しい配列内のユーザーに割りあてられた。
    // ローカルフラット配列をクリーンアップ
    dwDev = 0;
    while( prgAllDev[dwDev] )
    {
        prgAllDev[dwDev]->Release();
        prgAllDev[dwDev] = NULL;
        dwDev++;
    }
}

参考のため ReassignDevices で呼び出す列挙用の BuildFlatListCB コールバック関数をここに示します。このメソッドは、マシン上のすべてのデバイスを表すフラット配列に、デバイスを追加するだけです。

BOOL CALLBACK CInputDeviceManager::BuildFlatListCB( LPCDIDEVICEINSTANCE  pdidi,
                                                    LPDIRECTINPUTDEVICE8 pdidDevice, 
                                                    DWORD  dwFlags,
                                                    DWORD  dwRemainingDevices,
                                                    LPVOID pContext )
{
    static DWORD dwIndex = 0;
    LPDIRECTINPUTDEVICE8* pdidDevArray;
    pdidDevArray = (LPDIRECTINPUTDEVICE8*)pContext;

    pdidDevArray[dwIndex++] = pdidDevice;
    pdidDevice->AddRef(); // どんなインターフェイスにも AddRef すること。

    if(!dwRemainingDevices)
    {
        dwIndex = 0; 
    }

    return DIENUM_CONTINUE;
}

クリーンアップ: Cleanup()とCleanupDevices()メソッド

CInputDeviceManager クラスで使用したインターフェイスのクリーンアップは、2 段階で実行します。どちらの段階もクライアントから CInputDeviceManager::Cleanup メソッドを呼び出すたびに呼び出されます。

CleanupDevices メソッドでは、クリーンアップの最初の段階を実行します。これはクラスが使用中のすべての DirectInput デバイスを解放して無効にするプロセスです。デバイス クリーンアップは、DirectInput の削除とは別に実行されます。これは、DirectInput をすべて再初期化せずにデバイス リストを再構築することが望ましい場合があるからです。デバイスの再割り当てが、まさにそのような場合であり、 CInputDeviceManager::ReassignDevices のコードは、Cleanup メソッドを呼び出さずに CleanupDevices を呼び出します。

CleanupDevices メソッドはクラスが使用した DirectInput デバイス オブジェクトを逆取得し、解放します。デバイス所有権情報はインターフェイスのライフタイムの制約を受けなくので、デバイスの再割り当てをすると、デバイ スの最後の所有者がメモリに残ります。これは特殊な事例です。このメソッドのデフォルトの動作では、DIDSAM_NOUSER フラグとともに IDirectInputDevice8::SetActionMap メソッドが呼び出されて、現在のオーナーが無効になります。

VOID CInputDeviceManager::CleanupDevices(DWORD dwCleanFlags)
{
    DWORD dwD = 0;

    for( DWORD dwP = 0; dwP < m_dwNumUsers; dwP++ )
    {
        while( m_ppdidDevices[dwP][dwD] )
        {     
            m_ppdidDevices[dwP][dwD]->Unacquire();
            if(APP_CLEANUP_DEFAULT == dwCleanFlags)
            {
                m_ppdidDevices[dwP][dwD]->SetActionMap
                ( &m_diaf, NULL, DIDSAM_NOUSER );
            }
            m_ppdidDevices[dwP][dwD]->Release();
            m_ppdidDevices[dwP][dwD] = NULL;
            dwD++;
        }

        dwD = 0;
    }

最後のクリーンアップ段階は、CleanupDevices の戻りの後の Cleanup メソッドで実行されます。他のクリーンアップ コードの場合と同じく、このメソッドでは、クラスで使用した残りの DirectInput オブジェクトが解放されます。

VOID CInputDeviceManager::Cleanup()
{
    CleanupDevices( APP_CLEANUP_DEFAULT );
    
    if( m_pDI )
    {
        m_pDI->Release();
    }

    m_pDI = NULL;
}

結論

DirectInput Mapper は、ほとんどの入力開発ニーズに対応する、すぐれたテクノロジです。デバイス抽象化用の基本機能と、デバイスに割り当てられたアクションは、入力開発者と ユーザーのどちらの場合も手間を省くことができます。初歩段階のテクノロジにとっては、アプリケーションにトランスペアレントなマルチ ユーザーの多数デバイス操作の有効化 (およびそれらデバイスにおける構成) に関する問題への取り組みは、野心的な試みです。

DirectX 8.0 で利用したソリューションは、ライトウェイトなデバイス所有権をサポートし、デバイス所有権と構成の動的な変更を可能にし、今日のコンソールの単純化に寄 与しています。しかしながら、つねに変化し続ける PC の特性と PC 接続入力装置は、Mapper とそのクライアント アプリケーションのいずれにとっても大きな課題です。MultiMapper コードで使用されている技術は、デバイスの動的所有権モデルの実装方法として妥当な方法ですが、種類の異なるゲームのニーズに対して 1 つの方法で対応するのは困難でしょう。うまくいけば、今回の記事で示した概念から、Mapper を利用して各アプリケーションやユーザーに固有の課題を、シンプルで信頼性が高く、コーディングが簡単な方法で解決できるアイデアが生まれるかもしれませ ん。

ページのトップへ ページのトップへ