Delen via


Besturingselementen toevoegen

Opmerking

Dit onderwerp maakt deel uit van de tutorialreeks Een eenvoudig Universal Windows Platform (UWP) spel maken met DirectX. In het onderwerp op die koppeling wordt de context voor de reeks ingesteld.

[ Bijgewerkt voor UWP-apps in Windows 10. Zie het archief ] voor windows 8.x-artikelen

Een goed UWP-spel (Universal Windows Platform) ondersteunt een breed scala aan interfaces. Een potentiële speler kan Windows 10 hebben op een tablet zonder fysieke knoppen, een pc met een gamecontroller aangesloten of de nieuwste desktop gaming-rig met een krachtige muis en gaming toetsenbord. In ons spel worden de besturingselementen geïmplementeerd in de klasse MoveLookController. Met deze klasse worden alle drie de typen invoer (muis en toetsenbord, touch en gamepad) samengevoegd tot één controller. Het eindresultaat is een first-person shooter die gebruikmaakt van standaard besturingselementen binnen het genre voor bewegen en kijken, die compatibel zijn met meerdere apparaten.

Opmerking

Voor meer informatie over besturingselementen, zie Verplaats-kijk-besturingselementen voor games en Touch-besturingselementen voor games.

Doelstelling

Op dit moment hebben we een spel dat wordt weergegeven, maar we kunnen de speler niet verplaatsen of de doelen afschieten. We bekijken hoe ons spel besturingselementen voor beweging en zicht van een first-person shooter implementeert voor de volgende typen invoer in ons UWP DirectX-spel.

  • Muis en toetsenbord
  • Aanraken
  • Spelpaneel

Opmerking

Als u de nieuwste gamecode voor dit voorbeeld nog niet hebt gedownload, gaat u naar Direct3D-voorbeeldspel. Dit voorbeeld maakt deel uit van een grote verzameling UWP-functievoorbeelden. Zie Voorbeeldtoepassingen voor Windows-ontwikkelingvoor instructies over het downloaden van het voorbeeld.

Veelvoorkomend gedrag van besturingselementen

Aanraakbesturingselementen en muis-/toetsenbordbesturingselementen hebben een zeer vergelijkbare kern-implementatie. In een UWP-app is een aanwijzer gewoon een punt op het scherm. U kunt het verplaatsen door de muis te schuiven of uw vinger op het aanraakscherm te schuiven. Als gevolg hiervan kunt u zich registreren voor één set gebeurtenissen en hoeft u zich geen zorgen te maken of de speler een muis of een aanraakscherm gebruikt om te bewegen en op de aanwijzer te drukken.

Wanneer de MoveLookController klasse in het voorbeeldspel wordt geïnitialiseerd, wordt deze geregistreerd voor vier aanwijzerspecifieke gebeurtenissen en één muisspecifieke gebeurtenis:

Gebeurtenis Beschrijving
CoreWindow::P ointerPressed De linker- of rechtermuisknop werd ingedrukt (en vastgehouden) of het aanraakoppervlak werd aangeraakt.
CoreWindow::P ointerMoved De muis is verplaatst of er is een sleepactie uitgevoerd op het aanraakoppervlak.
CoreWindow::P ointerReleased De linkermuisknop is losgelaten of het object dat contact heeft met het aanraakoppervlak is opgeheven.
CoreWindow::P ointerExited De aanwijzer is uit het hoofdvenster verplaatst.
Windows::D evices::Input::MouseMoved De muis heeft een bepaalde afstand verplaatst. Houd er rekening mee dat we alleen geïnteresseerd zijn in deltawaarden voor muisbewegingen, en niet de huidige X-Y-positie.

Deze gebeurtenis-handlers zijn ingesteld om te beginnen met luisteren naar gebruikersinvoer zodra de MoveLookController wordt geïnitialiseerd in het toepassingsvenster.

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

    ...
}

Volledige code voor InitWindow- is te zien op GitHub.

Om te bepalen wanneer de game moet luisteren naar bepaalde invoer, heeft de MoveLookController klasse drie controllerspecifieke statussen, ongeacht het type controller:

Staat Beschrijving
Geen Dit is de geïnitialiseerde status voor de controller. Alle invoer wordt genegeerd omdat de game geen controllerinvoer verwacht.
WachtOpInvoer De controller wacht totdat de speler een bericht van de game bevestigt met een linkermuisklik, een aanraakevenement, of de menuknop op een gamepad.
Actieve De controller bevindt zich in de actieve spelmodus.

WaitForInput-status en het onderbreken van het spel

Het spel voert de WaitForInput status in wanneer het spel is onderbroken. Dit gebeurt wanneer de speler de aanwijzer buiten het hoofdvenster van de game verplaatst of op de pauzeknop drukt (de P-toets of de gamepad startknop). De MoveLookController registreert de druk en informeert de spel lus wanneer deze de methode IsPauseRequested aanroept. Als IsPauseRequestedtrueretourneert, roept de gameloop WaitForPress aan op de MoveLookController om de controller naar de WaitForInput toestand te verplaatsen.

Eenmaal in de WaitForInput status, stopt de game met het verwerken van bijna alle invoerevenementen voor gameplay totdat deze terugkeert naar de status Actief. De uitzondering is de pauzeknop, met een druk hierop, waardoor de game teruggaat naar de actieve status. Behalve de pauzeknop moet de speler een menu-item selecteren om terug te gaan naar de Actief staat.

De actieve status

Tijdens de status Actief verwerkt de MoveLookController exemplaar gebeurtenissen van alle ingeschakelde invoerapparaten en interpreteert de intenties van de speler. Als gevolg hiervan worden de snelheid en kijkrichting van het zicht van de speler bijgewerkt, en worden de bijgewerkte gegevens gedeeld met het spel nadat Update- wordt aangeroepen vanuit de game-loop.

Alle invoer van de aanwijzer wordt bijgehouden in de status Actief, met verschillende aanwijzer-id's die overeenkomen met verschillende aanwijzeracties. Wanneer een PointerPressed gebeurtenis wordt ontvangen, verkrijgt de MoveLookController de pointer-ID die door het venster is gemaakt. De aanwijzer-id vertegenwoordigt een specifiek type invoer. Op een apparaat met meerdere aanraakschermen kunnen er bijvoorbeeld verschillende actieve invoerwaarden tegelijk zijn. De id's worden gebruikt om bij te houden welke invoer de speler gebruikt. Als één gebeurtenis zich in de verplaatsingsrechthoek van het aanraakscherm bevindt, wordt een aanwijzer-id toegewezen om eventuele aanwijzergebeurtenissen in de verplaatsingsrechthoek bij te houden. Andere aanwijzergebeurtenissen in de brandrechthoek worden afzonderlijk bijgehouden, met een afzonderlijke aanwijzer-ID.

Opmerking

Invoer van de muis en rechter duimstick van een gamepad heeft ook id's die afzonderlijk worden verwerkt.

Nadat de aanwijzergebeurtenissen zijn toegewezen aan een specifieke gameactie, is het tijd om de gegevens bij te werken die de MoveLookController object deelt met de hoofdspellus.

Wanneer de Update methode in het voorbeeldspel wordt aangeroepen, worden de invoer en de variabelen voor snelheid en kijkrichting (m_velocity en m_lookdirection) verwerkt, die de gamelus vervolgens ophaalt door de openbare Velocity en LookDirection methoden aan te roepen.

Opmerking

Verderop op deze pagina vindt u meer informatie over de methode Update.

De gamelus kan controleren of de speler vuurt door de methode IsFiring aan te roepen op de MoveLookController instantie. De MoveLookController controleert of de speler op een van de drie invoertypen op de vuurknop heeft gedrukt.

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

Laten we nu eens kijken naar de implementatie van elk van de drie besturingstypen.

Relatieve muisbesturingselementen toevoegen

Als er muisbewegingen worden gedetecteerd, willen we die beweging gebruiken om de nieuwe toonhoogte en yaw van de camera te bepalen. Dat doen we door relatieve muisbesturingselementen te implementeren, waarbij we de relatieve afstand verwerken die de muis heeft verplaatst, de delta tussen het begin van de beweging en de stop, in plaats van de absolute x-y pixelcoördinaten van de beweging op te nemen.

Hiervoor verkrijgen we de wijzigingen in de X (de horizontale beweging) en de Y (de verticale beweging) door de velden MouseDelta::X en MouseDelta::Y in het argumentobject Windows::Device::Input::MouseEventArgs::MouseDelta, dat wordt geretourneerd door de MouseMoved gebeurtenis, te bekijken.

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

Aanraakondersteuning toevoegen

Aanraakbesturingselementen zijn ideaal voor het ondersteunen van gebruikers met tablets. Dit spel verzamelt aanraakinvoer door bepaalde gebieden van het scherm te zoneren, waarbij elk wordt uitgelijnd op specifieke acties in de game. De aanraakinvoer van dit spel maakt gebruik van drie zones.

verplaatsen uiterlijk aanraken ontwerp

De volgende opdrachten geven een overzicht van het gedrag van aanraakbesturing. Gebruikersinvoer | Actie:------- | :-------- Rechthoek verplaatsen | Aanraakinvoer wordt omgezet in een virtuele joystick waar de verticale beweging wordt omgezet in beweging van voorwaartse/achteruitpositie en horizontale beweging wordt omgezet in beweging van links/rechts. Vuur een rechthoek af | Lanceer een bol. Raak buiten de verplaatsings- en schietrechthoek | Wijzig de draaiing (de kanteling en richting) van het cameraperspectief.

De MoveLookController controleert de aanwijzer-id om te bepalen waar de gebeurtenis is opgetreden en voert een van de volgende acties uit:

  • Als de AanwijzerVerplaatst gebeurtenis gebeurt in de verplaatsings- of schietrechthoek, update de aanwijzerpositie voor de controller.
  • Als de PointerMoved gebeurtenis ergens in de rest van het scherm is opgetreden (gedefinieerd als de uiterlijkbesturingselementen), berekent u de wijziging in pitch en yaw van de blikrichtingsvector.

Zodra we onze aanraakbesturingselementen hebben geïmplementeerd, geven de rechthoeken die we eerder hebben getekend met Direct2D aan bij spelers waar de verplaatsings-, vuur- en lookzones zijn.

aanraakbesturingselementen

Laten we nu eens kijken hoe we elk besturingselement implementeren.

Verplaats- en brandcontroller

De rechthoek van de verplaatsingscontroller in het kwadrant linksonder van het scherm wordt gebruikt als een directioneel pad. Als u uw duim naar links en rechts schuift binnen deze ruimte, beweegt u de speler naar links en rechts, terwijl de camera omhoog en omlaag naar voren en achteruit beweegt. Nadat u dit hebt ingesteld, schiet u een bol door op de vuurregelaar in het kwadrant rechtsonder op het scherm te tikken.

De SetMoveRect en SetFireRect methoden maken onze invoerrechthoeken, waarbij twee 2D-vectoren worden gebruikt om de posities linksboven en de rechterbenedenhoek van elke rechthoek op het scherm op te geven.

De parameters worden vervolgens toegewezen aan m_fireUpperLeft en m_fireLowerRight die ons helpen te bepalen of de gebruiker aanraakt binnen een van de rechthoeken.

m_fireUpperLeft = upperLeft;
m_fireLowerRight = lowerRight;

Als het formaat van het scherm wordt gewijzigd, worden deze rechthoeken opnieuw getekend naar de juiste grootte.

Nu we onze besturingselementen hebben afgezoneerd, is het tijd om te bepalen wanneer een gebruiker deze daadwerkelijk gebruikt. Hiervoor hebben we een aantal gebeurtenis-handlers ingesteld in de MoveLookController::InitWindow methode voor wanneer de gebruiker op de aanwijzer drukt, verplaatst of loslaat.

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

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

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

We bepalen eerst wat er gebeurt wanneer de gebruiker voor het eerst op de beweeg- of vuurrechthoek drukt met de OnPointerPressed-methode. Hier controleren we waar ze een besturingselement aanraken en of er al een aanwijzer in die controller staat. Als dit de eerste vinger is om het specifieke besturingselement aan te raken, doen we het volgende.

  • Sla de locatie van de touchdown op in m_moveFirstDown of m_fireFirstDown als een 2D-vector.
  • Wijs de aanwijzer-id toe aan m_movePointerID of m_firePointerID.
  • Stel de juiste InUse vlag (m_moveInUse of m_fireInUse) in op true omdat we nu een actieve aanwijzer voor dat besturingselement hebben.
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;
                ...
            }
        }
        ...

Nu we hebben vastgesteld of de gebruiker een beweging of vuurbesturing aanraakt, zien we of de speler bewegingen maakt met hun ingedrukt vinger. Met behulp van de methode MoveLookController::OnPointerMoved, controleren we welke aanwijzer is verplaatst en slaat u de nieuwe positie vervolgens op als een 2D-vector.

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

Zodra de gebruiker zijn bewegingen binnen de besturingselementen heeft gemaakt, laat hij de aanwijzer los. Met behulp van de methode MoveLookController::OnPointerReleased bepalen we welke aanwijzer is vrijgegeven en voeren we een reeks resets uit.

Als het verplaatsingsbeheer is vrijgegeven, doen we het volgende.

  • Stel de snelheid van de speler in op 0 in alle richtingen om te voorkomen dat ze in het spel bewegen.
  • Schakel m_moveInUse over naar false omdat de gebruiker de verplaatsingscontroller niet meer aanraakt.
  • Stel de id van de verplaatsingspointer in op 0 omdat er geen aanwijzer meer in de verplaatsingscontroller staat.
if (pointerID == m_movePointerID)
{
    // Stop on release.
    m_velocity = XMFLOAT3(0, 0, 0);
    m_moveInUse = false;
    m_movePointerID = 0;
}

Als de brandbesturing is losgelaten, is het enige wat we doen de m_fireInUse vlag overschakelen naar false en de brandwijzer-id naar 0 omdat er geen aanwijzer meer is in de brandbesturing.

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

Controller zoeken

We behandelen de touch device-aanwijzergebeurtenissen voor de ongebruikte delen van het scherm als de blikbesturing. Als u uw vinger over deze zone schuift, verandert de verticale en horizontale rotatie van de spelercamera.

Als de gebeurtenis MoveLookController::OnPointerPressed wordt gegenereerd op een aanraakapparaat in deze regio en de gamestatus is ingesteld op Actief, wordt er een aanwijzer-id toegewezen.

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

Hier wijst de MoveLookController de aanwijzer-id toe voor de aanwijzer die de gebeurtenis heeft geactiveerd aan een specifieke variabele die overeenkomt met de lookregio. In het geval van een aanraking in de lookregio, wordt de m_lookPointerID variabele ingesteld op de aanwijzer-id die de gebeurtenis heeft geactiveerd. Een booleaanse variabele, m_lookInUse, wordt ook gebruikt om aan te geven dat het besturingselement nog niet is vrijgegeven.

Laten we nu eens kijken hoe het voorbeeldspel de PointerMoved gebeurtenis van het aanraakscherm verwerkt.

In de methode MoveLookController::OnPointerMoved controleren we welk type aanwijzer-id is toegewezen aan de gebeurtenis. Als het m_lookPointerIDis, berekenen we de wijziging in de positie van de aanwijzer. Vervolgens gebruiken we deze delta om te berekenen hoeveel de rotatie moet veranderen. Ten slotte zijn we op een punt waar we de m_pitch en de m_yaw kunnen bijwerken om te gebruiken in het spel om de draaiing van de speler te wijzigen.

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

Het laatste stuk dat we gaan bekijken, is hoe het voorbeeldspel de PointerReleased touch screen-gebeurtenis verwerkt. Nadat de gebruiker de aanraakbeweging heeft voltooid en zijn vinger van het scherm heeft verwijderd, wordt MoveLookController::OnPointerReleased gestart. Als de ID van de aanwijzer die de PointerReleased gebeurtenis startte de ID is van de eerder vastgelegde beweeg-aanwijzer, stelt de MoveLookController de snelheid in op 0 omdat de speler gestopt is met het aanraken van het gezichtveld.

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

Muis- en toetsenbordondersteuning toevoegen

Dit spel heeft de volgende besturingsindeling voor toetsenbord en muis.

Gebruikersinvoer Handeling
W Speler vooruit verplaatsen
Een Speler naar links verplaatsen
S Speler naar achteren verplaatsen
D Speler naar rechts verplaatsen
X Verplaats weergave omhoog
Spatiebalk Weergave omlaag verplaatsen
P Het spel onderbreken
Muisbeweging De rotatie (de hoek en draai) van de camerahoek aanpassen
Linkermuisknop Een bol afvuren

Als u het toetsenbord wilt gebruiken, registreert het voorbeeldspel twee nieuwe gebeurtenissen, CoreWindow::KeyUp en CoreWindow::KeyDown, binnen de methode MoveLookController::InitWindow. Deze events verwerken het indrukken en loslaten van een toets.

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

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

De muis wordt iets anders behandeld dan de aanraakbesturingselementen, ook al wordt er een aanwijzer gebruikt. Als u de besturingsindeling wilt uitlijnen, draait de MoveLookController de camera wanneer de muis wordt verplaatst en wordt geactiveerd wanneer de linkermuisknop wordt ingedrukt.

Dit wordt verwerkt in de OnPointerPressed-methode van de MoveLookController.

In deze methode controleren we om te zien welk type aanwijsapparaat wordt gebruikt met de Windows::Devices::Input::PointerDeviceType enum. Als het spel Actief is en het PointerDeviceType niet Touchis, nemen we aan dat het muisinvoer is.

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;

Wanneer de speler stopt met drukken op een van de muisknoppen, wordt de CoreWindow::PointerReleased muisgebeurtenis opgeroepen, waarbij de MoveLookController::OnPointerReleased methode wordt aangeroepen en is de invoer voltooid. Op dit moment zullen de bollen stoppen met schieten als de linkermuisknop werd ingedrukt en nu is losgelaten. Omdat look altijd is ingeschakeld, blijft de game dezelfde muis aanwijzer gebruiken om de lopende look-gebeurtenissen bij te houden.

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;

Laten we nu kijken naar het laatste besturingstype dat we gaan ondersteunen: gamepads. Gamepads worden los van de aanraak- en muisbesturingselementen verwerkt, omdat ze het aanwijzerobject niet gebruiken. Daarom moeten er enkele nieuwe gebeurtenis-handlers en -methoden worden toegevoegd.

Ondersteuning voor gamepad toevoegen

Voor deze game wordt gamepad-ondersteuning toegevoegd door aanroepen naar de Windows.Gaming.Input API's. Deze set API's biedt toegang tot gamecontrollerinvoer zoals racewielen en vluchtsticks.

Het volgende zijn onze gamepadbesturingselementen.

Gebruikersinvoer Handeling
Linker analoge stick Speler verplaatsen
Rechter analoge stick De rotatie (de hoek en draai) van de camerahoek aanpassen
Rechter trigger Een bol afvuren
Knop Start/Menu De game onderbreken of hervatten

In de methode InitWindow voegen we twee nieuwe gebeurtenissen toe om te bepalen of een gamepad is toegevoegd of verwijderd. Met deze gebeurtenissen wordt de eigenschap m_gamepadsChanged bijgewerkt. Dit wordt gebruikt in de methode UpdatePollingDevices om te controleren of de lijst met bekende gamepads is gewijzigd.

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

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

Opmerking

UWP-apps kunnen geen invoer ontvangen van een gamecontroller terwijl de app niet in de focus staat.

De methode UpdatePollingDevices

De UpdatePollingDevices methode van de MoveLookController instantie controleert onmiddellijk of er een gamepad is verbonden. Als dat het geval is, beginnen we met het lezen van de status met Gamepad.GetCurrentReading. Hiermee wordt de GamepadReading struct geretourneerd, zodat we kunnen controleren welke knoppen zijn geklikt of duimsticks zijn verplaatst.

Als de status van het spel is WaitForInput, luisteren we alleen naar de knop Start/Menu van de controller, zodat het spel kan worden hervat.

Als het Actieveis, controleren we de invoer van de gebruiker en bepalen we welke actie in de game moet plaatsvinden. Als de gebruiker bijvoorbeeld de linker analoge stick in een specifieke richting heeft verplaatst, laat dit het spel weten dat we de speler moeten verplaatsen in de richting waarin de stick wordt verplaatst. De beweging van de stok in een specifieke richting moet worden geregistreerd als groter dan de straal van de dode zone; anders gebeurt er niets. De radius van de dode zone is nodig om te voorkomen dat er 'driften' optreedt, wat gebeurt wanneer de controller kleine bewegingen van de duim van de speler verwerkt terwijl deze op de joystick rust. Zonder dode zones kunnen de besturingselementen te gevoelig voor de gebruiker worden weergegeven.

Duimstickinvoer ligt tussen -1 en 1 voor zowel de x- als de y-as. De volgende constante geeft de straal van de duimstick dode zone aan.

#define THUMBSTICK_DEADZONE 0.25f

Met deze variabele zullen we vervolgens beginnen met het verwerken van bruikbare duimstick-invoer. Beweging zou optreden met een waarde van [-1, -.26] of [.26, 1] op een van beide asen.

dode zone voor duimsticks

Dit deel van de UpdatePollingDevices methode verwerkt de linker- en rechtervingersticks. De X- en Y-waarden van elke stick worden gecontroleerd om te zien of ze zich buiten de dode zone bevinden. Als een van de twee of beide het geval zijn, zullen we het bijbehorende onderdeel bijwerken. Als de linkervingerstick bijvoorbeeld links langs de X-as wordt verplaatst, voegen we -1 toe aan het x onderdeel van de m_moveCommand vector. Deze vector wordt gebruikt om alle bewegingen op alle apparaten samen te voegen en wordt later gebruikt om te berekenen waar de speler moet bewegen.

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

Net als bij de manier waarop de linkerstick beweging regelt, bepaalt de rechter stick de draaiing van de camera.

Het gedrag van de rechter duimstick is afgestemd op het gedrag van muisbewegingen in de installatie van de muis en toetsenbordbesturing. Als de stick zich buiten de dode zone bevindt, berekenen we het verschil tussen de huidige aanwijzerpositie en waar de gebruiker nu probeert te zoeken. Deze verandering in de positie van de aanwijzer (pointerDelta) wordt vervolgens gebruikt om de kanteling en gier van de camerarotatie bij te werken, die later in onze Update-methode worden toegepast. De pointerDelta vector kan er bekend uitzien, omdat deze ook wordt gebruikt in de methode MoveLookController::OnPointerMoved om wijzigingen in de positie van de aanwijzer voor onze muis- en aanraakinvoer bij te houden.

// 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);

De besturingselementen van het spel zouden niet compleet zijn zonder de mogelijkheid om bollen af te vuren!

Deze methode UpdatePollingDevices controleert ook of de juiste trigger wordt ingedrukt. Als dat het geval is, wordt onze m_firePressed eigenschap omgezet naar waar, waardoor het spel een signaal krijgt dat bollen moeten beginnen te schieten.

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

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

De methode Update

Aan het einde, laten we dieper ingaan op de Update-methode. Deze methode voegt de bewegingen of rotaties die de speler heeft gemaakt samen met ondersteunde invoer om een snelheidsvector te genereren en onze pitch- en yaw-waarden bij te werken, zodat ze toegankelijk zijn voor onze gamelus.

De methode Update start dingen door UpdatePollingDevices aan te roepen om de status van de controller bij te werken. Deze methode verzamelt ook invoer van een gamepad en voegt de bewegingen toe aan de m_moveCommand vector.

In onze methode Update voeren we vervolgens de volgende invoercontroles uit.

  • Als de speler gebruikmaakt van de rechthoek van de verplaatsingscontroller, bepalen we de wijziging in de aanwijzerpositie en gebruiken we deze om te berekenen of de gebruiker de aanwijzer uit de dode zone van de controller heeft verplaatst. Als de eigenschap m_moveCommand vector buiten de dode zone valt, wordt deze bijgewerkt met de waarde van de virtuele joystick.
  • Als een van de invoer van het bewegingstoetsenbord wordt ingedrukt, wordt een waarde van 1.0f of -1.0f toegevoegd aan het bijbehorende onderdeel van de m_moveCommand vector,1.0f voor vooruit en -1.0f voor achteruit.

Zodra alle bewegingsinvoer in aanmerking is genomen, voeren we de m_moveCommand vector door enkele berekeningen om een nieuwe vector te genereren die de richting van de speler vertegenwoordigt met betrekking tot de spelwereld. We nemen dan onze bewegingen in relatie tot de wereld en passen ze toe op de speler als snelheid in die richting. Ten slotte stellen we de m_moveCommand vector opnieuw in op (0.0f, 0.0f, 0.0f) zodat alles klaar is voor het volgende gameframe.

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

Volgende stappen

Nu we onze besturingselementen hebben toegevoegd, is er nog een functie die we moeten toevoegen om een meeslepend spel te maken: geluid! Muziek en geluidseffecten zijn belangrijk voor elk spel, dus laten we bespreken geluid toevoegen volgende.