ゲームの入力プラクティス
このトピックでは、ユニバーサル Windows プラットフォーム (UWP) ゲームで入力デバイスを効果的に使用するためのパターンと手法について説明します。
ここでは、次の項目について紹介します。
- プレイヤーを追跡する方法と、現在使用している入力デバイスとナビゲーション デバイス
- ボタンの切り替えを検出する方法 (押された状態から離された状態、離された状態から押された状態)
- 1 つのテストで複雑なボタン配置を検出する方法
入力デバイス クラスの選択
ArcadeStick、FlightStick、Gamepad など、さまざまな種類の入力 API を使用できます。 ゲームに使用する API を決定するにはどうすればよいですか?
ゲームに最適な入力を提供する API を選択する必要があります。 たとえば、2D プラットフォーム ゲームを作成する場合は、おそらく Gamepad クラスを使用するだけで、他のクラスで使用できる追加機能を気にしないようにすることができます。 これにより、ゲームはゲームパッドのみをサポートするように制限され、追加のコードを必要とせず、多くの異なるゲームパッドで動作する一貫したインターフェイスが提供されます。
一方、複雑なフライトシミュレーションやレーシングシミュレーションでは、ベースラインとしてすべての RawGameController オブジェクトを列挙して、単一プレイヤーが引き続き使用する独立したペダルやスロットルなどのデバイスなど、愛好家が持つニッチデバイスを確実にサポートすることができます。
そこから、入力クラスの FromGameController メソッド ( Gamepad.FromGameController など) を使用して、各デバイスがよりキュレーションされたビューを持っているかどうかを確認できます。 たとえば、デバイスが Gamepad でもある場合は、それを反映するようにボタン マッピング UI を調整し、選択できる適切な既定のボタン マッピングを提供できます。 (これは、 のみを使用している場合、プレイヤーがゲームパッドの入力を手動で構成する必要があるのとは対照的です。RawGameController.)
または、 RawGameController のベンダー ID (VID) と製品 ID (PID) (それぞれ HardwareVendorId と HardwareProductId を使用) を確認し、一般的なデバイスに推奨されるボタン マッピングを提供しながら、プレイヤーによる手動マッピングを使用して将来提供される不明なデバイスとの互換性を残すことができます。
接続されているコントローラーを追跡する
各コントローラーの種類には接続されているコントローラーの一覧 ( Gamepad.Gamepads など) が含まれていますが、コントローラーの独自のリストを保持することをお勧めします。 詳細については、 ゲームパッドの一覧 を参照してください (各コントローラーの種類には、独自のトピックで同様の名前のセクションがあります)。
ただし、プレイヤーがコントローラーを取り外したり、新しいコントローラーをプラグインしたりするとどうなりますか? これらのイベントを処理し、それに応じてリストを更新する必要があります。 詳細については、「 ゲームパッドの追加と削除 を参照してください (ここでも、各コントローラーの種類には、独自のトピックで同様の名前のセクションがあります)。
追加および削除されたイベントは非同期的に発生するため、コントローラーの一覧を処理するときに正しくない結果が得られる可能性があります。 そのため、コントローラーの一覧にアクセスするたびに、一度にアクセスできるスレッドが 1 つだけになるようにロックを設定する必要があります。 これは、Concurrency Runtime(具体的には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);
}
}
ユーザーとそのデバイスの追跡
すべての入力デバイスが User に関連付けられているため、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::Loop
newReading
の既存の値 (前のループ イテレーションからのゲームパッドの読み取り) を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
し、次にブール値ロジックを実行して、ターゲットの遷移が発生したかどうかを判断します。 これらの関数はtrueを返します新しい読み取り値にターゲットの状態が含まれている場合にのみ (それぞれ押されるか、離されます)、古い読み取りにはターゲットの状態も含まれません。それ以外の場合は、falseを返します。
複雑なボタン配置の検出
入力デバイスの各ボタンは、デジタル読み取り値を提供します。デジタル読み取り値は、押された (下向き) か離されているか (上) かを示します。 効率を高める目的で、ボタンの読み取り値は個々のブール値として表されません。代わりに、これらはすべて、 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 つのボタンの状態を判断するのは簡単ですが、場合によっては、複数のボタンが押されたか離されたか、または一連のボタンの一部押されていて一部離されているような特定の方法で配置されているかどうかを判断したいことがあります。 複数のボタンを判断するのは 1 つのボタンを判断するより複雑です。特にボタンの状態が混在する可能性のあるときはなおさらです。ただし、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) などのプロパティを取得できます。
詳細なバッテリー レポートをサポートするゲーム コントローラーの場合は、「 Get battery information」で詳しく説明されているように、バッテリーに関する詳細情報を取得できます。 ただし、ほとんどのゲーム コントローラーでは、そのレベルのバッテリー レポートはサポートされておらず、代わりに低コストのハードウェアを使用します。 これらのコントローラーの場合は、次の考慮事項に留意する必要があります。
ChargeRateInMilliwatts および DesignCapacityInMilliwattHours は常に NULL になります。
バッテリーの割合を取得するには、 RemainingCapacityInMilliwattHours / FullChargeCapacityInMilliwattHours を計算します。 これらのプロパティの値は無視し、計算された割合のみを処理する必要があります。
前の箇条書きのパーセンテージは、常に次のいずれかになります。
- 100% (完全)
- 70% (中)
- 40% (低)
- 10% (クリティカル)
コードがバッテリー残量の割合に基づいて何らかのアクション (UI の描画など) を実行する場合は、上記の値に準拠していることを確認します。 たとえば、コントローラーのバッテリー残量が少ないときにプレイヤーに警告する場合は、10% に達したときに警告します。