本主題描述在通用 Windows 平臺 (UWP) 遊戲中有效使用輸入裝置的模式和技術。
閱讀本主題後,您將瞭解:
- 如何追蹤玩家及其目前使用的輸入和瀏覽裝置
- 如何偵測按鈕轉換(按下至放開、放開至按下)
- 如何使用單一測試來偵測複雜的按鈕排列方式
選擇輸入裝置類別
有許多不同類型的輸入 API 可供您使用,例如 ArcadeStick、FlightStick和 Gamepad。 如何決定要用於遊戲的 API?
您應該選擇哪一個 API 為您提供最適合遊戲的輸入。 例如,如果您要製作 2D 平台遊戲,可能只需要使用 Gamepad 類別,而不是使用其他類別提供的額外功能。 這會將遊戲限制為僅支援遊戲板,並提供一致的介面,可在許多不同的遊戲板上運作,而不需要額外的程序代碼。
另一方面,針對複雜的飛行和賽車模擬,您可能需要列出所有 RawGameController 物件作為基準,以確保它們支援愛好者玩家可能擁有的任何特殊裝置,例如單一玩家使用的獨立踏板或油門等裝置。
您可以從該處使用輸入類別的 FromGameController 方法,例如 Gamepad.FromGameController,來查看每個裝置是否有更精細的檢視。 例如,如果裝置也是 Gamepad,則您可能想要調整按鈕配置介面來反映這一點,並提供一些合理的預設按鈕對應以便選擇。 (這與要求玩家手動設定遊戲板輸入相反,如果您只使用 RawGameController。
或者,您可以查看 RawGameController 的廠商ID (VID) 和產品ID (PID)(分別使用 HardwareVendorId 和 HardwareProductId),並為熱門裝置提供按鈕對應建議,同時以確保與未來透過玩家手動對應的未知裝置兼容。
追蹤已連接的控制器
雖然每個控制器類型都包含連線的控制器清單(例如 Gamepad.Gamepads),但維護您自己的控制器清單是個不錯的主意。 如需詳細資訊,請參閱 遊戲板清單 (每個控制器類型在其專屬主題上都有類似名稱的區段)。
不過,當玩家拔除控制器或插入新的控制器時,會發生什麼事? 您需要處理這些事件,並據以更新您的清單。 如需詳細資訊,請參閱 新增和移除遊戲板 (同樣地,每個控制器類型都有自己的主題有類似名稱的區段)。
因為新增和移除的事件會以異步方式引發,所以處理控制器清單時可能會得到不正確的結果。 因此,只要您存取控制器清單,就應該在它上加鎖,讓一次只能有一個執行緒存取它。 這可以使用
另一個要考慮的是,連線控制器的清單一開始會是空的,並需要一兩秒才能填入。 因此,如果您只在 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);
}
}
追蹤使用者及其裝置
所有輸入裝置都會與 使用者 相關聯,以便其身分識別可以連結到其遊戲、成就、設定變更和其他活動。 使用者可以隨意登入或登出,而且在上一個用戶登出之後,其他使用者通常會登入仍連線到系統的輸入裝置。當使用者登入或登出時,會引發 IGameController.UserChanged 事件。 您可以註冊此事件的事件處理程式,以追蹤玩家及其所使用的裝置。
使用者身份識別的方式也是將輸入裝置與其對應的 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;
}
這兩個函式會從 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).
}
如您所見,判斷單一按鈕的狀態很簡單,但有時您可能會想要判斷多個按鈕是否被按下或放開,或者組合中有些按鈕是按下而有些不是。 測試多個按鈕比測試單一按鈕更為複雜,尤其是在可能出現按鈕狀態混合的情況下。不過,對於單一及多個按鈕測試,都有一個適用的簡單公式。
下列範例會判斷是否按下遊戲板按鈕 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).
}
下列範例用於檢查當遊戲板按鈕 A 被按下的同時,按鈕 B 是否已鬆開:
if (GamepadButtons::A == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
// The A button is pressed and the B button is released (B is not pressed).
}
這五個範例通用的公式是,要測試的按鈕的排列方式是由等號運算子左邊的運算式所指定,而需要考慮的按鈕則是由右側遮罩式所選取。
下列範例會藉由重寫上一個範例,更清楚地示範此公式:
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。
你可以通過計算 剩餘容量毫瓦時 / 完全充電容量毫瓦時來得到電池百分比。 您應該忽略這些屬性的值,並只處理計算的百分比。
前一項的百分比一律為下列其中一項:
- 100% (完整)
- 70% (中等)
- 40% (低)
- 10% (重大)
如果您的程式代碼會根據剩餘的電池使用時間百分比執行某些動作(例如繪製 UI),請確定其符合上述值。 例如,如果您想要在控制器的電池不足時警告玩家,請在達到 10%時發出警告。