Aggiunta di controlli

Nota

Questo argomento fa parte della serie di esercitazioni Creare un semplice gioco UWP (Universal Windows Platform) con DirectX. L'argomento in tale collegamento imposta il contesto per la serie.

[Aggiornato per le app UWP in Windows 10. Per gli articoli di Windows 8.x, vedere l'archivio]

Un buon gioco UWP (Universal Windows Platform) supporta un'ampia gamma di interfacce. Un potenziale giocatore potrebbe avere Windows 10 su un tablet senza pulsanti fisici, un PC con un controller di gioco collegato o l'attrezzatura di gioco desktop più all'avanguardia con mouse ad alte prestazioni e tastiera di gioco. Nel nostro gioco i controlli vengono implementati nella classe MoveLookController. Questa classe aggrega tutti e tre i tipi di input (mouse e tastiera, Touch e gamepad) in un singolo controller. Il risultato finale è uno sparatutto in prima persona che utilizza controlli move-look standard di genere che funzionano con più dispositivi.

Nota

Per altre info sui controlli, vedere Controlli Move-look per i giochi e Controlli Touch per i giochi.

Obiettivo

A questo punto abbiamo un gioco che esegue il rendering, ma non possiamo spostare il nostro giocatore o sparare ai bersagli. Esamineremo il modo in cui il nostro gioco implementa i controlli move-look per uno sparatutto in prima persona per i tipi di input seguenti nel nostro gioco UWP DirectX.

  • Mouse e tastiera
  • Touch
  • Gamepad

Nota

Se non è ancora stato scaricato il codice di gioco più recente per questo esempio, andare in Esempio di gioco Direct3D. Questo esempio fa parte di una vasta raccolta di esempi di funzionalità UWP. Per istruzioni su come scaricare l'esempio, vedere Applicazioni di esempio per lo sviluppo di Windows.

Comportamenti dei controlli comuni

I controlli Touch e i controlli tramite mouse/tastiera hanno un'implementazione di base molto simile. In un'app UWP un puntatore è semplicemente un punto sullo schermo. È possibile spostarlo scorrendo il mouse o scorrendo un dito sullo schermo Touch. Di conseguenza, è possibile registrare un'unica serie di eventi, senza preoccuparsi se il giocatore utilizzi un mouse o uno schermo Touch per spostare e premere il puntatore.

Quando viene inizializzata nel gioco di esempio, la classe MoveLookController esegue la registrazione di quattro eventi specifici del puntatore e un evento specifico del mouse:

Evento Descrizione
CoreWindow::PointerPressed Il pulsante sinistro o destro del mouse è stato premuto (e tenuto premuto) o la superficie Touch è stata toccata
CoreWindow::PointerMoved Il mouse è stato spostato o è stata eseguita un'azione di trascinamento sulla superficie Touch.
CoreWindow::PointerReleased Il pulsante sinistro del mouse è stato rilasciato oppure l'oggetto a contatto con la superficie Touch è stato sollevato.
CoreWindow::PointerExited Il puntatore è stato spostato dalla finestra principale.
Windows::Devices::Input::MouseMoved Il mouse si è spostato di una certa distanza. Tenere presente che siamo interessati solo ai valori delta dello spostamento del mouse e non alla posizione X-Y corrente.

Questi gestori eventi sono impostati per avviare l'ascolto dell'input dell'utente non appena il MoveLookController viene inizializzato nella finestra dell'applicazione.

void MoveLookController::InitWindow(_In_ CoreWindow const& window)
{
    ResetState();

    window.PointerPressed({ this, &MoveLookController::OnPointerPressed });

    window.PointerMoved({ this, &MoveLookController::OnPointerMoved });

    window.PointerReleased({ this, &MoveLookController::OnPointerReleased });

    window.PointerExited({ this, &MoveLookController::OnPointerExited });

    ...

    // There is a separate handler for mouse-only relative mouse movement events.
    MouseDevice::GetForCurrentView().MouseMoved({ this, &MoveLookController::OnMouseMoved });

    ...
}

È possibile visualizzare il codice completo per InitWindow in GitHub.

Per determinare quando il gioco deve essere in ascolto per un determinato input, la classe MoveLookController presenta tre stati specifici del controller, indipendentemente dal tipo di controller:

Stato Descrizione
Nessuno Si tratta dello stato inizializzato per il controller. Tutto gli input vengono ignorati perché il gioco non prevede alcun input del controller.
WaitForInput Il controller è in attesa che il giocatore riconosca un messaggio proveniente dal gioco utilizzando un clic con il tasto sinistro del mouse, un evento Touch, o il pulsante menu su un gamepad.
Attivo Il controller è in modalità di gioco attiva.

Stato WaitForInput e sospensione del gioco

Il gioco entra nello stato WaitForInput quando il gioco è stato sospeso. Ciò si verifica quando il giocatore sposta il puntatore all'esterno della finestra principale del gioco o preme il pulsante di pausa (il tasto P o il pulsante Start del gamepad). Il MoveLookController registra la pressione e informa il ciclo di gioco quando chiama il metodo IsPauseRequested. A quel punto, se IsPauseRequested restituisce true, il ciclo di gioco chiama quindi WaitForPress sul MoveLookController per spostare il controller nello stato WaitForInput.

Una volta nello stato WaitForInput, il gioco interrompe l'elaborazione di quasi tutti gli eventi di input del gioco fino a quando non ritorna allo stato Attivo. L'eccezione è il pulsante di sospensione, che con una sua pressione fa sì che il gioco torni allo stato attivo. Oltre al pulsante di sospensione, affinché il gioco torni allo stato Attivo, il giocatore deve selezionare una voce di menu.

Lo stato Attivo

Durante lo stato Attivo, l'istanza MoveLookController elabora gli eventi provenienti da tutti i dispositivi di input abilitati e interpreta le intenzioni del giocatore. Di conseguenza, aggiorna la velocità e la direzione dello sguardo della visuale del giocatore e condivide i dati aggiornati con il gioco dopo la chiamata di Update dal ciclo di gioco.

Tutti gli input del puntatore vengono rilevati nello stato Attivo, con ID puntatore diversi corrispondenti a diverse azioni del puntatore. Quando viene ricevuto un evento PointerPressed, il MoveLookController ottiene il valore ID puntatore creato dalla finestra. L'ID puntatore rappresenta un tipo specifico di input. Ad esempio, in un dispositivo multi-touch potrebbero essere presenti diversi input attivi contemporaneamente. Gli ID vengono utilizzati per tenere traccia dell'input usato dal giocatore. Se un evento si trova nel rettangolo di spostamento dello schermo Touch, viene assegnato un ID puntatore per tenere traccia degli eventuali eventi del puntatore nel rettangolo di spostamento. Gli altri eventi del puntatore nel rettangolo di sparo vengono rilevati separatamente, con un ID puntatore separato.

Nota

L'input del mouse e la levetta destra di un gamepad hanno anche ID che vengono gestiti separatamente.

Dopo che gli eventi del puntatore sono stati mappati a un'azione di gioco specifica, è tempo di aggiornare i dati che l'oggetto MoveLookController condivide con il ciclo di gioco principale.

Quando viene chiamato, il metodo Update nel gioco di esempio elabora l'input e aggiorna le variabili velocità e direzione dello sguardo (m_velocity e m_lookdirection), che il ciclo di gioco poi recupera chiamando i metodi Velocity e LookDirection pubblici.

Nota

È possibile visualizzare maggiori informazioni sul metodo Update più avanti in questa pagina.

Il ciclo di gioco può verificare se il giocatore sta sparando chiamando il metodo IsFiring nell'istanza MoveLookController. Il MoveLookController controlla se il giocatore ha premuto il pulsante di sparo su uno dei tre tipi di input.

bool MoveLookController::IsFiring()
{
    if (m_state == MoveLookControllerState::Active)
    {
        if (m_autoFire)
        {
            return (m_fireInUse || (m_mouseInUse && m_mouseLeftInUse) || PollingFireInUse());
        }
        else
        {
            if (m_firePressed)
            {
                m_firePressed = false;
                return true;
            }
        }
    }
    return false;
}

Esaminiamo ora l'implementazione di ognuno dei tre tipi di controllo in maggiore dettaglio.

Aggiunta di controlli relativi del mouse

Se viene rilevato lo spostamento del mouse, vogliamo utilizzare tale spostamento per determinare la nuova inclinazione e imbardata della telecamera. Lo facciamo implementando controlli relativi del mouse, in cui gestiamo la distanza relativa in cui si è spostato il mouse, ovvero il delta tra l'inizio del movimento e l'arresto, invece di registrare le coordinate pixel x-y assolute del movimento.

A tale scopo, otteniamo le modifiche nelle coordinate X (movimento orizzontale) e Y (movimento verticale) esaminando i campi MouseDelta::X e MouseDelta::Y nell'oggetto di argomento Windows::Device::Input::MouseEventArgs::MouseDelta restituito dall'evento MouseMoved.

void MoveLookController::OnMouseMoved(
    _In_ MouseDevice const& /* mouseDevice */,
    _In_ MouseEventArgs const& args
    )
{
    // Handle Mouse Input via dedicated relative movement handler.

    switch (m_state)
    {
    case MoveLookControllerState::Active:
        XMFLOAT2 mouseDelta;
        mouseDelta.x = static_cast<float>(args.MouseDelta().X);
        mouseDelta.y = static_cast<float>(args.MouseDelta().Y);

        XMFLOAT2 rotationDelta;
        // Scale for control sensitivity.
        rotationDelta.x = mouseDelta.x * MoveLookConstants::RotationGain;
        rotationDelta.y = mouseDelta.y * MoveLookConstants::RotationGain;

        // Update our orientation based on the command.
        m_pitch -= rotationDelta.y;
        m_yaw += rotationDelta.x;

        // Limit pitch to straight up or straight down.
        float limit = XM_PI / 2.0f - 0.01f;
        m_pitch = __max(-limit, m_pitch);
        m_pitch = __min(+limit, m_pitch);

        // Keep longitude in sane range by wrapping.
        if (m_yaw > XM_PI)
        {
            m_yaw -= XM_PI * 2.0f;
        }
        else if (m_yaw < -XM_PI)
        {
            m_yaw += XM_PI * 2.0f;
        }
        break;
    }
}

Aggiunta del supporto Touch

I controlli Touch sono ideali per supportare gli utenti con tablet. Questo gioco raccoglie gli input Touch suddividendo in zone determinate aree dello schermo, ognuna corrispondente a specifiche azioni in gioco. L'input Touch di questo gioco utilizza tre zone.

move look touch layout

I seguenti comandi riepilogano il comportamento del nostro controllo Touch. Input utente | Azione :------- | :-------- Sposta rettangolo | L'input Touch viene convertito in un joystick virtuale in cui lo spostamento verticale sarà tradotto in movimento in avanti/indietro della posizione mentre lo spostamento orizzontale sarà tradotto in movimento a sinistra/destra della posizione. Rettangolo di sparo | Sparare a una sfera. Tocco all'esterno del rettangolo di spostamento e sparo | Modificare la rotazione (inclinazione e imbardata) della visualizzazione della telecamera.

Il MoveLookController controlla l'ID puntatore per determinare dove si è verificato l'evento e attua una delle seguenti azioni:

  • Se l'evento PointerMoved si è verificato nel rettangolo di spostamento o di sparo, aggiorna la posizione del puntatore per il controller.
  • Se l'evento PointerMoved si è verificato in un punto qualsiasi del resto dello schermo (definito come controlli di visuale), calcola la modifica di inclinazione e imbardata del vettore di direzione dello sguardo.

Dopo aver implementato i nostri controlli Touch, i rettangoli che abbiamo disegnato in precedenza tramite Direct2D indicheranno ai giocatori dove si trovano le zone di spostamento, sparo e sguardo.

touch controls

Esaminiamo ora come implementare ciascun controllo.

Controller di spostamento e sparo

Il rettangolo del controller di spostamento nel quadrante inferiore sinistro dello schermo viene utilizzato come riquadro direzionale. Lo scorrimento del pollice verso sinistra e destra all'interno di questo spazio sposta il giocatore a sinistra e a destra, mentre lo spostamento in alto e in basso sposta la telecamera in avanti e indietro. Dopo questa impostazione, toccando il controller di sparo nel quadrante inferiore destro dello schermo viene attivata una sfera.

I metodi SetMoveRect e SetFireRect creano i nostri rettangoli di input, prendendo due vettori 2D per specificare le posizioni dell'angolo superiore sinistro e inferiore destro di ogni rettangolo sullo schermo.

I parametri vengono quindi assegnati a m_fireUpperLeft e m_fireLowerRight che ci aiuteranno a determinare se l'utente sta toccando all'interno di uno dei rettangoli.

m_fireUpperLeft = upperLeft;
m_fireLowerRight = lowerRight;

Se lo schermo viene ridimensionato, questi rettangoli vengono ridisegnati in base alle dimensioni appropriate.

Ora che abbiamo suddiviso in zone i nostri controlli, è il momento di determinare quando un utente li sta effettivamente utilizzando. A tale scopo, configuriamo alcuni gestori eventi nel metodo MoveLookController::InitWindow per quando l'utente preme, sposta o rilascia il suo puntatore.

window.PointerPressed({ this, &MoveLookController::OnPointerPressed });

window.PointerMoved({ this, &MoveLookController::OnPointerMoved });

window.PointerReleased({ this, &MoveLookController::OnPointerReleased });

Prima di tutto determineremo cosa accade quando l'utente preme per la prima volta all'interno dei rettangoli di spostamento o di sparo utilizzando il metodo OnPointerPressed. Qui controlliamo dove stanno toccando un controllo e se un puntatore è già presente in tale controller. Se si tratta del primo dito a toccare il controllo specifico, agiamo nel modo seguente.

  • Archiviare la posizione del touchdown in m_moveFirstDown or m_fireFirstDown come vettore 2D.
  • Assegnare l'ID puntatore a m_movePointerID o m_firePointerID.
  • Impostare il flag InUse corretto (m_moveInUse or m_fireInUse) su true poiché ora abbiamo un puntatore attivo per tale controllo.
PointerPoint point = args.CurrentPoint();
uint32_t pointerID = point.PointerId();
Point pointerPosition = point.Position();
PointerPointProperties pointProperties = point.Properties();
auto pointerDevice = point.PointerDevice();
auto pointerDeviceType = pointerDevice.PointerDeviceType();

XMFLOAT2 position = XMFLOAT2(pointerPosition.X, pointerPosition.Y);

...
case MoveLookControllerState::Active:
    switch (pointerDeviceType)
    {
    case winrt::Windows::Devices::Input::PointerDeviceType::Touch:
        // Check to see if this pointer is in the move control.
        if (position.x > m_moveUpperLeft.x &&
            position.x < m_moveLowerRight.x &&
            position.y > m_moveUpperLeft.y &&
            position.y < m_moveLowerRight.y)
        {
            // If no pointer is in this control yet.
            if (!m_moveInUse)
            {
                // Process a DPad touch down event.
                // Save the location of the initial contact
                m_moveFirstDown = position;
                // Store the pointer using this control
                m_movePointerID = pointerID;
                // Set InUse flag to signal there is an active move pointer
                m_moveInUse = true;
            }
        }
        // Check to see if this pointer is in the fire control.
        else if (position.x > m_fireUpperLeft.x &&
            position.x < m_fireLowerRight.x &&
            position.y > m_fireUpperLeft.y &&
            position.y < m_fireLowerRight.y)
        {
            if (!m_fireInUse)
            {
                // Save the location of the initial contact
                m_fireLastPoint = position;
                // Store the pointer using this control
                m_firePointerID = pointerID;
                // Set InUse flag to signal there is an active fire pointer
                m_fireInUse = true;
                ...
            }
        }
        ...

Ora che abbiamo determinato se l'utente sta toccando o meno un controllo di movimento o sparo, vediamo se il giocatore sta facendo movimenti con il dito premuto. Utilizzando il metodo MoveLookController::OnPointerMoved, controlliamo quale puntatore si è spostato e quindi archiviamo la sua nuova posizione come vettore 2D.

PointerPoint point = args.CurrentPoint();
uint32_t pointerID = point.PointerId();
Point pointerPosition = point.Position();
PointerPointProperties pointProperties = point.Properties();
auto pointerDevice = point.PointerDevice();

// convert to allow math
XMFLOAT2 position = XMFLOAT2(pointerPosition.X, pointerPosition.Y);

switch (m_state)
{
case MoveLookControllerState::Active:
    // Decide which control this pointer is operating.

    // Move control
    if (pointerID == m_movePointerID)
    {
        // Save the current position.
        m_movePointerPosition = position;
    }
    // Look control
    else if (pointerID == m_lookPointerID)
    {
        ...
    }
    // Fire control
    else if (pointerID == m_firePointerID)
    {
        m_fireLastPoint = position;
    }
    ...

Una volta che l'utente ha eseguito i movimenti all'interno dei controlli, rilascia il puntatore. Utilizzando il metodo MoveLookController::OnPointerReleased, determiniamo quale puntatore è stato rilasciato ed eseguiamo una serie di reimpostazioni.

Se il controllo di spostamento è stato rilasciato, agiamo nel modo seguente.

  • Impostare la velocità del giocatore su 0 in tutte le direzioni per impedire spostamenti nel gioco.
  • Passare m_moveInUse su false poiché l'utente non tocca più il controller di spostamento.
  • Impostare l'ID puntatore di spostamento su 0 poiché non è più presente un puntatore nel controller di spostamento.
if (pointerID == m_movePointerID)
{
    // Stop on release.
    m_velocity = XMFLOAT3(0, 0, 0);
    m_moveInUse = false;
    m_movePointerID = 0;
}

Per il controllo di sparo, se è stato rilasciato tutto ciò che facciamo è commutare il flag m_fireInUse su false e l'ID puntatore di sparo su 0 poiché non è più presente un puntatore nel controllo dello sparo.

else if (pointerID == m_firePointerID)
{
    m_fireInUse = false;
    m_firePointerID = 0;
}

Controller di visuale

Trattiamo gli eventi del puntatore del dispositivo Touch per le aree inutilizzate dello schermo come controller di visuale. Lo scorrimento del dito intorno a questa zona cambia inclinazione e imbardata della telecamera del giocatore.

Se l'evento MoveLookController::OnPointerPressed viene generato in un dispositivo Touch in questa area e lo stato del gioco è impostato su Attivo, viene assegnato un ID puntatore.

// If no pointer is in this control yet.
if (!m_lookInUse)
{
    // Save point for later move.
    m_lookLastPoint = position;
    // Store the pointer using this control.
    m_lookPointerID = pointerID;
    // These are for smoothing.
    m_lookLastDelta.x = m_lookLastDelta.y = 0;
    m_lookInUse = true;
}

In questo caso MoveLookController assegna l'ID puntatore per il puntatore che ha generato l'evento a una variabile specifica che corrisponde all'area di visuale. Nel caso di un tocco nell'area di visuale, la variabile m_lookPointerID viene impostata sull'ID puntatore che ha generato l'evento. Una variabile booleana, m_lookInUse, è impostata anche per indicare che il controllo non è ancora stato rilasciato.

A questo punto, esaminiamo come il gioco di esempio gestisce l'evento touch screenPointerMoved.

All'interno del metodo MoveLookController::OnPointerMoved controlliamo per vedere quale tipo di ID puntatore sia stato assegnato all'evento. Se è m_lookPointerID, calcoliamo la modifica nella posizione del puntatore. Utilizziamo quindi questo delta per calcolare la quantità di rotazione da modificare. Infine, siamo in un punto in cui possiamo aggiornare m_pitch e m_yaw da utilizzare nel gioco per modificare la rotazione del giocatore.

// This is the look pointer.
else if (pointerID == m_lookPointerID)
{
    // Look control.
    XMFLOAT2 pointerDelta;
    // How far did the pointer move?
    pointerDelta.x = position.x - m_lookLastPoint.x;
    pointerDelta.y = position.y - m_lookLastPoint.y;

    XMFLOAT2 rotationDelta;
    // Scale for control sensitivity.
    rotationDelta.x = pointerDelta.x * MoveLookConstants::RotationGain;
    rotationDelta.y = pointerDelta.y * MoveLookConstants::RotationGain;
    // Save for next time through.
    m_lookLastPoint = position;

    // Update our orientation based on the command.
    m_pitch -= rotationDelta.y;
    m_yaw += rotationDelta.x;

    // Limit pitch to straight up or straight down.
    float limit = XM_PI / 2.0f - 0.01f;
    m_pitch = __max(-limit, m_pitch);
    m_pitch = __min(+limit, m_pitch);
    ...
}

L'ultima parte che esamineremo è il modo in cui il gioco di esempio gestisce l'evento PointerReleased del touch screen. Una volta che l'utente ha completato il movimento Touch e rimosso il dito dallo schermo, MoveLookController::OnPointerReleased viene avviato. Se l'ID del puntatore che ha generato l'evento PointerReleased è l'ID del puntatore di spostamento registrato in precedenza, MoveLookController imposta la velocità su 0 perché il giocatore ha smesso di toccare l'area di visuale.

else if (pointerID == m_lookPointerID)
{
    m_lookInUse = false;
    m_lookPointerID = 0;
}

Aggiunta del supporto per mouse e tastiera

Questo gioco ha il seguente layout di controllo per tastiera e mouse.

Input utente Azione
W Sposta il giocatore in avanti
A Sposta il giocatore a sinistra
S Sposta il giocatore all'indietro
D Sposta il giocatore a destra
X Sposta la visualizzazione verso l'alto
Barra spaziatrice Sposta la visualizzazione verso il basso
P Mette in pausa il gioco
Movimento del mouse Modifica la rotazione (il passo e l'imbardata) della visuale della telecamera
Tasto sinistro del mouse Spara una sfera

Per utilizzare la tastiera, il gioco di esempio registra due nuovi eventi, CoreWindow::KeyUp e CoreWindow::KeyDown, all'interno del metodo MoveLookController::InitWindow. Questi eventi gestiscono la pressione e il rilascio di un tasto.

window.KeyDown({ this, &MoveLookController::OnKeyDown });

window.KeyUp({ this, &MoveLookController::OnKeyUp });

Il mouse viene trattato in modo leggermente diverso dai controlli Touch anche se utilizza un puntatore. Per allinearsi al nostro layout del controllo, il MoveLookController ruota la telecamera ogni volta che il mouse viene spostato e spara quando viene premuto il tasto sinistro del mouse.

Tutto questo viene gestito nel metodo OnPointerPressed del MoveLookController.

In questo metodo verifichiamo il tipo di dispositivo puntatore che viene utilizzato con l'enumerazione Windows::Devices::Input::PointerDeviceType. Se il gioco è Attivo e PointerDeviceType non è Touch, presupponiamo che sia l'input del mouse.

case MoveLookControllerState::Active:
    switch (pointerDeviceType)
    {
    case winrt::Windows::Devices::Input::PointerDeviceType::Touch:
        // Behavior for touch controls
        ...

    default:
        // Behavior for mouse controls
        bool rightButton = pointProperties.IsRightButtonPressed();
        bool leftButton = pointProperties.IsLeftButtonPressed();

        if (!m_autoFire && (!m_mouseLeftInUse && leftButton))
        {
            m_firePressed = true;
        }

        if (!m_mouseInUse)
        {
            m_mouseInUse = true;
            m_mouseLastPoint = position;
            m_mousePointerID = pointerID;
            m_mouseLeftInUse = leftButton;
            m_mouseRightInUse = rightButton;
            // These are for smoothing.
            m_lookLastDelta.x = m_lookLastDelta.y = 0;
        }
        break;
    }
    break;

Quando il giocatore smette di premere uno dei tasti del mouse, viene generato l'evento mouse CoreWindow::PointerReleased, chiamando il metodo MoveLookController::OnPointerReleased e l'input è completo. A questo punto, non saranno più sparate sfere se il tasto sinistro del mouse era stato premuto e ora è rilasciato. Poiché la visuale è sempre abilitata, il gioco continua a utilizzare lo stesso puntatore del mouse per tenere traccia degli eventi di visuale in corso.

case MoveLookControllerState::Active:
    // Touch points
    if (pointerID == m_movePointerID)
    {
        // Stop movement
        ...
    }
    else if (pointerID == m_lookPointerID)
    {
        // Stop look rotation
        ...
    }
    // Fire button has been released
    else if (pointerID == m_firePointerID)
    {
        // Stop firing
        ...
    }
    // Mouse point
    else if (pointerID == m_mousePointerID)
    {
        bool rightButton = pointProperties.IsRightButtonPressed();
        bool leftButton = pointProperties.IsLeftButtonPressed();

        // Mouse no longer in use so stop firing
        m_mouseInUse = false;

        // Don't clear the mouse pointer ID so that Move events still result in Look changes.
        // m_mousePointerID = 0;
        m_mouseLeftInUse = leftButton;
        m_mouseRightInUse = rightButton;
    }
    break;

Esaminiamo ora l'ultimo tipo di controllo che supporteremo: i gamepad. I gamepad vengono gestiti separatamente dai controlli Touch e mouse in quanto non utilizzano l'oggetto puntatore. Per questo motivo, dovremo aggiungere alcuni nuovi gestori eventi e metodi.

Aggiunta del supporto Gamepad

Per questo gioco, il supporto del gamepad viene aggiunto tramite chiamate alle API Windows.Gaming.Input. Questo set di API fornisce accesso agli input del controller di gioco, ad esempio volanti da corsa e levette di volo.

Di seguito saranno riportati i nostri controlli per gamepad.

Input utente Azione
Levetta analogica sinistro Sposta il giocatore
Levetta analogica destra Modifica la rotazione (il passo e l'imbardata) della visuale della telecamera
Trigger destro Spara una sfera
Pulsante Start/Menu Sospende o riprende il gioco

Nel metodo InitWindow aggiungiamo due nuovi eventi per determinare se un gamepad è stato aggiunto o rimosso. Questi eventi aggiornano la proprietà m_gamepadsChanged. Questa viene utilizzata nel metodo UpdatePollingDevices per verificare se l'elenco dei gamepad noti è cambiato.

// Detect gamepad connection and disconnection events.
Gamepad::GamepadAdded({ this, &MoveLookController::OnGamepadAdded });

Gamepad::GamepadRemoved({ this, &MoveLookController::OnGamepadRemoved });

Nota

Le app UWP non possono ricevere input da un controller di gioco mentre l'app non è attiva.

Il metodo UpdatePollingDevices

Il metodo UpdatePollingDevices dell'istanza MoveLookController verifica immediatamente se un gamepad è collegato. In caso affermativo, inizieremo a leggerne lo stato con Gamepad.GetCurrentReading. Questo restituisce lo struct GamepadReading, che ci consente di controllare quali pulsanti sono stati premuti o quali levette sono state spostate.

Se lo stato del gioco è WaitForInput, ascoltiamo solo il pulsante Start/Menu del controller in modo che il gioco possa essere ripreso.

Se è Attivo, controlliamo l'input dell'utente e determiniamo quale azione nel gioco deve accadere. Ad esempio, se l'utente ha spostato la levetta analogica sinistra in una direzione specifica, questa azione consente al gioco di sapere che dobbiamo spostare il giocatore nella direzione in cui viene spostata la levetta. Il movimento della levetta in una direzione specifica deve essere registrato come più grande del raggio della zona morta; in caso contrario, non accadrà nulla. Questo raggio della zona morta è necessario per evitare "deviazioni", ovvero quando il controller raccoglie i piccoli movimenti dal pollice del giocatore mentre è inattivo sulla levetta. Senza zone morte, i controlli possono apparire troppo sensibili all'utente.

L'input della levetta è compreso tra -1 e 1 per l'asse x e y. La costante seguente specifica il raggio della zona morta della levetta.

#define THUMBSTICK_DEADZONE 0.25f

Utilizzando questa variabile, inizieremo quindi a elaborare l'input della levetta azionabile. Lo spostamento si verificherebbe con un valore compreso tra [-1, -.26] o [.26, 1] su entrambi gli assi.

dead zone for thumbsticks

Questa parte del metodo UpdatePollingDevices gestisce le levette sinistra e destra. I valori X e Y di ciascuna levetta vengono controllati per verificare se si trovano all'esterno della zona morta. Se una o entrambe lo sono, aggiorneremo il componente corrispondente. Ad esempio, se la levetta sinistra viene spostata a sinistra lungo l'asse X, aggiungeremo -1 al componente x del vettore m_moveCommand. Questo vettore è quello che utilizzeremo per aggregare tutti i movimenti in tutti i dispositivi e che utilizzeremo successivamente per calcolare dove deve spostarsi il giocatore.

// Use the left thumbstick to control the eye point position
// (position of the player).

// Check if left thumbstick is outside of dead zone on x axis
if (reading.LeftThumbstickX > THUMBSTICK_DEADZONE ||
    reading.LeftThumbstickX < -THUMBSTICK_DEADZONE)
{
    // Get value of left thumbstick's position on x axis
    float x = static_cast<float>(reading.LeftThumbstickX);
    // Set the x of the move vector to 1 if the stick is being moved right.
    // Set to -1 if moved left. 
    m_moveCommand.x -= (x > 0) ? 1 : -1;
}

// Check if left thumbstick is outside of dead zone on y axis
if (reading.LeftThumbstickY > THUMBSTICK_DEADZONE ||
    reading.LeftThumbstickY < -THUMBSTICK_DEADZONE)
{
    // Get value of left thumbstick's position on y axis
    float y = static_cast<float>(reading.LeftThumbstickY);
    // Set the y of the move vector to 1 if the stick is being moved forward.
    // Set to -1 if moved backwards.
    m_moveCommand.y += (y > 0) ? 1 : -1;
}

Analogamente al modo in cui la levetta sinistra controlla il movimento, la levetta destra controlla la rotazione della telecamera.

Il comportamento della levetta destra è allineato al comportamento dello spostamento del mouse nella nostra configurazione di controllo di mouse e tastiera. Se la levetta si trova all'esterno della zona morta, calcoliamo la differenza tra la posizione corrente del puntatore e la posizione in cui l'utente sta tentando di cercare. Questa modifica nella posizione del puntatore (pointerDelta) viene quindi utilizzata per aggiornare inclinazione e imbardata della rotazione della telecamera che successivamente vengono applicate nel nostro metodo Update. Il vettore pointerDelta potrebbe sembrare familiare in quanto viene utilizzato anche nel metodo MoveLookController::OnPointerMoved per tenere traccia della modifica della posizione del puntatore per i nostri input di mouse e Touch.

// Use the right thumbstick to control the look at position

XMFLOAT2 pointerDelta;

// Check if right thumbstick is outside of deadzone on x axis
if (reading.RightThumbstickX > THUMBSTICK_DEADZONE ||
    reading.RightThumbstickX < -THUMBSTICK_DEADZONE)
{
    float x = static_cast<float>(reading.RightThumbstickX);
    // Register the change in the pointer along the x axis
    pointerDelta.x = x * x * x;
}
// No actionable thumbstick movement. Register no change in pointer.
else
{
    pointerDelta.x = 0.0f;
}
// Check if right thumbstick is outside of deadzone on y axis
if (reading.RightThumbstickY > THUMBSTICK_DEADZONE ||
    reading.RightThumbstickY < -THUMBSTICK_DEADZONE)
{
    float y = static_cast<float>(reading.RightThumbstickY);
    // Register the change in the pointer along the y axis
    pointerDelta.y = y * y * y;
}
else
{
    pointerDelta.y = 0.0f;
}

XMFLOAT2 rotationDelta;
// Scale for control sensitivity.
rotationDelta.x = pointerDelta.x * 0.08f;
rotationDelta.y = pointerDelta.y * 0.08f;

// Update our orientation based on the command.
m_pitch += rotationDelta.y;
m_yaw += rotationDelta.x;

// Limit pitch to straight up or straight down.
m_pitch = __max(-XM_PI / 2.0f, m_pitch);
m_pitch = __min(+XM_PI / 2.0f, m_pitch);

I controlli del gioco non sarebbero completi senza la possibilità di sparare sfere!

Questo metodo UpdatePollingDevices controlla anche se viene premuto il trigger destro. In caso affermativo, la nostra proprietà m_firePressed viene cambiata in true, segnalando al gioco che deve iniziare a sparare le sfere.

if (reading.RightTrigger > TRIGGER_DEADZONE)
{
    if (!m_autoFire && !m_gamepadTriggerInUse)
    {
        m_firePressed = true;
    }

    m_gamepadTriggerInUse = true;
}
else
{
    m_gamepadTriggerInUse = false;
}

Il metodo Update

Per completare le operazioni, esaminiamo più in dettaglio il metodo Update. Questo metodo unisce qualsiasi movimento o rotazione che il giocatore ha creato con qualsiasi input supportato per generare un vettore di velocità e aggiornare i nostri valori di inclinazione e imbardata per l'accesso al nostro ciclo di gioco.

Il metodo Update avvia le operazioni chiamando UpdatePollingDevices per aggiornare lo stato del controller. Questo metodo raccoglie anche qualsiasi altro input da un gamepad e ne aggiunge i movimenti al vettore m_moveCommand .

Nel nostro metodo Update vengono quindi effettuati i seguenti controlli di input.

  • Se il giocatore utilizza il rettangolo del controller di spostamento, determineremo quindi la variazione nella posizione del puntatore e la useremo per calcolare se l'utente ha spostato il puntatore fuori dalla zona morta del controller. Se al di fuori della zona morta, la proprietà vettoriale m_moveCommand viene quindi aggiornata con il valore del joystick virtuale.
  • Se uno degli input della tastiera di spostamento viene premuto, un valore di 1.0f o -1.0f viene aggiunto nel componente corrispondente del vettore m_moveCommand,1.0f per avanti e -1.0f per indietro.

Dopo che tutti gli input di movimento sono stati presi in considerazione, eseguiamo il vettore m_moveCommand attraverso alcuni calcoli per generare un nuovo vettore che rappresenti la direzione del giocatore per quanto riguarda il mondo del gioco. Prendiamo quindi i nostri movimenti in relazione al mondo e li applichiamo al giocatore come velocità in quella direzione. Infine, reimpostamo il vettore m_moveCommand su (0.0f, 0.0f, 0.0f) in modo che tutto sia pronto per il frame successivo del gioco.

void MoveLookController::Update()
{
    // Get any gamepad input and update state
    UpdatePollingDevices();

    if (m_moveInUse)
    {
        // Move control.
        XMFLOAT2 pointerDelta;

        pointerDelta.x = m_movePointerPosition.x - m_moveFirstDown.x;
        pointerDelta.y = m_movePointerPosition.y - m_moveFirstDown.y;

        // Figure out the command from the virtual joystick.
        XMFLOAT3 commandDirection = XMFLOAT3(0.0f, 0.0f, 0.0f);
        // Leave 32 pixel-wide dead spot for being still.
        if (fabsf(pointerDelta.x) > 16.0f)
            m_moveCommand.x -= pointerDelta.x / fabsf(pointerDelta.x);

        if (fabsf(pointerDelta.y) > 16.0f)
            m_moveCommand.y -= pointerDelta.y / fabsf(pointerDelta.y);
    }

    // Poll our state bits set by the keyboard input events.
    if (m_forward)
    {
        m_moveCommand.y += 1.0f;
    }
    if (m_back)
    {
        m_moveCommand.y -= 1.0f;
    }
    if (m_left)
    {
        m_moveCommand.x += 1.0f;
    }
    if (m_right)
    {
        m_moveCommand.x -= 1.0f;
    }
    if (m_up)
    {
        m_moveCommand.z += 1.0f;
    }
    if (m_down)
    {
        m_moveCommand.z -= 1.0f;
    }

    // Make sure that 45deg cases are not faster.
    if (fabsf(m_moveCommand.x) > 0.1f ||
        fabsf(m_moveCommand.y) > 0.1f ||
        fabsf(m_moveCommand.z) > 0.1f)
    {
        XMStoreFloat3(&m_moveCommand, XMVector3Normalize(XMLoadFloat3(&m_moveCommand)));
    }

    // Rotate command to align with our direction (world coordinates).
    XMFLOAT3 wCommand;
    wCommand.x = m_moveCommand.x * cosf(m_yaw) - m_moveCommand.y * sinf(m_yaw);
    wCommand.y = m_moveCommand.x * sinf(m_yaw) + m_moveCommand.y * cosf(m_yaw);
    wCommand.z = m_moveCommand.z;

    // Scale for sensitivity adjustment.
    // Our velocity is based on the command. Y is up.
    m_velocity.x = -wCommand.x * MoveLookConstants::MovementGain;
    m_velocity.z = wCommand.y * MoveLookConstants::MovementGain;
    m_velocity.y = wCommand.z * MoveLookConstants::MovementGain;

    // Clear movement input accumulator for use during next frame.
    m_moveCommand = XMFLOAT3(0.0f, 0.0f, 0.0f);
}

Passaggi successivi

Ora che abbiamo aggiunto i nostri controlli, è necessario aggiungere un'altra funzionalità per creare un gioco immersivo: il suono! Musica ed effetti sonori sono importanti per qualsiasi gioco, quindi esaminiamo l'aggiunta del suono successiva.