Condividi tramite


Procedure di input per i giochi

Questo argomento descrive modelli e tecniche per l'uso efficace dei dispositivi di input nei giochi 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 (da premuto a rilasciato, da rilasciato a premuto)
  • come rilevare le disposizioni complesse dei pulsanti con un singolo test

Scelta di una classe di dispositivi di input

Sono disponibili molti tipi diversi di API di input, ad esempio ArcadeStick, FlightSticke 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 platform 2D, probabilmente puoi usare la classe Gamepad e non preoccuparti delle 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, si potrebbe voler enumerare tutti gli oggetti RawGameController come punto di partenza per assicurarsi che supportino qualsiasi dispositivo di nicchia che i giocatori appassionati potrebbero avere, inclusi dispositivi come pedali separati o manette che sono ancora usati da un singolo giocatore.

Da lì, puoi usare il metodo FromGameController della classe di input, come Gamepad.FromGameController, per vedere se ogni dispositivo dispone di una vista più curata. Ad esempio, se il dispositivo è anche un Gamepad, potresti voler modificare la UI per il mapping dei pulsanti per riflettere questa caratteristica e fornire alcune mappature predefinite appropriate 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 l'ID fornitore (VID) e l'ID prodotto (PID) di un RawGameController (usando HardwareVendorId e HardwareProductId, rispettivamente) e fornire mappature dei pulsanti suggerite per i dispositivi più diffusi, pur rimanendo compatibili con dispositivi sconosciuti che verranno rilasciati in futuro tramite mappature 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.Game pads), è consigliabile mantenere il proprio elenco di controller. Vedi l'elenco dei gamepad per ulteriori informazioni (ogni tipo di controller ha una sezione denominata in modo simile nel proprio argomento).

Tuttavia, cosa accade quando il giocatore scollega il controller o collega un nuovo controller? È necessario gestire questi eventi e aggiornare di conseguenza l'elenco. Vedi Aggiunta e rimozione di gamepad per ulteriori informazioni (di nuovo, ogni tipo di controller ha una sezione denominata in modo simile nel suo contesto).

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 Concurrency Runtime , in particolare con la classe critical_section , all'interno di ppl.h <>.

Un'altra cosa da considerare è che l'elenco dei controller connessi inizialmente sarà vuoto e richiede uno o due secondi per riempirsi. 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 gestore eventi per il controller aggiunto che nel gestore eventi per il controller rimosso, o nel metodo di aggiornamento.

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 User in modo che la loro identità possa essere collegata al gioco, ai risultati, alle impostazioni e ad altre attività. Gli utenti possono accedere o disconnettersi a volontà ed è comune che un utente diverso acceda a un dispositivo di input che rimane connesso al sistema dopo che l'utente precedente si è 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 navigazione dell'interfaccia utente corrispondente .

Per questi motivi, l'input del giocatore deve essere monitorato e correlato alla 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, è necessario ricordare la lettura del dispositivo precedente e confrontare la lettura corrente con quella precedente per vedere che cosa è cambiato.

Nell'esempio seguente viene illustrato un approccio di base per ricordare la lettura precedente; i gamepad sono visualizzati qui, ma i principi sono gli stessi per gli arcade stick, il volante da corsa e gli altri tipi di dispositivi 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 trasferisce il valore esistente di newReading (la lettura del gamepad dall'iterazione precedente del ciclo) in oldReading, quindi riempie newReading con una nuova lettura del gamepad per l'attuale iterazione. 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 prima 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 lo stato di destinazione (premuto o rilasciato rispettivamente) e la lettura precedente non contiene anche 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 premuto (giù) o rilasciato (su). Per efficienza, le letture dei pulsanti non sono rappresentate come singoli valori booleani; sono invece tutti compressi in campi bit rappresentati da enumerazioni specifiche del dispositivo, ad esempio GamepadButtons. Per leggere pulsanti specifici, il mascheramento bit a bit viene usato per isolare i valori di interesse. Un 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 gamepad sono visualizzati qui, ma i principi sono gli stessi per le levette arcade, i volanti da corsa e altri tipi di dispositivi 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 pulsanti multipli è più complesso rispetto al test di pulsanti singoli, in particolare con il potenziale di stati misti dei pulsanti, ma esiste una semplice formula per questi test che si applica 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 vengono rilasciati entrambi i pulsanti del game pad A e B:

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 o espressione che questi cinque 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 attraverso l'espressione di mascheramento 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 TryGetBatteryReportReport 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:

  • RataDiCaricaInMilliwatt e CapacitàDiProgettoInMilliwattOra 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 elenco precedente sarà sempre una delle seguenti:

    • 100% (completo)
    • 70% (medio)
    • 40% (bassa)
    • 10% (critico)

Se il tuo codice esegue un'azione (come disegnare l'interfaccia utente) in base alla percentuale di durata della batteria rimanente, assicurati che rispetti i valori indicati sopra. Ad esempio, se si vuole avvisare il giocatore quando la batteria del controller è bassa, farlo quando raggiunge 10%.

Vedere anche