共用方式為


遊戲輸入方式

本主題描述在通用 Windows 平臺 (UWP) 遊戲中有效使用輸入裝置的模式和技術。

閱讀本主題後,您將瞭解:

  • 如何追蹤玩家及其目前使用的輸入和瀏覽裝置
  • 如何偵測按鈕轉換(按下至放開、放開至按下)
  • 如何使用單一測試來偵測複雜的按鈕排列方式

選擇輸入裝置類別

有許多不同類型的輸入 API 可供您使用,例如 ArcadeStickFlightStickGamepad。 如何決定要用於遊戲的 API?

您應該選擇哪一個 API 為您提供最適合遊戲的輸入。 例如,如果您要製作 2D 平台遊戲,可能只需要使用 Gamepad 類別,而不是使用其他類別提供的額外功能。 這會將遊戲限制為僅支援遊戲板,並提供一致的介面,可在許多不同的遊戲板上運作,而不需要額外的程序代碼。

另一方面,針對複雜的飛行和賽車模擬,您可能需要列出所有 RawGameController 物件作為基準,以確保它們支援愛好者玩家可能擁有的任何特殊裝置,例如單一玩家使用的獨立踏板或油門等裝置。

您可以從該處使用輸入類別的 FromGameController 方法,例如 Gamepad.FromGameController,來查看每個裝置是否有更精細的檢視。 例如,如果裝置也是 Gamepad,則您可能想要調整按鈕配置介面來反映這一點,並提供一些合理的預設按鈕對應以便選擇。 (這與要求玩家手動設定遊戲板輸入相反,如果您只使用 RawGameController

或者,您可以查看 RawGameController 的廠商ID (VID) 和產品ID (PID)(分別使用 HardwareVendorIdHardwareProductId),並為熱門裝置提供按鈕對應建議,同時以確保與未來透過玩家手動對應的未知裝置兼容。

追蹤已連接的控制器

雖然每個控制器類型都包含連線的控制器清單(例如 Gamepad.Gamepads),但維護您自己的控制器清單是個不錯的主意。 如需詳細資訊,請參閱 遊戲板清單 (每個控制器類型在其專屬主題上都有類似名稱的區段)。

不過,當玩家拔除控制器或插入新的控制器時,會發生什麼事? 您需要處理這些事件,並據以更新您的清單。 如需詳細資訊,請參閱 新增和移除遊戲板 (同樣地,每個控制器類型都有自己的主題有類似名稱的區段)。

因為新增和移除的事件會以異步方式引發,所以處理控制器清單時可能會得到不正確的結果。 因此,只要您存取控制器清單,就應該在它上加鎖,讓一次只能有一個執行緒存取它。 這可以使用 並行運行時間來完成,特別是 ppl.h中的 critical_section 類別

另一個要考慮的是,連線控制器的清單一開始會是空的,並需要一兩秒才能填入。 因此,如果您只在 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::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;
}

這兩個函式會從 newReadingoldReading中取得按鈕選擇的布爾狀態,然後執行布爾邏輯以判斷目標轉換是否已發生。 只有當新的讀取包含目標狀態(分別按下或放開) 舊的讀取不包含目標狀態時,這些函式才會傳 回 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)。

對於支援詳細電池報告的遊戲控制器,您可以取得電池的詳細資訊,如 取得電池資訊中所述。 不過,大部分的遊戲控制器都不支援電池報告等級,而是使用低成本的硬體。 針對這些控制器,您必須記住下列考慮:

  • ChargeRateInMilliwattsDesignCapacityInMilliwattHours 會始終是 NULL

  • 你可以通過計算 剩餘容量毫瓦時 / 完全充電容量毫瓦時來得到電池百分比。 您應該忽略這些屬性的值,並只處理計算的百分比。

  • 前一項的百分比一律為下列其中一項:

    • 100% (完整)
    • 70% (中等)
    • 40% (低)
    • 10% (重大)

如果您的程式代碼會根據剩餘的電池使用時間百分比執行某些動作(例如繪製 UI),請確定其符合上述值。 例如,如果您想要在控制器的電池不足時警告玩家,請在達到 10%時發出警告。

另請參閱