このトピックでは、ユニバーサル Windows プラットフォーム (UWP) ゲームで入力デバイスを効果的に使用するためのパターンと手法について説明します。
このトピックを読むと、次の内容を学習できます。
- プレイヤーを追跡する方法と、現在使用している入力デバイスとナビゲーション デバイス
- ボタンの状態変化を検出する方法 (押下された状態から離された状態への変化、離された状態から押下された状態への変化)
- 1 つのテストで複雑なボタン配置を検出する方法
入力デバイス クラスの選択
ArcadeStick、FlightStick、Gamepadなど、さまざまな種類の入力 API を使用できます。 ゲームに使用する API を決定するにはどうすればよいですか?
ゲームに最適な入力を提供する API を選択する必要があります。 たとえば、2D プラットフォーム ゲームを作成する場合は、Gamepad クラスを使用するだけで、他のクラスで使用できる追加機能を気にしない可能性があります。 これにより、ゲームはゲームパッドのみをサポートするように制限され、追加のコードを必要とせず、多くの異なるゲームパッドで動作する一貫したインターフェイスが提供されます。
一方、複雑なフライトシミュレーションやレーシングシミュレーションでは、すべての RawGameController オブジェクトをベースラインとして列挙して、愛好家のプレイヤーが持つニッチデバイス (独立したペダルやスロットルなど、1 人のプレイヤーが引き続き使用するスロットルなど) を確実にサポートできるようにする必要があります。
そこから、入力クラスの FromGameController メソッド (Gamepad.FromGameControllerなど) を使用して、各デバイスがよりキュレーションされたビューを持っているかどうかを確認できます。 たとえば、デバイスが Gamepadでもある場合は、それを反映するようにボタン マッピング UI を調整し、選択できる適切な既定のボタン マッピングを提供できます。 (これは、RawGameControllerのみを使用している場合に、プレイヤーがゲームパッドの入力を手動で構成する必要があるのとは対照的です)。
または、RawGameController のベンダー ID (VID) と製品 ID (PID) (それぞれ HardwareVendorId と HardwareProductIdを使用) を確認し、一般的なデバイスに推奨されるボタン マッピングを提供しながら、プレイヤーによる手動マッピングを使用して将来出てくる不明なデバイスとの互換性を残すことができます。
接続されているコントローラーを追跡する
各コントローラーの種類には、接続されているコントローラーの一覧 (Gamepad.Gamepadsなど) が含まれていますが、コントローラーの独自のリストを維持することをお勧めします。 詳細については ゲームパッドの一覧 を参照してください (各コントローラーの種類には、独自のトピックで同様の名前のセクションがあります)。
ただし、プレイヤーがコントローラーを取り外したり、新しいコントローラーをプラグインしたりするとどうなりますか? これらのイベントを処理し、それに応じてリストを更新する必要があります。 詳細については、「ゲームパッド の追加と削除」を参照してください (ここでも、各コントローラーの種類には、独自のトピックで同様の名前のセクションがあります)。
追加および削除されたイベントは非同期的に発生するため、コントローラーの一覧を処理するときに正しくない結果が得られる可能性があります。 そのため、コントローラーの一覧にアクセスするたびに、一度にアクセスできるスレッドが 1 つだけになるようにロックを設定する必要があります。 これは、コンカレンシー ランタイム(具体的には、critical_section クラス) <ppl.h>で実行できます。
もう 1 つ考慮すべき点は、接続されているコントローラーの一覧が最初は空になり、設定に 1 秒または 2 秒かかることです。 したがって、start メソッドで現在のゲームパッドのみを割り当てると、nullになります。
これを修正するには、メイン ゲームパッドを "更新" するメソッドが必要です (シングルプレイヤー ゲームでは、マルチプレイヤー ゲームではより高度なソリューションが必要になります)。 その後、このメソッドは、追加されたコントローラーとコントローラーから削除されたイベント ハンドラー、または更新メソッドの両方で呼び出す必要があります。
次のメソッドは、リスト内の最初のゲームパッドを単に返します(リストが空の場合は nullptr を)。 その後、コントローラーで何かを行う場合は、いつでも nullptr を確認することを忘れないでください。 コントローラーが接続されていない場合 (たとえば、ゲームを一時停止するなど) にゲームプレイをブロックするか、入力を無視しながらゲームプレイを続行するかは、ユーザー次第です。
#include <ppl.h>
using namespace Platform::Collections;
using namespace Windows::Gaming::Input;
using namespace concurrency;
Vector<Gamepad^>^ m_myGamepads = ref new Vector<Gamepad^>();
Gamepad^ GetFirstGamepad()
{
Gamepad^ gamepad = nullptr;
critical_section::scoped_lock{ m_lock };
if (m_myGamepads->Size > 0)
{
gamepad = m_myGamepads->GetAt(0);
}
return gamepad;
}
すべてをまとめると、ゲームパッドからの入力を処理する方法の例を次に示します。
#include <algorithm>
#include <ppl.h>
using namespace Platform::Collections;
using namespace Windows::Foundation;
using namespace Windows::Gaming::Input;
using namespace concurrency;
static Vector<Gamepad^>^ m_myGamepads = ref new Vector<Gamepad^>();
static Gamepad^ m_gamepad = nullptr;
static critical_section m_lock{};
void Start()
{
// Register for gamepad added and removed events.
Gamepad::GamepadAdded += ref new EventHandler<Gamepad^>(&OnGamepadAdded);
Gamepad::GamepadRemoved += ref new EventHandler<Gamepad^>(&OnGamepadRemoved);
// Add connected gamepads to m_myGamepads.
for (auto gamepad : Gamepad::Gamepads)
{
OnGamepadAdded(nullptr, gamepad);
}
}
void Update()
{
// Update the current gamepad if necessary.
if (m_gamepad == nullptr)
{
auto gamepad = GetFirstGamepad();
if (m_gamepad != gamepad)
{
m_gamepad = gamepad;
}
}
if (m_gamepad != nullptr)
{
// Gather gamepad reading.
}
}
// Get the first gamepad in the list.
Gamepad^ GetFirstGamepad()
{
Gamepad^ gamepad = nullptr;
critical_section::scoped_lock{ m_lock };
if (m_myGamepads->Size > 0)
{
gamepad = m_myGamepads->GetAt(0);
}
return gamepad;
}
void OnGamepadAdded(Platform::Object^ sender, Gamepad^ args)
{
// Check if the just-added gamepad is already in m_myGamepads; if it isn't,
// add it.
critical_section::scoped_lock lock{ m_lock };
auto it = std::find(begin(m_myGamepads), end(m_myGamepads), args);
if (it == end(m_myGamepads))
{
m_myGamepads->Append(args);
}
}
void OnGamepadRemoved(Platform::Object^ sender, Gamepad^ args)
{
// Remove the gamepad that was just disconnected from m_myGamepads.
unsigned int indexRemoved;
critical_section::scoped_lock lock{ m_lock };
if (m_myGamepads->IndexOf(args, &indexRemoved))
{
if (m_gamepad == m_myGamepads->GetAt(indexRemoved))
{
m_gamepad = nullptr;
}
m_myGamepads->RemoveAt(indexRemoved);
}
}
ユーザーとそのデバイスの追跡
すべての入力デバイスは ユーザー に関連付けられているため、ID をゲームプレイ、実績、設定の変更、その他のアクティビティにリンクできます。 ユーザーは、サインインまたはサインアウトを行うことができます。別のユーザーが、前のユーザーがサインアウトした後もシステムに接続されたままの入力デバイスにサインインするのが一般的です。ユーザーがサインインまたはサインアウトすると、IGameController.UserChanged イベントが発生します。 このイベントのイベント ハンドラーを登録して、プレイヤーとプレイヤーが使用しているデバイスを追跡できます。
ユーザー ID は、入力デバイスが対応する UI ナビゲーション コントローラーに関連付けられる方法でもあります。
このような理由から、プレイヤーの入力を追跡し、デバイス クラスの User プロパティ (IGameController インターフェイスから継承) と関連付ける必要があります。
GitHub の UserGamepadPairingUWP サンプル アプリは、ユーザーとユーザーが使用しているデバイスを追跡する方法を示しています。
ボタンの遷移の検出
ボタンが最初に押されたり離されたりするタイミングを知りたい場合があります。それはつまり、ボタンの状態が正確に離された状態から押された状態に切り替わる瞬間、または押された状態から離された状態に切り替わる瞬間です。 これを判断するには、以前のデバイスの読み取り値を記憶し、現在の読み取りと比較して、何が変更されたかを確認する必要があります。
次の例は、前の読み取りを記憶するための基本的なアプローチを示しています。ここにはゲームパッドが示されていますが、アーケード スティック、レーシング ホイール、およびその他の入力デバイスの種類の原則は同じです。
Gamepad gamepad;
GamepadReading newReading();
GamepadReading oldReading();
// Called at the start of the game.
void Game::Start()
{
gamepad = Gamepad::Gamepads[0];
}
// Game::Loop represents one iteration of a typical game loop
void Game::Loop()
{
// move previous newReading into oldReading before getting next newReading
oldReading = newReading, newReading = gamepad.GetCurrentReading();
// process device readings using buttonJustPressed/buttonJustReleased (see below)
}
他の操作を行う前に、Game::LoopnewReading の既存の値 (前のループ イテレーションからのゲームパッドの読み取り) を oldReadingに移動し、現在のイテレーションの新しいゲームパッドの読み取り値で newReading を埋めます。 これにより、ボタンの遷移を検出するために必要な情報が提供されます。
次の例は、ボタンの遷移を検出するための基本的な方法を示しています。
bool ButtonJustPressed(const GamepadButtons selection)
{
bool newSelectionPressed = (selection == (newReading.Buttons & selection));
bool oldSelectionPressed = (selection == (oldReading.Buttons & selection));
return newSelectionPressed && !oldSelectionPressed;
}
bool ButtonJustReleased(GamepadButtons selection)
{
bool newSelectionReleased =
(GamepadButtons.None == (newReading.Buttons & selection));
bool oldSelectionReleased =
(GamepadButtons.None == (oldReading.Buttons & selection));
return newSelectionReleased && !oldSelectionReleased;
}
これら 2 つの関数は、最初にボタンの選択のブール状態を newReading から派生させ、oldReadingし、次にブール値ロジックを実行して、ターゲット遷移が発生したかどうかを判断します。 これらの関数は
複雑なボタン配置の検出
入力デバイスの各ボタンは、押された状態 (押下) か離された状態 (開放) かを示すデジタル信号を出力します。 効率を高める目的で、ボタンの読み取り値は個々のブール値として表されません。代わりに、これらはすべて、GamepadButtonsなどのデバイス固有の列挙で表されるビットフィールドにパックされます。 特定のボタンを読み取るために、ビットごとのマスクを使用して、関心のある値を分離します。 対応するビットが設定されると、ボタンが押されます (下)。それ以外の場合は、リリース (アップ) されます。
単一のボタンが押されたり離されたりすると判断される方法を思い出してください。ここにはゲームパッドが示されていますが、アーケード スティック、レーシング ホイール、およびその他の入力デバイスの種類の原則は同じです。
GamepadReading reading = gamepad.GetCurrentReading();
// Determines whether gamepad button A is pressed.
if (GamepadButtons::A == (reading.Buttons & GamepadButtons::A))
{
// The A button is pressed.
}
// Determines whether gamepad button A is released.
if (GamepadButtons::None == (reading.Buttons & GamepadButtons::A))
{
// The A button is released (not pressed).
}
ご覧のように、1 つのボタンの状態を判断するのは簡単ですが、複数のボタンが押されているか離されているか、または一連のボタンが特定の方法で配置されているか (一部は押されていないか) を判断することが必要な場合があります。 複数のボタンのテストは、単一のボタンをテストするよりも複雑です 。特に、ボタンの状態が混在する可能性がありますが、これらのテストには、単一と複数のボタンのテストに同様に適用される簡単な数式があります。
次の例では、ゲームパッドのボタン A と B の両方が押されているかどうかを判断します。
if ((GamepadButtons::A | GamepadButtons::B) == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
// The A and B buttons are both pressed.
}
次の例では、ゲームパッド のボタン A と B の両方がリリースされているかどうかを確認します。
if ((GamepadButtons::None == (reading.Buttons & GamepadButtons::A | GamepadButtons::B))
{
// The A and B buttons are both released (not pressed).
}
次の例では、ボタン B が離されている間にゲームパッド のボタン A を押すかどうかを決定します。
if (GamepadButtons::A == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
// The A button is pressed and the B button is released (B is not pressed).
}
これらの 5 つの例すべてに共通する数式は、テスト対象のボタンの配置が等値演算子の左側の式で指定されているのに対し、考慮するボタンは右側のマスク式によって選択されるということです。
次の例では、前の例を書き換えることで、この数式をより明確に示します。
auto buttonArrangement = GamepadButtons::A;
auto buttonSelection = (reading.Buttons & (GamepadButtons::A | GamepadButtons::B));
if (buttonArrangement == buttonSelection)
{
// The A button is pressed and the B button is released (B is not pressed).
}
この数式は、任意の数のボタンを状態の任意の配置でテストするために適用できます。
バッテリーの状態を取得する
IGameControllerBatteryInfo インターフェイスを実装するゲーム コントローラーの場合は、コントローラー インスタンス TryGetBatteryReport を呼び出して、コントローラー内のバッテリに関する情報を提供する BatteryReport オブジェクトを取得できます。 バッテリの充電速度 (ChargeRateInMilliwatts)、新しいバッテリの推定エネルギー容量 (DesignCapacityInMilliwattHours)、および現在のバッテリの完全に充電されたエネルギー容量 (FullChargeCapacityInMilliwattHours) などのプロパティを取得できます。
詳細なバッテリーレポートをサポートするゲームコントローラーの場合、「バッテリー情報を取得する」で詳しく説明されているように、バッテリーに関する詳細な情報取得できます。 ただし、ほとんどのゲーム コントローラーでは、そのレベルのバッテリー レポートはサポートされておらず、代わりに低コストのハードウェアを使用します。 これらのコントローラーの場合は、次の考慮事項に留意する必要があります。
ChargeRateInMilliwatts とDesignCapacityInMilliwattHours は常に NULLされます。 バッテリーの割合を取得するには、RemainingCapacityInMilliwattHours
FullChargeCapacityInMilliwattHours を計算します。 これらのプロパティの値は無視し、計算された割合のみを処理する必要があります。 前の箇条書きのパーセンテージは、常に次のいずれかになります。
- 100% (完全)
- 70% (中)
- 40% (低)
- 10% (クリティカル)
コードがバッテリー残量の割合に基づいて何らかのアクション (UI の描画など) を実行する場合は、上記の値に準拠していることを確認します。 たとえば、コントローラーのバッテリー残量が少ないときにプレイヤーに警告する場合は、10%に達したときに警告します。