コントロールの追加

Note

このトピックは、「DirectX を使った単純なユニバーサル Windows プラットフォーム (UWP) ゲームの作成」チュートリアル シリーズの一部です。 リンク先のトピックでは、このシリーズのコンテキストを説明しています。

[ Windows 10 の UWP アプリ向けに更新。 Windows 8.x の記事については、アーカイブをご覧ください ]

優れたユニバーサル Windows プラットフォーム (UWP) ゲームでは、さまざまなインターフェイスがサポートされています。 潜在的なプレーヤーは、物理ボタンのないタブレット、ゲーム コントローラが接続された PC、または高性能マウスとゲーム キーボードを備えた最新のデスクトップ ゲーム装置に Windows 10 を搭載している可能性があります。 このゲームでは、MoveLookController クラスにコントロールが実装されています。 このクラスは、3 種類の入力 (マウスとキーボード、タッチ、ゲームパッド) すべてを 1 つのコントローラに統合します。 最終的な成果は、複数のデバイスで動作するジャンル標準ムーブ/ルック コントロールを使用する一人称シューティング ゲームです。

Note

コントロールの詳細については、「ゲームのムーブ/ルック コントロール」および「ゲームのタッチ コントロール」をご覧ください。

目的

この時点でレンダリングするゲームがありますが、プレイヤーを動かす、またはターゲットを撃つことはできません。 ここでは UWP DirectX ゲームで、次の種類の入力に対して、一人称シューティング ムーブ/ルック コントロールをゲームに実装する方法について説明します。

  • マウスとキーボード
  • タッチ
  • Gamepad

Note

このサンプルの最新ゲーム コードをダウンロードしていない場合は、Direct3D サンプル ゲームのページに移動してください。 このサンプルは、UWP 機能サンプルの大規模なコレクションの一部です。 サンプルをダウンロードする方法については、「Windows 開発用のサンプル アプリケーション」をご覧ください。

共通コントロールの動作

タッチ コントロールとマウス/キーボード コントロールには、非常によく似たコア実装があります。 UWP アプリでは、ポインターは画面上の単なるポイントです。 マウスをスライドさせる、またはタッチ スクリーンで指をスライドさせて移動できます。 その結果、1 つのイベント セットに登録でき、プレイヤーがポインターを移動して押すのにマウスを使っているかタッチ スクリーンを使っているかを考慮する必要はありません。

このサンプル ゲームの MoveLookController クラスを初期化すると、ポインターに固有の 4 つのイベントとマウスに固有の 1 つのイベントが登録されます。

イベント 説明
CoreWindow::PointerPressed マウスの左ボタンまたは右ボタンが押された (押し続けた) か、タッチ画面がタッチされました。
CoreWindow::PointerMoved マウスが動いたか、タッチ面でドラッグ操作が行われました。
CoreWindow::PointerReleased マウスの左ボタンが放されたか、タッチ面に接触しているオブジェクトが持ち上げられました。
CoreWindow::PointerExited ポインターがメイン ウィンドウの外に移動しました。
Windows::Devices::Input::MouseMoved マウスが一定の距離を移動しました。 現在の X-Y 位置ではなく、マウス移動のデルタ値のみに関心があることに注意してください。

これらのイベント ハンドラーは、MoveLookController がアプリケーション ウィンドウで初期化された直後に、ユーザー入力のリッスンを開始するよう設定されます。

void MoveLookController::InitWindow(_In_ CoreWindow const& window)
{
    ResetState();

    window.PointerPressed({ this, &MoveLookController::OnPointerPressed });

    window.PointerMoved({ this, &MoveLookController::OnPointerMoved });

    window.PointerReleased({ this, &MoveLookController::OnPointerReleased });

    window.PointerExited({ this, &MoveLookController::OnPointerExited });

    ...

    // There is a separate handler for mouse-only relative mouse movement events.
    MouseDevice::GetForCurrentView().MouseMoved({ this, &MoveLookController::OnMouseMoved });

    ...
}

InitWindow の完全なコードは、GitHub で確認できます。

ゲームが特定の入力をいつリッスンするかを判断するために、MoveLookController クラスには、コントローラーの種類に関係なく、次の 3 つのコントローラー固有の状態があります。

完了状態 説明
なし これは、コントローラーが初期化された状態です。 ゲームがコントローラーの入力を予測していないため、すべての入力は無視されます。
WaitForInput コントローラーは、プレイヤーがマウスの左クリック、タッチ イベント、ゲームパッドのメニュー ボタンのいずれかを使用して、ゲームからのメッセージを確認するのを待機しています。
アクティブです コントローラーはアクティブなゲームプレイ モードです。

WaitForInput 状態とゲームの一時停止

ゲームが一時停止されると、ゲームは WaitForInput 状態になります。 これは、プレイヤーがゲームのメイン ウィンドウの外にポインターを移動するか、一時停止ボタン (P キーまたはゲームパッドの [スタート] ボタン) を押すと発生します。 MoveLookController はプレスを登録し、IsPauseRequested メソッドを呼び出すときにゲーム ループに通知します。 その時点で IsPauseRequestedtrue を返した場合、ゲーム ループは MoveLookControllerWaitForPress を呼び出して、コントローラーを WaitForInput 状態に移行します。

WaitForInput 状態になると、ゲームはアクティブ状態に戻るまで、ほぼすべてのゲームプレイ入力イベントの処理を停止します。 例外は一時停止ボタンで、これを押すとゲームがアクティブな状態に戻ります。 一時停止ボタン以外の方法で、ゲームを Active 状態に戻すには、プレイヤーはメニュー項目を選択する必要があります。

アクティブ状態

アクティブ状態の間、MoveLookController インスタンスは、有効なすべての入力デバイスからのイベントを処理し、プレイヤーの意図を解釈します。 その結果、プレイヤーのビューの速度と視線の方向が更新され、Update がゲーム ループから呼び出された後にゲームと更新されたデータが共有されます。

すべてのポインター入力は、異なるポインター アクションに対応する異なるポインター ID で、アクティブ状態で追跡されます。 PointerPressed イベントを受信すると、MoveLookController はウィンドウによって作成されたポインター ID 値を取得します。 ポインター ID は、特定の種類の入力を表します。 たとえば、マルチタッチ デバイスでは、複数の異なるアクティブな入力が同時に存在する可能性があります。 ID は、プレイヤーが使用している入力を追跡するために使用されます。 1 つのイベントがタッチ スクリーンの移動矩形内にある場合は、移動矩形内のポインター イベントを追跡するためのポインター ID が割り当てられます。 ファイア矩形内の他のポインター イベントは、別のポインター ID で別に追跡されます。

Note

ゲームパッドのマウスと右サムスティックからの入力には、別に処理される ID もあります。

ポインター イベントが特定のゲーム アクションにマップされたら、MoveLookController オブジェクトがメイン ゲーム ループと共有するデータを更新します。

このサンプル ゲームの Update メソッドは、呼び出されると、入力を処理し、速度とルック方向の変数 (m_velocitym_lookdirection) を更新します。この後、ゲーム ループは、Velocity および LookDirection パブリック メソッドを呼び出すことで、これらの変数を取得します。

Note

Update メソッドの詳細については、このページで後述します。

ゲーム ループは、MoveLookController インスタンスで IsFiring メソッドを呼び出すことによって、プレイヤーが起動しているかどうかをテストできます。 MoveLookController は、プレイヤーが 3 種類の入力のいずれかでファイア ボタンを押したかどうかを確認します。

bool MoveLookController::IsFiring()
{
    if (m_state == MoveLookControllerState::Active)
    {
        if (m_autoFire)
        {
            return (m_fireInUse || (m_mouseInUse && m_mouseLeftInUse) || PollingFireInUse());
        }
        else
        {
            if (m_firePressed)
            {
                m_firePressed = false;
                return true;
            }
        }
    }
    return false;
}

次は、3 種類のコントロールのそれぞれの実装について少し詳しく説明します。

相対マウス コントロールの追加

マウスの動きが検出された場合は、その動きを使用して、カメラの新しいピッチとヨーを決定します。 これは、相対マウス コントロールを実装し、マウスが移動した相対距離 (移動の開始と停止の間の差分) を処理することで実装します。つまり、モーションの絶対 x-y ピクセル座標を記録するのとは対照的です。

そのためには、MouseMoved イベントによって返される Windows::Device::Input::MouseEventArgs::MouseDelta 引数オブジェクトの MouseDelta::X フィールドと MouseDelta::Y フィールドを調べることで、X (水平モーション) と Y (垂直モーション) 座標の変化を取得します。

void MoveLookController::OnMouseMoved(
    _In_ MouseDevice const& /* mouseDevice */,
    _In_ MouseEventArgs const& args
    )
{
    // Handle Mouse Input via dedicated relative movement handler.

    switch (m_state)
    {
    case MoveLookControllerState::Active:
        XMFLOAT2 mouseDelta;
        mouseDelta.x = static_cast<float>(args.MouseDelta().X);
        mouseDelta.y = static_cast<float>(args.MouseDelta().Y);

        XMFLOAT2 rotationDelta;
        // Scale for control sensitivity.
        rotationDelta.x = mouseDelta.x * MoveLookConstants::RotationGain;
        rotationDelta.y = mouseDelta.y * MoveLookConstants::RotationGain;

        // Update our orientation based on the command.
        m_pitch -= rotationDelta.y;
        m_yaw += rotationDelta.x;

        // Limit pitch to straight up or straight down.
        float limit = XM_PI / 2.0f - 0.01f;
        m_pitch = __max(-limit, m_pitch);
        m_pitch = __min(+limit, m_pitch);

        // Keep longitude in sane range by wrapping.
        if (m_yaw > XM_PI)
        {
            m_yaw -= XM_PI * 2.0f;
        }
        else if (m_yaw < -XM_PI)
        {
            m_yaw += XM_PI * 2.0f;
        }
        break;
    }
}

タッチ サポートの追加

タッチ コントロールは、タブレットでユーザーをサポートするのに最適です。 このゲームでは、特定のゲーム内アクションに合わせて画面の特定の領域をゾーニングすることで、タッチ入力を収集します。 このゲームのタッチ入力では、3 つのゾーンが使用されます。

move look touch layout

次のコマンドは、タッチ コントロールの動作をまとめたものです。 ユーザー入力 | アクション :------- | :-------- 四角形の移動 | タッチ入力は仮想ジョイスティックに変換され、垂直方向のモーションは前/後の位置モーションに変換され、水平方向のモーションは左/右の位置モーションに変換されます。 ファイア四角形 | 球体を発射します。 ムーブとファイアの四角形の外側でタッチ | カメラ ビューの回転 (ピッチとヨー) を変更します。

MoveLookController は、ポインター ID をチェックしてイベントが発生した場所を特定し、次のいずれかのアクションを実行します。

  • PointerMoved イベントが移動またはファイア矩形で発生した場合は、コントローラーのポインター位置を更新します。
  • PointerMoved イベントが画面の残りの部分で発生した場合 (ルック コントロールとして定義)、視線方向ベクトルのピッチとヨーの変化を計算します。

タッチ コントロールを実装すると、前に Direct2D を使用して描画した矩形は、移動ゾーン、ファイア ゾーン、ルック ゾーンがどこにあるかをプレイヤーに示します。

touch controls

次に、各コントロールを実装する方法を見てみましょう。

ムーブ コントローラーとファイア コントローラー

画面左下クアドラントにある移動コントローラー矩形は、方向パッドとして使用されます。 このスペース内で親指を左右にスライドさせるとプレイヤーが左右に移動し、上下にスライドさせるとカメラが前後に移動します。 これを設定した後、画面右下クアドラントにあるファイア コントローラーをタップすると、球が発射されます。

SetMoveRect メソッドと SetFireRect メソッドは、入力矩形を作成し、2 つの 2D ベクトルを使用して、画面上の各矩形の左上隅と右下隅の位置を指定します。

次に、m_fireUpperLeftm_fireLowerRight にパラメーターが割り当てられます。これは、ユーザーが四角形の内部をタッチしているかどうかを判断するのに役立ちます。

m_fireUpperLeft = upperLeft;
m_fireLowerRight = lowerRight;

画面のサイズが変更されると、これらの矩形は適切なサイズに再描画されます。

これでコントロールをゾーン化したので、次にユーザーがそれらを実際に使用しているタイミングを判断します。 これを行うには、ユーザーがポインターを押す、移動する、または離すときに、MoveLookController::InitWindow メソッドにいくつかのイベント ハンドラーを設定します。

window.PointerPressed({ this, &MoveLookController::OnPointerPressed });

window.PointerMoved({ this, &MoveLookController::OnPointerMoved });

window.PointerReleased({ this, &MoveLookController::OnPointerReleased });

まず、OnPointerPressed メソッドを使用して、ユーザーが移動またはファイア矩形で最初に押したときに何が起こるかを決定します。 ここでは、コントロールに触れている場所と、そのコントローラーにポインターが既に存在するかどうかを確認します。 これが特定のコントロールに触れる最初の指である場合は、次の操作を行います。

  • タッチの位置を 2D ベクトルとして m_moveFirstDown または m_fireFirstDown に格納します。
  • ポインター ID を m_movePointerID または m_firePointerID に割り当てます。
  • コントロールのアクティブなポインターが取得できたら、適切な InUse フラグ (m_moveInUse または m_fireInUse) を true に設定します。
PointerPoint point = args.CurrentPoint();
uint32_t pointerID = point.PointerId();
Point pointerPosition = point.Position();
PointerPointProperties pointProperties = point.Properties();
auto pointerDevice = point.PointerDevice();
auto pointerDeviceType = pointerDevice.PointerDeviceType();

XMFLOAT2 position = XMFLOAT2(pointerPosition.X, pointerPosition.Y);

...
case MoveLookControllerState::Active:
    switch (pointerDeviceType)
    {
    case winrt::Windows::Devices::Input::PointerDeviceType::Touch:
        // Check to see if this pointer is in the move control.
        if (position.x > m_moveUpperLeft.x &&
            position.x < m_moveLowerRight.x &&
            position.y > m_moveUpperLeft.y &&
            position.y < m_moveLowerRight.y)
        {
            // If no pointer is in this control yet.
            if (!m_moveInUse)
            {
                // Process a DPad touch down event.
                // Save the location of the initial contact
                m_moveFirstDown = position;
                // Store the pointer using this control
                m_movePointerID = pointerID;
                // Set InUse flag to signal there is an active move pointer
                m_moveInUse = true;
            }
        }
        // Check to see if this pointer is in the fire control.
        else if (position.x > m_fireUpperLeft.x &&
            position.x < m_fireLowerRight.x &&
            position.y > m_fireUpperLeft.y &&
            position.y < m_fireLowerRight.y)
        {
            if (!m_fireInUse)
            {
                // Save the location of the initial contact
                m_fireLastPoint = position;
                // Store the pointer using this control
                m_firePointerID = pointerID;
                // Set InUse flag to signal there is an active fire pointer
                m_fireInUse = true;
                ...
            }
        }
        ...

ユーザーが移動コントロールに触れているか、ファイア コントロールに触れているかを判断したので、次にプレイヤーの押した指が動いているかどうかを確認します。 MoveLookController::OnPointerMoved メソッドを使用して、移動したポインターを確認し、その新しい位置を 2D ベクトルとして格納します。

PointerPoint point = args.CurrentPoint();
uint32_t pointerID = point.PointerId();
Point pointerPosition = point.Position();
PointerPointProperties pointProperties = point.Properties();
auto pointerDevice = point.PointerDevice();

// convert to allow math
XMFLOAT2 position = XMFLOAT2(pointerPosition.X, pointerPosition.Y);

switch (m_state)
{
case MoveLookControllerState::Active:
    // Decide which control this pointer is operating.

    // Move control
    if (pointerID == m_movePointerID)
    {
        // Save the current position.
        m_movePointerPosition = position;
    }
    // Look control
    else if (pointerID == m_lookPointerID)
    {
        ...
    }
    // Fire control
    else if (pointerID == m_firePointerID)
    {
        m_fireLastPoint = position;
    }
    ...

ユーザーがコントロール内でジェスチャを行うと、ポインターが解放されます。 MoveLookController::OnPointerReleased メソッドを使用して、解放されたポインターを特定し、一連のリセットを実行します。

移動コントロールが解放されている場合は、次の操作を行います。

  • プレイヤーのすべての方向の速度を 0 に設定して、ゲーム内を移動しないようにします。
  • ユーザーはもうムーブ コントローラーにタッチしていないため、m_moveInUsefalse に切り替えます。
  • 移動コントローラーにポインターが存在しなくなったため、移動ポインター ID を 0 に設定します。
if (pointerID == m_movePointerID)
{
    // Stop on release.
    m_velocity = XMFLOAT3(0, 0, 0);
    m_moveInUse = false;
    m_movePointerID = 0;
}

ファイア コントロールの場合、m_fireInUse フラグを false に切り替える必要があります。また、ファイア コントロールにポインターが存在しなくなったため、ファイア ポインター ID を 0 に切り替える必要があります。

else if (pointerID == m_firePointerID)
{
    m_fireInUse = false;
    m_firePointerID = 0;
}

ルック コントローラー

画面の未使用領域のタッチ デバイス ポインター イベントは、ルック コントローラーとして扱います。 このゾーンの周りで指をスライドさせると、プレーヤー カメラのピッチとヨー (回転) が変化します。

MoveLookController::OnPointerPressed イベントがこの領域内のタッチ デバイスで発生し、ゲームの状態がアクティブに設定されている場合は、ポインター ID が割り当てられます。

// If no pointer is in this control yet.
if (!m_lookInUse)
{
    // Save point for later move.
    m_lookLastPoint = position;
    // Store the pointer using this control.
    m_lookPointerID = pointerID;
    // These are for smoothing.
    m_lookLastDelta.x = m_lookLastDelta.y = 0;
    m_lookInUse = true;
}

ここで、MoveLookController は、イベントを発生したポインターのポインター ID を、ルック領域に対応する特定の変数に割り当てます。 ルック領域内でタッチが発生した場合、m_lookPointerID 変数は、そのイベントを発生させたポインターの ID に設定されます。 ブール変数 m_lookInUse も、コントロールがまだ離されていないことを示すために設定されます。

次は、このサンプル ゲームで PointerMoved タッチ スクリーン イベントを処理する方法を見てみましょう。

MoveLookController::OnPointerMoved メソッド内で、イベントに割り当てられているポインター ID の種類を確認します。 m_lookPointerID の場合は、ポインターの位置の変化を計算します。 次に、このデルタを使用して、回転の変更量を計算します。 最後に、プレイヤーの回転を変更するためにゲームで使用される m_pitchm_yaw を更新します。

// This is the look pointer.
else if (pointerID == m_lookPointerID)
{
    // Look control.
    XMFLOAT2 pointerDelta;
    // How far did the pointer move?
    pointerDelta.x = position.x - m_lookLastPoint.x;
    pointerDelta.y = position.y - m_lookLastPoint.y;

    XMFLOAT2 rotationDelta;
    // Scale for control sensitivity.
    rotationDelta.x = pointerDelta.x * MoveLookConstants::RotationGain;
    rotationDelta.y = pointerDelta.y * MoveLookConstants::RotationGain;
    // Save for next time through.
    m_lookLastPoint = position;

    // Update our orientation based on the command.
    m_pitch -= rotationDelta.y;
    m_yaw += rotationDelta.x;

    // Limit pitch to straight up or straight down.
    float limit = XM_PI / 2.0f - 0.01f;
    m_pitch = __max(-limit, m_pitch);
    m_pitch = __min(+limit, m_pitch);
    ...
}

確認する最後の要素は、このサンプル ゲームで PointerReleased タッチ スクリーン イベントがどのように処理されているかです。 ユーザーがタッチ ジェスチャを完了し、画面から指を離すと、MoveLookController::OnPointerReleased が開始されます。 PointerReleased イベントを発生させたポインターの ID が以前に記録された移動ポインターの ID である場合、MoveLookController は、プレイヤーがルックエリアに触れるのを停止したために速度を 0 に設定します。

else if (pointerID == m_lookPointerID)
{
    m_lookInUse = false;
    m_lookPointerID = 0;
}

マウスとキーボードのサポートの追加

このゲームには、キーボードとマウス用の次のコントロール レイアウトがあります。

ユーザー入力 アクション
W プレイヤーを前に移動する
A プレイヤーを左に移動する
S プレイヤーを後ろに移動する
D プレイヤーを右に移動する
x ビューを上に移動する
スペース バー ビューを下に移動する
P ゲームを一時停止する
マウスの移動 カメラ ビューの回転 (ピッチとヨー) を変更する
マウスの左ボタンを放す 球を発射する

キーボードを使用するために、サンプル ゲームは、MoveLookController::InitWindow メソッド内で 2 つの新しいイベント CoreWindow::KeyUpCoreWindow::KeyDown を登録します。 これらのイベントは、キーの押下と解放を処理します。

window.KeyDown({ this, &MoveLookController::OnKeyDown });

window.KeyUp({ this, &MoveLookController::OnKeyUp });

マウスはポインターを使用する場合でも、タッチ コントロールとは少し異なる方法で処理されます。 コントロール レイアウトに合わせて、MoveLookController はマウスを移動するたびにカメラを回転させ、マウスの左ボタンを押すと発射します。

これは、MoveLookControllerOnPointerPressed メソッドで処理されます。

このメソッドでは、Windows::Devices::Input::PointerDeviceType 列挙型で使用されているポインター デバイスの種類を確認します。 ゲームがアクティブで、PointerDeviceTypeTouch でない場合は、マウス入力であると想定します。

case MoveLookControllerState::Active:
    switch (pointerDeviceType)
    {
    case winrt::Windows::Devices::Input::PointerDeviceType::Touch:
        // Behavior for touch controls
        ...

    default:
        // Behavior for mouse controls
        bool rightButton = pointProperties.IsRightButtonPressed();
        bool leftButton = pointProperties.IsLeftButtonPressed();

        if (!m_autoFire && (!m_mouseLeftInUse && leftButton))
        {
            m_firePressed = true;
        }

        if (!m_mouseInUse)
        {
            m_mouseInUse = true;
            m_mouseLastPoint = position;
            m_mousePointerID = pointerID;
            m_mouseLeftInUse = leftButton;
            m_mouseRightInUse = rightButton;
            // These are for smoothing.
            m_lookLastDelta.x = m_lookLastDelta.y = 0;
        }
        break;
    }
    break;

プレイヤーがいずれのマウス ボタンも押さなくなると、CoreWindow::P ointerReleased マウス イベントが発生し、MoveLookController::OnPointerReleased メソッドが呼び出され、入力が完了します。 この時点で、押されていたマウスの左ボタンが、現在離されている場合、球は発射を停止します。 視線は常に有効になっているため、ゲームは同じマウス ポインターを使用して、進行中のルック イベントを追跡し続けます。

case MoveLookControllerState::Active:
    // Touch points
    if (pointerID == m_movePointerID)
    {
        // Stop movement
        ...
    }
    else if (pointerID == m_lookPointerID)
    {
        // Stop look rotation
        ...
    }
    // Fire button has been released
    else if (pointerID == m_firePointerID)
    {
        // Stop firing
        ...
    }
    // Mouse point
    else if (pointerID == m_mousePointerID)
    {
        bool rightButton = pointProperties.IsRightButtonPressed();
        bool leftButton = pointProperties.IsLeftButtonPressed();

        // Mouse no longer in use so stop firing
        m_mouseInUse = false;

        // Don't clear the mouse pointer ID so that Move events still result in Look changes.
        // m_mousePointerID = 0;
        m_mouseLeftInUse = leftButton;
        m_mouseRightInUse = rightButton;
    }
    break;

次に、サポートする最後のコントロールの種類 (ゲームパッド) を見てみましょう。 ゲームパッドはポインター オブジェクトを使用しないため、タッチ コントロールとマウス コントロールとは別に処理されます。 このため、いくつかの新しいイベント ハンドラーとメソッドを追加する必要があります。

ゲームパッドのサポートの追加

このゲームでは、Windows.Gaming.Input API の呼び出しによってゲームパッドのサポートが追加されます。 この一連の API は、レーシング ホイールやフライト スティックなどのゲーム コントローラー入力へのアクセスを提供します。

ゲームパッド コントロールには次があります。

ユーザー入力 アクション
左アナログ スティック プレイヤーを移動する
右アナログ スティック カメラ ビューの回転 (ピッチとヨー) を変更する
右トリガー 球を発射する
[スタート]/[メニュー] ボタン ゲームを一時停止または再開する

InitWindow メソッドでは、2 つの新しいイベントを追加して、ゲームパッドが追加または削除されたかどうかを判断します。 これらのイベントは、m_gamepadsChanged プロパティを更新します。 これは、既知のゲームパッドの一覧が変更されたかどうかを確認するために、UpdatePollingDevices メソッドで使用されます。

// Detect gamepad connection and disconnection events.
Gamepad::GamepadAdded({ this, &MoveLookController::OnGamepadAdded });

Gamepad::GamepadRemoved({ this, &MoveLookController::OnGamepadRemoved });

Note

アプリがフォーカスされていない間、UWP アプリはゲーム コントローラーから入力を受け取ることができません。

UpdatePollingDevices メソッド

MoveLookController インスタンスの UpdatePollingDevices メソッドは、ゲームパッドが接続されているかどうかをただちに確認します。 接続されていれば、Gamepad.GetCurrentReading でその状態の読み取りを開始します。 これにより GamepadReading 構造体が返され、クリックされたボタンや、サムスティックが移動されたボタンを確認できます。

ゲームの状態が WaitForInput の場合、ゲームを再開できるようコントローラーの [スタート]/[メニュー] ボタンのみをリッスンします。

アクティブ状態の場合は、ユーザーの入力を確認し、ゲーム内アクションを実行する必要があるかどうかを判断します。 たとえば、ユーザーが左アナログ スティックを特定の方向に移動した場合、これにより、ゲームではスティックが移動されている方向にプレイヤーを移動する必要があることがわかります。 特定の方向のスティックの動きは、デッド ゾーンの半径よりも大きく登録する必要があります。それ以外の場合は、何も起こりません。 このデッドゾーン半径は、コントローラーがスティック上に置かれるときにプレイヤーの親指から小さな動きを拾う "ドリフト" を防ぐために必要です。 デッド ゾーンなしでは、ユーザーに対してコントロールが敏感すぎるように見える可能性があります。

サムスティック入力は、x 軸と y 軸の両方に対して -1 から 1 の間です。 次の子音は、サムスティックのデッド ゾーン半径を指定します。

#define THUMBSTICK_DEADZONE 0.25f

この変数を使用して、操作可能なサムスティック入力の処理を開始します。 移動は、いずれかの軸で [-1, -0.26] または [0.26, 1] の値で発生しています。

dead zone for thumbsticks

UpdatePollingDevices メソッドのこの部分は、左と右のサムスティックを処理します。 各スティックの X と Y の値が、デッド ゾーンの外にあるかどうかを確認します。 一方または両方がデッド ゾーンの外にある場合は、対応するコンポーネントを更新します。 たとえば、左サムスティックが X 軸に沿って左に移動されている場合は、m_moveCommand ベクトルの x コンポーネントに -1 を追加します。 このベクトルは、すべてのデバイス間のすべての動きを集計するために使用され、後でプレイヤーが移動する場所を計算するために使用されます。

// Use the left thumbstick to control the eye point position
// (position of the player).

// Check if left thumbstick is outside of dead zone on x axis
if (reading.LeftThumbstickX > THUMBSTICK_DEADZONE ||
    reading.LeftThumbstickX < -THUMBSTICK_DEADZONE)
{
    // Get value of left thumbstick's position on x axis
    float x = static_cast<float>(reading.LeftThumbstickX);
    // Set the x of the move vector to 1 if the stick is being moved right.
    // Set to -1 if moved left. 
    m_moveCommand.x -= (x > 0) ? 1 : -1;
}

// Check if left thumbstick is outside of dead zone on y axis
if (reading.LeftThumbstickY > THUMBSTICK_DEADZONE ||
    reading.LeftThumbstickY < -THUMBSTICK_DEADZONE)
{
    // Get value of left thumbstick's position on y axis
    float y = static_cast<float>(reading.LeftThumbstickY);
    // Set the y of the move vector to 1 if the stick is being moved forward.
    // Set to -1 if moved backwards.
    m_moveCommand.y += (y > 0) ? 1 : -1;
}

左スティックが動きを制御するのと同様に、右スティックはカメラの回転を制御します。

右サム スティックの動作は、マウスおよびキーボード コントロールの構成でのマウスの動作に合わせて調整されます。 スティックがデッド ゾーンの外にある場合は、現在のポインターの位置と、ユーザーが現在見ようとしている場所の差を計算します。 このポインター位置の変化 (pointerDelta) は、後で Update メソッドで適用されるカメラの回転のピッチとヨーを更新するために使用されます。 pointerDelta ベクトルは、マウス入力とタッチ入力のポインター位置の変化を追跡するために MoveLookController::OnPointerMoved メソッドでも使用されるため、見慣れているかもしれません。

// Use the right thumbstick to control the look at position

XMFLOAT2 pointerDelta;

// Check if right thumbstick is outside of deadzone on x axis
if (reading.RightThumbstickX > THUMBSTICK_DEADZONE ||
    reading.RightThumbstickX < -THUMBSTICK_DEADZONE)
{
    float x = static_cast<float>(reading.RightThumbstickX);
    // Register the change in the pointer along the x axis
    pointerDelta.x = x * x * x;
}
// No actionable thumbstick movement. Register no change in pointer.
else
{
    pointerDelta.x = 0.0f;
}
// Check if right thumbstick is outside of deadzone on y axis
if (reading.RightThumbstickY > THUMBSTICK_DEADZONE ||
    reading.RightThumbstickY < -THUMBSTICK_DEADZONE)
{
    float y = static_cast<float>(reading.RightThumbstickY);
    // Register the change in the pointer along the y axis
    pointerDelta.y = y * y * y;
}
else
{
    pointerDelta.y = 0.0f;
}

XMFLOAT2 rotationDelta;
// Scale for control sensitivity.
rotationDelta.x = pointerDelta.x * 0.08f;
rotationDelta.y = pointerDelta.y * 0.08f;

// Update our orientation based on the command.
m_pitch += rotationDelta.y;
m_yaw += rotationDelta.x;

// Limit pitch to straight up or straight down.
m_pitch = __max(-XM_PI / 2.0f, m_pitch);
m_pitch = __min(+XM_PI / 2.0f, m_pitch);

ゲームのコントロールは、球を発射する機能がなければ完全ではないでしょう!

この UpdatePollingDevices メソッドは、適切なトリガーが押下されているかどうかを確認します。 押下されている場合、m_firePressed プロパティは true に反転し、球の発射を開始する必要があることをゲームに通知します。

if (reading.RightTrigger > TRIGGER_DEADZONE)
{
    if (!m_autoFire && !m_gamepadTriggerInUse)
    {
        m_firePressed = true;
    }

    m_gamepadTriggerInUse = true;
}
else
{
    m_gamepadTriggerInUse = false;
}

Update メソッド

物事をまとめるために、Update メソッドについて詳しく調べてみましょう。 このメソッドは、プレイヤーが行った動きや回転を、サポートされている任意の入力とマージして速度ベクトルを生成し、ゲーム ループがアクセスできるようにピッチとヨーの値を更新します。

Update メソッドは、UpdatePollingDevices を呼び出してコントローラーの状態を更新することで、処理を開始します。 また、このメソッドはゲームパッドから入力を収集し、その動きを m_moveCommand ベクトルに追加します。

Update メソッドでは、次の入力チェックを実行します。

  • プレイヤーが移動コントローラーの矩形を使用している場合は、ポインターの位置の変化を判断し、それを使用して、ユーザーがコントローラーのデッド ゾーンからポインターを移動したかどうかを計算します。 デッド ゾーンの外にある場合、m_moveCommand ベクトル プロパティは仮想ジョイスティック値で更新されます。
  • 移動のキーボード入力のいずれかが押されている場合、m_moveCommand ベクトルの対応するコンポーネントに 1.0f または -1.0f の値が追加されます。1.0f は前進、-1.0f は後退です。

すべての動きの入力が考慮されたら、いくつかの計算を通じて m_moveCommand ベクトルを実行して、ゲームの世界に関するプレイヤーの方向を表す新しいベクトルを生成します。 そして、世界との関係の中で動きをとらえ、その方向の速度としてプレイヤーに適用します。 最後に、m_moveCommand ベクトルを (0.0f, 0.0f, 0.0f) にリセットして、すべてが次のゲーム フレームに対応できるようにします。

void MoveLookController::Update()
{
    // Get any gamepad input and update state
    UpdatePollingDevices();

    if (m_moveInUse)
    {
        // Move control.
        XMFLOAT2 pointerDelta;

        pointerDelta.x = m_movePointerPosition.x - m_moveFirstDown.x;
        pointerDelta.y = m_movePointerPosition.y - m_moveFirstDown.y;

        // Figure out the command from the virtual joystick.
        XMFLOAT3 commandDirection = XMFLOAT3(0.0f, 0.0f, 0.0f);
        // Leave 32 pixel-wide dead spot for being still.
        if (fabsf(pointerDelta.x) > 16.0f)
            m_moveCommand.x -= pointerDelta.x / fabsf(pointerDelta.x);

        if (fabsf(pointerDelta.y) > 16.0f)
            m_moveCommand.y -= pointerDelta.y / fabsf(pointerDelta.y);
    }

    // Poll our state bits set by the keyboard input events.
    if (m_forward)
    {
        m_moveCommand.y += 1.0f;
    }
    if (m_back)
    {
        m_moveCommand.y -= 1.0f;
    }
    if (m_left)
    {
        m_moveCommand.x += 1.0f;
    }
    if (m_right)
    {
        m_moveCommand.x -= 1.0f;
    }
    if (m_up)
    {
        m_moveCommand.z += 1.0f;
    }
    if (m_down)
    {
        m_moveCommand.z -= 1.0f;
    }

    // Make sure that 45deg cases are not faster.
    if (fabsf(m_moveCommand.x) > 0.1f ||
        fabsf(m_moveCommand.y) > 0.1f ||
        fabsf(m_moveCommand.z) > 0.1f)
    {
        XMStoreFloat3(&m_moveCommand, XMVector3Normalize(XMLoadFloat3(&m_moveCommand)));
    }

    // Rotate command to align with our direction (world coordinates).
    XMFLOAT3 wCommand;
    wCommand.x = m_moveCommand.x * cosf(m_yaw) - m_moveCommand.y * sinf(m_yaw);
    wCommand.y = m_moveCommand.x * sinf(m_yaw) + m_moveCommand.y * cosf(m_yaw);
    wCommand.z = m_moveCommand.z;

    // Scale for sensitivity adjustment.
    // Our velocity is based on the command. Y is up.
    m_velocity.x = -wCommand.x * MoveLookConstants::MovementGain;
    m_velocity.z = wCommand.y * MoveLookConstants::MovementGain;
    m_velocity.y = wCommand.z * MoveLookConstants::MovementGain;

    // Clear movement input accumulator for use during next frame.
    m_moveCommand = XMFLOAT3(0.0f, 0.0f, 0.0f);
}

次のステップ

コントロールを追加したので、次にイマーシブ ゲームを作成するために必要なもう 1 つの機能であるサウンドを追加します。 音楽や効果音はゲームにとって重要なので、次にサウンドを追加する方法について説明しましょう。