Freigeben über


Eingabemethoden für Spiele

In diesem Thema werden Muster und Techniken für die effektive Verwendung von Eingabegeräten in Universelle Windows-Plattform-Spielen (UWP) beschrieben.

Wenn Sie dieses Thema lesen, erfahren Sie Folgendes:

  • Nachverfolgen von Spielern und den von ihnen aktuell verwendeten Eingabe- und Navigationsgeräten
  • Erkennen von Tastenübergängen (von „Gedrückt“ zu „Losgelassen“ oder von „Losgelassen“ zu „Gedrückt“)
  • Erkennen komplexer Tastenanordnungen mit einem einzelnen Test

Auswählen einer Eingabegeräteklasse

Es gibt viele verschiedene Arten von Eingabe-APIs, z. B. ArcadeStick, FlightStick und Gamepad. Wie entscheiden Sie, welche API für Ihr Spiel verwendet werden soll?

Sie sollten auswählen, welche API Ihnen die am besten geeignete Eingabe für Ihr Spiel bietet. Wenn Sie beispielsweise ein 2D-Plattformspiel erstellen, können Sie wahrscheinlich einfach die Gamepad-Klasse verwenden und sich nicht mit den zusätzlichen Funktionen, die über andere Klassen verfügbar sind, stören. Dies würde das Spiel auf die Unterstützung von Gamepads beschränken und eine konsistente Schnittstelle bereitstellen, die auf vielen verschiedenen Gamepads funktioniert, ohne dass zusätzlicher Code erforderlich ist.

Andererseits sollten Sie bei komplexen Flug- und Rennsimulationen alle RawGameController-Objekte als Basislinie aufzählen, um sicherzustellen, dass sie alle Nischengeräte unterstützen, die enthusiastische Spieler möglicherweise haben, einschließlich Geräten wie separaten Pedalen oder Drosselungen, die noch von einem einzelnen Spieler verwendet werden.

Von dort aus können Sie die FromGameController-Methode einer Eingabeklasse verwenden, z . B. Gamepad.FromGameController, um festzustellen, ob jedes Gerät über eine zusammengestellte Ansicht verfügt. Wenn das Gerät beispielsweise auch ein Gamepad ist, sollten Sie die Schaltflächenzuordnungs-Ui anpassen, um dies widerzuspiegeln und einige sinnvolle Standardschaltflächenzuordnungen zur Auswahl bereitzustellen. (Dies steht im Gegensatz dazu, dass der Spieler die Gamepadeingaben manuell konfigurieren muss, wenn Sie nur RawGameController verwenden.)

Alternativ können Sie sich die Anbieter-ID (VID) und die Produkt-ID (PID) eines RawGameControllers (mit HardwareVendorId bzw . HardwareProductId) ansehen und vorgeschlagene Schaltflächenzuordnungen für beliebte Geräte bereitstellen, während sie weiterhin mit unbekannten Geräten kompatibel bleiben, die in Zukunft über manuelle Zuordnungen durch den Spieler herauskommen.

Nachverfolgen verbundener Controller

Jeder Controllertyp enthält zwar eine Liste verbundener Controller (z . B. Gamepad.Gamepads), aber es empfiehlt sich, eine eigene Liste von Controllern zu verwalten. Weitere Informationen finden Sie unter Die Liste der Gamepads (jeder Controllertyp hat einen ähnlich benannten Abschnitt zu seinem eigenen Thema).

Was passiert jedoch, wenn der Spieler seinen Controller aussteckt oder einen neuen einsteckt? Sie müssen diese Ereignisse behandeln und Ihre Liste entsprechend aktualisieren. Weitere Informationen finden Sie unter Hinzufügen und Entfernen von Gamepads (auch hier hat jeder Controllertyp einen ähnlich benannten Abschnitt zu seinem eigenen Thema).

Da die hinzugefügten und entfernten Ereignisse asynchron ausgelöst werden, erhalten Sie möglicherweise falsche Ergebnisse, wenn Sie sich mit der Liste der Controller befassen. Daher sollten Sie bei jedem Zugriff auf die Liste der Controller eine Sperre um sie herum platzieren, sodass jeweils nur ein Thread darauf zugreifen kann. Dies kann mit der Concurrency Runtime, insbesondere der critical_section-Klasse, in <ppl.h> erfolgen.

Eine andere Sache, die zu berücksichtigen ist, ist, dass die Liste der verbundenen Controller zunächst leer ist und ein oder zwei Sekunden benötigt, um aufzufüllen. Wenn Sie also nur das aktuelle Gamepad in der Startmethode zuweisen, ist es NULL!

Um dies zu korrigieren, sollten Sie über eine Methode verfügen, die das Standard Gamepad "aktualisiert" (in einem Einzelspielerspiel; Multiplayer-Spiele erfordern komplexere Lösungen). Sie sollten diese Methode dann sowohl in den Ereignishandlern des hinzugefügten Controllers als auch in den entfernten Ereignishandlern oder in der Updatemethode aufrufen.

Die folgende Methode gibt einfach den ersten Gamepad in der Liste zurück (oder nullptr , wenn die Liste leer ist). Dann müssen Sie nur daran denken, jedes Mal auf NULLptr zu überprüfen, wenn Sie etwas mit dem Controller tun. Es liegt an Ihnen, ob Sie das Gameplay blockieren möchten, wenn kein Controller verbunden ist (z. B. durch Anhalten des Spiels) oder einfach das Gameplay fortsetzen möchten, während Sie Eingaben ignorieren.

#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;
}

Hier ist ein Beispiel für die Verarbeitung von Eingaben aus einem 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);
    }
}

Nachverfolgen von Benutzern und ihren Geräten

Alle Eingabegeräte sind einem Benutzer zugeordnet, damit seine Identität mit seinem Spiel, seinen Erfolgen, geänderten Einstellungen und anderen Aktivitäten verknüpft werden kann. Benutzer können sich nach Bewilligen an- oder abmelden, und es ist üblich, dass sich ein anderer Benutzer bei einem Eingabegerät anmeldet, das mit dem System verbunden bleibt, nachdem sich der vorherige Benutzer abgemeldet hat. Wenn sich ein Benutzer an- oder abmeldet, wird das Ereignis IGameController.UserChanged ausgelöst. Sie können einen Ereignishandler für dieses Ereignis registrieren, um Spieler und die von ihnen verwendeten Geräte nachzuverfolgen.

Die Benutzeridentität ist auch die Art und Weise, wie ein Eingabegerät dem entsprechenden Benutzeroberflächennavigationscontroller zugeordnet wird.

Aus diesen Gründen sollte die Spielereingabe nachverfolgt und mit der User-Eigenschaft der Geräteklasse korreliert werden (geerbt von der IGameController-Schnittstelle ).

Die Beispiel-App UserGamepadPairingUWP auf GitHub veranschaulicht, wie Sie Benutzer und die von ihnen verwendeten Geräte nachverfolgen können.

Erkennen von Tastenübergängen

In einigen Fällen möchten Sie wissen, wann eine Taste zuerst gedrückt oder losgelassen wird, also genau, wann der Zustand der Taste von „Losgelassen“ in „Gedrückt“ oder von „Gedrückt“ in „Losgelassen“ übergegangen ist. Um dies zu ermitteln und herauszufinden, was sich geändert hat, müssen Sie den vorherigen Geräte-Ablesewert beibehalten und mit dem aktuellen Ablesewert vergleichen.

Im folgenden Beispiel wird ein grundlegender Ansatz für die Erinnerung an die vorherige Lektüre veranschaulicht. Gamepads werden hier gezeigt, aber die Prinzipien sind für Arcade-Stick, Rennlenkrad und die anderen Eingabegerätetypen identisch.

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

Als Erstes verschiebt Game::Loop den vorhandenen Wert von newReading (den Gamepad-Ablesewert aus der vorherigen Schleifeniteration) in oldReading und fügt dann einen neuen Gamepad-Ablesewert für die aktuelle Iteration in newReading ein. Mithilfe dieser Informationen können Sie den Übergang von Tasten erkennen.

Das folgende Beispiel veranschaulicht einen grundlegenden Ansatz zum Erkennen von Schaltflächenübergängen:

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;
}

Diese beiden Funktionen leiten zuerst den booleschen Zustand der Schaltflächenauswahl von und oldReadingabnewReading, führen dann boolesche Logik aus, um zu bestimmen, ob der Zielübergang stattgefunden hat. Diese Funktionen geben nur true zurück, wenn der neue Ablesewert den Zielzustand enthält („Gedrückt“ bzw. „Losgelassen“) und der alte Ablesewert nicht auch den Zielzustand enthält. Andernfalls geben sie false zurück.

Erkennen komplexer Tastenanordnungen

Jede Taste eines Eingabegeräts stellt einen digitalen Wert bereit, der angibt, ob es gedrückt (nach unten) oder losgelassen (nach oben) ist. Aus Effizienzgründen werden die Ablesewerte der Tasten nicht als einzelne boolesche Werte dargestellt, sondern in Bitfeldern zusammengefasst, die durch gerätespezifische Enumerationen wie GamepadButtons dargestellt werden. Zum Lesen bestimmter Tastenwerte wird eine bitweise Maskierung verwendet, um die für Sie relevanten Werte zu isolieren. Eine Schaltfläche wird gedrückt (nach unten), wenn das entsprechende Bit festgelegt ist. Andernfalls wird es freigegeben (up).

Erinnern Sie sich daran, wie einzelne Schaltflächen gedrückt oder losgelassen werden. Gamepads werden hier gezeigt, aber die Prinzipien sind für Arcade-Stick, Rennlenkrad und die anderen Eingabegerätetypen identisch.

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

Wie Sie sehen können, ist die Bestimmung des Zustands einer einzelnen Schaltfläche einfach, aber manchmal möchten Sie vielleicht feststellen, ob mehrere Schaltflächen gedrückt oder losgelassen werden oder ob eine Reihe von Schaltflächen auf eine bestimmte Weise angeordnet ist – einige gedrückt, andere nicht. Das Testen mehrerer Schaltflächen ist komplexer als das Testen einzelner Schaltflächen – insbesondere mit dem Potenzial des gemischten Schaltflächenzustands – aber es gibt eine einfache Formel für diese Tests, die sowohl für einzelne als auch für mehrere Schaltflächentests gilt.

Im folgenden Beispiel wird ermittelt, ob die Gamepadtasten A und B gedrückt werden:

if ((GamepadButtons::A | GamepadButtons::B) == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
    // The A and B buttons are both pressed.
}

Im folgenden Beispiel wird ermittelt, ob die Gamepadtasten A und B beide freigegeben werden:

if ((GamepadButtons::None == (reading.Buttons & GamepadButtons::A | GamepadButtons::B))
{
    // The A and B buttons are both released (not pressed).
}

Im folgenden Beispiel wird bestimmt, ob die Gamepadtaste A gedrückt wird, während Schaltfläche B losgelassen wird:

if (GamepadButtons::A == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
    // The A button is pressed and the B button is released (B is not pressed).
}

Die Formel, die in allen fünf Beispielen verwendet wird, ist wie folgt aufgebaut: Die Anordnung der zu überprüfenden Tasten wird durch den Ausdruck auf der linken Seite des Gleichheitsoperators angegeben, während die Tasten, die berücksichtigt werden sollen, durch den Maskierungsausdruck auf der rechten Seite ausgewählt werden.

Im folgenden Beispiel wird diese Formel deutlicher veranschaulicht, indem das vorherige Beispiel umgeschrieben wird:

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

Die Formel kann zum Testen einer beliebigen Anzahl von Tasten in einer beliebigen Anordnung ihrer Zustände verwendet werden.

Abrufen des Zustands des Akkus

Für jeden Gamecontroller, der die IGameControllerBatteryInfo-Schnittstelle implementiert, können Sie TryGetBatteryReport auf dem Controller aufrufen instance, um ein BatteryReport-Objekt abzurufen, das Informationen zum Akku im Controller bereitstellt. Sie können Eigenschaften wie die Laderate des Akkus (ChargeRateInMilliwatts), die geschätzte Energiekapazität eines neuen Akkus (DesignCapacityInMilliwattHours) und die vollständig geladene Energiekapazität der aktuellen Batterie (FullChargeCapacityInMilliwattHours) abrufen.

Für Gamecontroller, die detaillierte Akkuberichte unterstützen, erhalten Sie diese und weitere Informationen zum Akku, wie unter Abrufen von Akkuinformationen beschrieben. Die meisten Gamecontroller unterstützen diese Akkuberichterstellung jedoch nicht und verwenden stattdessen kostengünstige Hardware. Bei diesen Controllern müssen Sie die folgenden Überlegungen berücksichtigen:

  • ChargeRateInMilliwatts und DesignCapacityInMilliwattHours sind immer NULL.

  • Sie können den Akkuprozentsatz abrufen, indem Sie RemainingCapacityInMilliwattHours / FullChargeCapacityInMilliwattHours berechnen. Sie sollten die Werte dieser Eigenschaften ignorieren und sich nur mit dem berechneten Prozentsatz befassen.

  • Der Prozentsatz aus dem vorherigen Aufzählungszeichen ist immer einer der folgenden:

    • 100% (vollständig)
    • 70 % (Mittel)
    • 40 % (niedrig)
    • 10 % (kritisch)

Wenn Ihr Code eine Aktion (z. B. die Zeichnungsbenutzeroberfläche) basierend auf dem Prozentsatz der verbleibenden Akkulaufzeit ausführt, stellen Sie sicher, dass er den oben genannten Werten entspricht. Wenn Sie beispielsweise den Spieler warnen möchten, wenn der Akku des Controllers niedrig ist, tun Sie dies, wenn er 10 % erreicht.

Siehe auch