Procedure di input per i giochi

Questo argomento descrive i modelli e le tecniche per l'uso efficace dei dispositivi di input nei giochi piattaforma UWP (Universal Windows Platform).

Leggendo questo argomento, si apprenderà quanto segue:

  • come tenere traccia dei giocatori e dei dispositivi di input e navigazione attualmente in uso
  • come rilevare le transizioni dei pulsanti (premuto a rilascio, rilasciato a pressione)
  • come rilevare le disposizioni complesse dei pulsanti con un singolo test

Scelta di una classe del dispositivo di input

Sono disponibili molti tipi diversi di API di input, ad esempio ArcadeStick, FlightStick e Game pad. Come decidi quale API usare per il tuo gioco?

È consigliabile scegliere l'API che fornisce l'input più appropriato per il gioco. Ad esempio, se stai creando un gioco di piattaforma 2D, probabilmente puoi usare solo la classe Game pad e non disturbare con le funzionalità aggiuntive disponibili tramite altre classi. Questo limiterebbe il gioco al supporto solo dei game pad e fornire un'interfaccia coerente che funzionerà in molti game pad diversi senza bisogno di codice aggiuntivo.

D'altra parte, per simulazioni complesse di volo e corse, potresti voler enumerare tutti gli oggetti RawGameController come baseline per assicurarsi che supportino qualsiasi dispositivo di nicchia che i giocatori appassionati potrebbero avere, inclusi dispositivi come pedali separati o limitazioni che vengono ancora usati da un singolo giocatore.

Da qui puoi usare il metodo FromGameController di una classe di input, ad esempio Gamepad.FromGameController, per verificare se ogni dispositivo ha una visualizzazione più accurata. Ad esempio, se il dispositivo è anche un Game pad, potresti voler regolare l'interfaccia utente del mapping dei pulsanti in modo da riflettere tale elemento e fornire alcuni mapping dei pulsanti predefiniti sensibili tra cui scegliere. Questo è in contrasto con la richiesta al giocatore di configurare manualmente gli input del game pad se usi solo RawGameController.

In alternativa, è possibile esaminare rispettivamente l'ID fornitore (VID) e l'ID prodotto (PID) di un RawGameController (usando HardwareVendorId e HardwareProductId) e fornire mapping dei pulsanti suggeriti per i dispositivi più diffusi, pur rimanendo compatibili con dispositivi sconosciuti che vengono rilasciati in futuro tramite mapping manuali da parte del giocatore.

Tenere traccia dei controller connessi

Anche se ogni tipo di controller include un elenco di controller connessi (ad esempio Gamepad.Gamepads), è consigliabile mantenere il proprio elenco di controller. Per altre informazioni, vedere l'elenco dei game pad (ogni tipo di controller ha una sezione denominata in modo analogo nel proprio argomento).

Tuttavia, cosa accade quando il lettore scollega il controller o collega un nuovo controller? È necessario gestire questi eventi e aggiornare di conseguenza l'elenco. Per altre informazioni, vedere Aggiunta e rimozione di game pad (anche in questo caso, ogni tipo di controller ha una sezione denominata in modo analogo nel proprio argomento).

Poiché gli eventi aggiunti e rimossi vengono generati in modo asincrono, è possibile ottenere risultati non corretti quando si gestiscono gli elenchi di controller. Pertanto, ogni volta che si accede all'elenco dei controller, è necessario mettere un blocco intorno a esso in modo che un solo thread possa accedervi alla volta. Questa operazione può essere eseguita con il runtime di concorrenza, in particolare la classe critical_section, in <ppl.h>.

Un'altra cosa da considerare è che l'elenco dei controller connessi inizialmente sarà vuoto e richiede un secondo o due per popolare. Quindi, se si assegna solo il game pad corrente nel metodo start, sarà null!

Per correggere questo problema, devi avere un metodo che "aggiorna" il game pad principale (in un gioco a giocatore singolo; i giochi multiplayer richiederanno soluzioni più sofisticate). È quindi necessario chiamare questo metodo sia nel controller aggiunto che nel controller i gestori eventi rimossi o nel metodo update.

Il metodo seguente restituisce semplicemente il primo game pad nell'elenco (o nullptr se l'elenco è vuoto). È sufficiente ricordare di cercare nullptr ogni volta che si esegue qualsiasi operazione con il controller. Spetta a te se vuoi bloccare il gioco quando non c'è alcun controller connesso (ad esempio, sospendo il gioco) o semplicemente avere un gioco in continuazione, ignorando l'input.

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

Mettendo tutto insieme, ecco un esempio di come gestire l'input da un game pad:

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

Rilevamento di utenti e dispositivi

Tutti i dispositivi di input sono associati a un utente in modo che la loro identità possa essere collegata al gioco, agli obiettivi, alle modifiche delle impostazioni e ad altre attività. Gli utenti possono accedere o disconnettersi a volontà ed è comune che un utente diverso accinga a accedere a un dispositivo di input che rimane connesso al sistema dopo che l'utente precedente ha disconnesso. Quando un utente esegue l'accesso o l'uscita, viene generato l'evento IGameController.UserChanged . Puoi registrare un gestore eventi per questo evento per tenere traccia dei giocatori e dei dispositivi in uso.

L'identità utente è anche il modo in cui un dispositivo di input è associato al controller di spostamento interfaccia utente corrispondente.

Per questi motivi, l'input del giocatore deve essere monitorato e correlato con la proprietà User della classe del dispositivo (ereditata dall'interfaccia IGameController ).

L'app di esempio UserGamepadPairingUWP in GitHub illustra come tenere traccia degli utenti e dei dispositivi in uso.

Rilevamento delle transizioni dei pulsanti

A volte vuoi sapere quando un pulsante viene premuto o rilasciato per la prima volta; ovvero quando lo stato del pulsante passa da rilasciato a premuto o da premuto a rilasciato. Per determinare questo problema, è necessario ricordare la lettura del dispositivo precedente e confrontare la lettura corrente con essa per vedere cosa è cambiato.

Nell'esempio seguente viene illustrato un approccio di base per ricordare la lettura precedente; i game pad sono visualizzati qui, ma i principi sono gli stessi per levette Arcade, il volante da corsa e gli altri tipi di dispositivo di input.

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

Prima di eseguire qualsiasi altra operazione, Game::Loop sposta il valore esistente di newReading (la lettura del game pad dall'iterazione del ciclo precedente) in oldReading, quindi riempie newReading con una nuova lettura del game pad per l'iterazione corrente. In questo modo vengono fornite le informazioni necessarie per rilevare le transizioni dei pulsanti.

L'esempio seguente illustra un approccio di base per il rilevamento delle transizioni dei pulsanti:

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

Queste due funzioni derivano innanzitutto lo stato booleano della selezione del pulsante da newReading e oldReading, quindi eseguono la logica booleana per determinare se si è verificata la transizione di destinazione. Queste funzioni restituiscono true solo se la nuova lettura contiene rispettivamente lo stato di destinazione (premuto o rilasciato) e la lettura precedente non contiene lo stato di destinazione; in caso contrario, restituiscono false.

Rilevamento di disposizioni complesse dei pulsanti

Ogni pulsante di un dispositivo di input fornisce una lettura digitale che indica se viene premuta (giù) o rilasciata (su). Per ragioni di efficienza, le letture dei pulsanti non sono rappresentate come singoli valori booleani; invece, sono tutti racchiusi in bitfield rappresentati da enumerazioni specifiche del dispositivo come GamepadButtons. Per leggere pulsanti specifici, la maschera bit per bit viene usata per isolare i valori a cui si è interessati. Il pulsante viene premuto (giù) quando viene impostato il bit corrispondente; in caso contrario, viene rilasciato (su).

Ricordare come i singoli pulsanti vengono determinati per essere premuti o rilasciati; i game pad sono visualizzati qui, ma i principi sono gli stessi per levette Arcade, il volante da corsa e gli altri tipi di dispositivo di input.

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

Come si può notare, determinare lo stato di un singolo pulsante è semplice, ma a volte si potrebbe voler determinare se più pulsanti vengono premuti o rilasciati o se un set di pulsanti è disposto in un modo particolare, alcuni premuti, alcuni no. Il test di più pulsanti è più complesso rispetto al test di singoli pulsanti, in particolare con il potenziale dello stato del pulsante misto, ma è disponibile una semplice formula per questi test che si applicano a test di pulsanti singoli e multipli allo stesso modo.

L'esempio seguente determina se i pulsanti A e B del game pad vengono premuti entrambi:

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

L'esempio seguente determina se i pulsanti A e B del game pad vengono premuti entrambi:

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

L'esempio seguente determina se il pulsante A del game pad viene premuto mentre viene rilasciato il pulsante 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).
}

La formula che tutti e cinque di questi esempi hanno in comune è che la disposizione dei pulsanti da testare è specificata dall'espressione sul lato sinistro dell'operatore di uguaglianza mentre i pulsanti da considerare sono selezionati dall'espressione di maschera sul lato destro.

L'esempio seguente illustra più chiaramente questa formula riscrivendo l'esempio precedente:

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

Questa formula può essere applicata per testare qualsiasi numero di pulsanti in qualsiasi disposizione dei relativi stati.

Ottenere lo stato della batteria

Per qualsiasi controller di gioco che implementa l'interfaccia IGameControllerBatteryInfo , puoi chiamare TryGetBatteryReport nell'istanza del controller per ottenere un oggetto BatteryReport che fornisce informazioni sulla batteria nel controller. È possibile ottenere proprietà come la velocità di ricarica della batteria (ChargeRateInMilliwatts), la capacità energetica stimata di una nuova batteria (DesignCapacityInMilliwattHours) e la capacità energetica completamente carica della batteria corrente (FullChargeCapacityInMilliwattHours).

Per i controller di gioco che supportano la segnalazione dettagliata della batteria, puoi ottenere questo e altre informazioni sulla batteria, come descritto in Ottenere informazioni sulla batteria. Tuttavia, la maggior parte dei controller di gioco non supporta tale livello di segnalazione della batteria e usa invece hardware a basso costo. Per questi controller, è necessario tenere presenti le considerazioni seguenti:

  • ChargeRateInMilliwatts e DesignCapacityInMilliwattHours saranno sempre NULL.

  • È possibile ottenere la percentuale di batteria calcolando RemainingCapacityInMilliwattHours / FullChargeCapacityInMilliwattHours. È consigliabile ignorare i valori di queste proprietà e gestire solo la percentuale calcolata.

  • La percentuale del punto punto elenco precedente sarà sempre una delle seguenti:

    • 100% (Completo)
    • 70% (Medio)
    • 40% (Basso)
    • 10% (Critico)

Se il codice esegue un'azione (ad esempio l'interfaccia utente di disegno) in base alla percentuale di durata della batteria rimanente, assicurarsi che sia conforme ai valori precedenti. Ad esempio, se si vuole avvisare il giocatore quando la batteria del controller è bassa, farlo quando raggiunge il 10%.

Vedi anche