遊戲輸入實務

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

透過閱讀此主題,您將瞭解:

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

選擇輸入裝置類別

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

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

另一方面,對於複雜的飛行和賽車模擬,您可能需要列舉所有 RawGameController 物件做為基準,以確保它們支援狂熱玩家可能擁有的任何利基裝置,包括仍由單一玩家使用的獨立踏板或油門。

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

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

追蹤連線的控制器

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

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

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

另一個要考慮的是,連線控制器的清單一開始會是空的,並需要一兩秒才能填入。 所以如果您在啟動方法中只指派目前的遊戲台,它將會是 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 瀏覽控制器相關聯的方式。

由於這些原因,應追蹤玩家輸入並與裝置類別 (繼承自 IGameController 介面) 的 User 屬性相關聯。

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).
}

下列範例會判斷放開按鈕 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).
}

所有這五個範例的共同點是,要測試的按鈕的排列由等號運算子左側的表達式指定,而要考慮的按鈕則由右側的遮罩表達式選取。

下列範例會藉由重寫上一個範例,更清楚地示範此公式:

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

  • 您可以透過計算 RemainingCapacityInMilliwattHours / FullChargeCapacityInMilliwattHours 來取得電池百分比。 您應該忽略這些屬性的值,並只處理計算的百分比。

  • 上一個項目符號點的百分比一律為下列其中一項:

    • 100% (充滿電)
    • 70% (中等)
    • 40% (低)
    • 10% (危急)

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

另請參閱