Dela via


Lägga till kontroller

Anmärkning

Det här avsnittet är en del av Skapa ett enkelt UWP-spel (Universal Windows Platform) med DirectX självstudieserie. Ämnet på länken anger kontexten för serien.

[ Uppdaterad för UWP-appar i Windows 10. Information om Windows 8.x-artiklar finns i arkivet ]

Ett bra UWP-spel (Universal Windows Platform) stöder en mängd olika gränssnitt. En potentiell spelare kan ha Windows 10 på en surfplatta utan fysiska knappar, en dator med en spelstyrenhet ansluten eller den senaste skrivbordsspelriggen med en högpresterande mus och speltangentbord. I vårt spel implementeras kontrollerna i klassen MoveLookController. Den här klassen aggregerar alla tre typerna av indata (mus och tangentbord, touch och spelplatta) till en enda kontrollant. Slutresultatet är en förstapersonsskjutare som använder vanliga move-look-kontroller för genren som fungerar med flera enheter.

Anmärkning

Mer information om kontroller finns i Move-look-kontroller för spel och Touch-kontroller för spel.

Mål

Just nu har vi ett spel som renderar, men vi kan inte flytta runt vår spelare eller skjuta målen. Vi undersöker hur vårt spel implementerar förstapersonsskjutare med rörelse- och siktkontroller för följande inmatningstyper i vårt UWP DirectX-spel.

  • Mus och tangentbord
  • Beröring
  • Handkontroll

Anmärkning

Om du inte har laddat ned den senaste spelkoden för det här exemplet går du till Direct3D-exempelspel. Det här exemplet är en del av en stor samling UWP-funktionsexempel. Anvisningar om hur du laddar ned exemplet finns i Exempelprogram för Windows-utveckling.

Vanliga kontrollbeteenden

Touch-kontroller och mus-/tangentbordskontroller har en mycket liknande kärnimplementering. I en UWP-app är en pekare helt enkelt en punkt på skärmen. Du kan flytta den genom att dra musen eller dra fingret på pekskärmen. Därför kan du registrera dig för en enda uppsättning händelser och inte oroa dig för om spelaren använder en mus eller en pekskärm för att flytta och trycka på pekaren.

När MoveLookController klass i exempelspelet initieras registreras den för fyra pekarspecifika händelser och en musspecifik händelse:

Evenemang Beskrivning
CoreWindow::P ointerPressed Vänster eller höger musknapp trycktes (och hölls), eller touchytan rördes.
CoreWindow::P ointerMoved Musen flyttades eller en dragåtgärd gjordes på pekytan.
CoreWindow::P ointerReleased Den vänstra musknappen släpptes eller objektet som kontaktade pekytan lyftes.
CoreWindow::P ointerExited Pekaren flyttades ut från huvudfönstret.
Windows::D enheter::Input::MouseMoved Musen flyttade ett visst avstånd. Tänk på att vi bara är intresserade av deltavärden för musrörelser och inte den aktuella X-Y-positionen.

Dessa händelsehanterare är inställda på att börja lyssna efter användarindata så snart MoveLookController initieras i programfönstret.

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

    ...
}

Fullständig kod för InitWindow kan visas på GitHub.

För att avgöra när spelet ska lyssna efter vissa indata har MoveLookController-klassen tre kontrollantspecifika tillstånd, oavsett kontrollanttyp:

Stat/län Beskrivning
Ingen Det här är det initierade tillståndet för kontrollanten. Alla indata ignoreras eftersom spelet inte förutser några kontrollantindata.
VäntaPåInmatning Kontrollern väntar på att spelaren ska bekräfta ett meddelande från spelet genom att antingen använda en vänster musklik, en touchhändelse, eller menyknappen på en spelkontroll.
Aktiv Styrenheten är i aktivt spelläge.

WaitForInput-tillstånd och pausa spelet

Spelet går in i WaitForInput tillstånd när spelet har pausats. Detta inträffar när spelaren flyttar pekaren utanför huvudfönstret i spelet eller trycker på pausknappen (P-tangenten eller spelplattan Knappen Starta). MoveLookController registrerar knapptryckningen och informerar spelloopen när den anropar metoden IsPauseRequested. Om IsPauseRequested returnerar sant, anropar spelloopen WaitForPressMoveLookController för att överföra kontrollern till WaitForInput--tillstånd.

När spelet befinner sig i WaitForInput-tillstånd slutar det att bearbeta nästan alla spelinmatningshändelser tills det återgår till tillståndet Active. Undantaget är pausknappen, med ett tryck på detta som gör att spelet återgår till det aktiva tillståndet. Förutom pausknappen måste spelaren välja ett menyalternativ för att spelet ska gå tillbaka till Active tillstånd.

Aktivt tillstånd

Under Active-tillstånd bearbetar MoveLookController-instansen händelser från alla aktiverade indataenheter och tolkar spelarens avsikter. Som ett resultat uppdaterar den hastigheten och riktningen för spelarens vy och delar de uppdaterade uppgifterna med spelet, efter att Uppdatering har anropats från spelloopen.

Alla pekarinmatningar spåras i tillståndet Aktiv, med olika pekar-ID som motsvarar olika pekaråtgärder. När en PointerPressed-händelse tas emot hämtar MoveLookController pekarens ID-värde som skapats av fönstret. Pekar-ID:t representerar en viss typ av indata. På en multi-touch-enhet kan det till exempel finnas flera olika aktiva indata samtidigt. ID:erna används för att hålla reda på vilka indata spelaren använder. Om en händelse finns i rörelserektangeln på pekskärmen tilldelas ett pekar-ID för att spåra eventuella pekarhändelser i flyttrektangeln. Andra pekarhändelser i brandrektangeln spåras separat med ett separat pekar-ID.

Anmärkning

Indata från musen och den högra tumstickan i en spelplatta har också ID:er som hanteras separat.

När pekarhändelserna har mappats till en specifik spelåtgärd är det dags att uppdatera den data som MoveLookController-objektet delar med huvudspelsloopen.

När den anropas bearbetar metoden Update i exempelspelet indata och uppdaterar hastighets- och lookriktningsvariablerna (m_velocity och m_lookdirection), som spelloopen sedan hämtar genom att anropa metoderna public Velocity och LookDirection.

Anmärkning

Mer information om metoden Uppdatera visas senare på den här sidan.

Spellopen kan användas för att testa om spelaren eldar genom att anropa metoden IsFiringMoveLookController-instansen. MoveLookController kontrollerar om spelaren har tryckt på brandknappen på någon av de tre indatatyperna.

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

Nu ska vi titta närmare på implementeringen av var och en av de tre kontrolltyperna.

Lägga till relativa muskontroller

Om musrörelser identifieras vill vi använda den för att fastställa kamerans nya pitch och yaw. Vi gör det genom att implementera relativa muskontroller, där vi hanterar det relativa avståndet som musen har flyttat – deltat mellan början av rörelsen och stoppet – i stället för att registrera rörelsernas absoluta x-y pixelkoordinater.

För att göra det, hämtar vi ändringarna i koordinaterna X (den vågräta rörelsen) och Y (den lodräta rörelsen) genom att undersöka fälten MouseDelta::X och MouseDelta::YWindows::Device::Input::MouseEventArgs::MouseDelta argumentobjektet som returneras av händelsen 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;
    }
}

Lägga till pekstöd

Touch-kontroller är bra för att stödja användare med surfplattor. Det här spelet samlar in pekinmatning genom att zonindela vissa delar av skärmen, där varje zon motsvarar specifika åtgärder i spelet. Det här spelets beröringsfunktion använder tre zoner.

flytta peklayouten

Följande kommandon sammanfattar vårt pekkontrollbeteende. Användarindata | Åtgärd :------- | :-------- Flytta rektangel | Pekinmatningen omvandlas till en virtuell joystick där den lodräta rörelsen omvandlas till framåt-/bakåtpositionsrörelse och den vågräta rörelsen omvandlas till vänster-/högerpositionsrörelse. Brandrektangeln | Avfyra en sfär. Rör vid utanför flytta- och skjutrektangeln | Ändra rotationen (höjdled och girled) i kameravyn.

MoveLookController kontrollerar pekar-ID:t för att avgöra var händelsen inträffade och vidtar någon av följande åtgärder:

  • Om PointerMoved-händelsen inträffade i flytt- eller aktiveringsrektangeln, ska du uppdatera pekarpositionen för kontrollanten.
  • Om PointerMoved-händelsen inträffade någonstans på resten av skärmen (definierad som siktkontroller), beräkna ändringen i höjning och girning av siktvektorn.

När vi har implementerat våra pekkontroller visar rektanglarna som vi ritade tidigare med Direct2D för spelarna var flytt-, brand- och lookzonerna finns.

touchkontroller

Nu ska vi ta en titt på hur vi implementerar varje kontroll.

Flytta och skjutkontroll

Rektangeln för flyttstyrenheten i skärmens nedre vänstra kvadrant används som riktningsplatta. Genom att dra tummen åt vänster och höger inom detta utrymme flyttas spelaren åt vänster och höger, medan upp och ner rör kameran framåt och bakåt. När du har konfigurerat detta utlöses en sfär genom att trycka på brandstyrenheten i skärmens nedre högra kvadrant.

Metoderna SetMoveRect och SetFireRect skapar våra indatarektanglar och tar två 2D-vektorer för att ange varje rektanglars övre vänstra och nedre högra hörnpositioner på skärmen.

Parametrarna tilldelas sedan till m_fireUpperLeft och m_fireLowerRight som hjälper oss att avgöra om användaren rör vid rektanglarna.

m_fireUpperLeft = upperLeft;
m_fireLowerRight = lowerRight;

Om skärmens storlek ändras, ritas dessa rektanglar om till lämplig storlek.

Nu när vi har zonat bort våra kontroller är det dags att avgöra när en användare faktiskt använder dem. För att göra detta konfigurerar vi några händelsehanterare i MoveLookController::InitWindow metod för när användaren trycker på, flyttar eller släpper sin pekare.

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

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

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

Vi tar först reda på vad som händer när användaren först trycker inom rektanglarna för flytta eller skjuta med hjälp av metoden OnPointerPressed. Här kontrollerar vi var de rör vid en kontrollenhet och om en pekare redan finns i den kontrollenheten. Om det här är det första fingret som rör den specifika kontrollen gör vi följande.

  • Lagra platsen för touchdown i m_moveFirstDown eller m_fireFirstDown som en 2D-vektor.
  • Tilldela pekar-ID:t till m_movePointerID eller m_firePointerID.
  • Ställ in rätt InUse-flagga (m_moveInUse eller m_fireInUse) till true eftersom vi nu har en aktiv pekare för den kontrollen.
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 när vi har fastställt om användaren rör vid en rörelse- eller brandkontroll ser vi om spelaren gör några rörelser med sitt tryckta finger. Med hjälp av metoden MoveLookController::OnPointerMoved kontrollerar vi vilken pekare som har flyttats och lagrar sedan dess nya position som 2D-vektor.

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

När användaren har gjort sina gester i kontrollerna släpper de pekaren. Med hjälp av metoden MoveLookController::OnPointerReleased avgör vi vilken pekare som har släppts och gör en serie återställningar.

Om kontrollen för förflyttning har släppts gör vi följande.

  • Ange spelarens hastighet till 0 i alla riktningar för att förhindra att de rör sig i spelet.
  • Växla m_moveInUse till false eftersom användaren inte längre rör flyttstyrenheten.
  • Ställ in flyttpekarens ID till 0 eftersom det inte längre finns någon pekare i rörelsekontrollen.
if (pointerID == m_movePointerID)
{
    // Stop on release.
    m_velocity = XMFLOAT3(0, 0, 0);
    m_moveInUse = false;
    m_movePointerID = 0;
}

För brandkontrollen, om den har släppts allt vi gör är att växla m_fireInUse-flaggan till false och brandpekarens ID till 0 eftersom det inte längre finns någon pekare i brandkontrollen.

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

Tittkontroll

Vi hanterar pekhändelser på pekskärmsenheter för de delar av skärmen som inte används som rörelsestyrning. När du glider fingret runt den här zonen ändras spelarens kameravinkel i höjd och gir.

Om händelsen MoveLookController::OnPointerPressed aktiveras på en touchenhet i den här regionen och speltillståndet är inställt på Activetilldelas den ett pekar-ID.

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

Här tilldelar MoveLookController pekar-ID:t för pekaren som utlöste händelsen till en specifik variabel som motsvarar lookregionen. Om en touch inträffar i look-regionen är variabeln m_lookPointerID inställd på pekar-ID:t som utlöste händelsen. En boolesk variabel, m_lookInUse, är också inställd på att indikera att kontrollen ännu inte har släppts.

Nu ska vi titta på hur exempelspelet hanterar PointerMoved pekskärmshändelse.

I metoden MoveLookController::OnPointerMoved kontrollerar vi vilken typ av pekar-ID som har tilldelats händelsen. Om det är m_lookPointerIDberäknar vi ändringen i pekarens position. Sedan använder vi det här deltat för att beräkna hur mycket rotationen ska ändras. Slutligen är vi vid en punkt där vi kan uppdatera m_pitch och m_yaw som ska användas i spelet för att ändra spelarrotationen.

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

Det sista vi ska titta på är hur exempelspelet hanterar PointerReleased pekskärmshändelse. När användaren har slutfört pekgesten och tagit bort fingret från skärmen initieras MoveLookController::OnPointerReleased. Om ID:t för pekaren som utlöste händelsen PointerReleased är ID:t för den tidigare inspelade flyttpekaren anger MoveLookController hastigheten till 0 eftersom spelaren har slutat röra lookområdet.

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

Stöd för att lägga till mus och tangentbord

Det här spelet har följande kontrolllayout för tangentbord och mus.

Användarindata Åtgärd
W Flytta spelaren framåt
A Flytta spelaren till vänster
S Flytta spelaren bakåt
D Flytta spelaren åt höger
X Flytta vyn uppåt
Mellanslagstangent Flytta vyn nedåt
P Pausa spelet
Musrörelse Ändra rotationen (höjd och gir) i kameravyn
Vänster musknapp Avfyra en sfär

Om du vill använda tangentbordet registrerar exempelspelet två nya händelser, CoreWindow::KeyUp och CoreWindow::KeyDown, inom metoden MoveLookController::InitWindow. Dessa händelser hanterar nedtryckning och släpp av en tangent.

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

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

Musen behandlas lite annorlunda än touchkontrollerna även om den använder en pekare. För att justera med vår kontrolllayout roterar MoveLookController kameran när musen flyttas och utlöses när den vänstra musknappen trycks ned.

Detta hanteras i metoden OnPointerPressed i MoveLookController.

I den här metoden kontrollerar vi vilken typ av pekenhet som används med Windows::Devices::Input::PointerDeviceType-enumeration. Om spelet är Active och PointerDeviceType är inte Touch, antar vi att det är mus-indata.

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;

När en av musknapparna släpps, aktiveras mushändelsen CoreWindow::PointerReleased, metoden MoveLookController::OnPointerReleased anropas, och inmatningen är slutförd. Vid den här tidpunkten slutar sfärerna att skjutas om den vänstra musknappen har tryckts in och nu släpps. Eftersom blick är alltid aktiverad fortsätter spelet att använda samma muspekare för att spåra de pågående blickhändelserna.

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;

Nu ska vi titta på den sista kontrolltypen som vi kommer att stödja: gamepads. Gamepads hanteras separat från pek- och muskontrollerna eftersom de inte använder pekarobjektet. På grund av detta måste några nya händelsehanterare och metoder läggas till.

Lägga till stöd för gamepad

För det här spelet läggs gamepad-stöd till av anrop till Windows.Gaming.Input API:er. Den här uppsättningen API:er ger åtkomst till spelstyrenhetsindata som racinghjul och flygpinnar.

Följande kommer att vara våra gamepad-kontroller.

Användarindata Åtgärd
Vänster analog spak Flytta spelare
Höger analog spak Ändra rotationen (höjd och gir) i kameravyn
Höger utlösare Avfyra en sfär
Start-/menyknappen Pausa eller återuppta spelet

I metoden InitWindow lägger vi till två nya händelser för att avgöra om en spelplatta har lagts till eller tagits bort. Dessa händelser uppdaterar egenskapen m_gamepadsChanged. Detta används i metoden UpdatePollingDevices för att kontrollera om listan över kända gamepads har ändrats.

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

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

Anmärkning

UWP-appar kan inte ta emot indata från en spelkontrollant medan appen inte är i fokus.

Metoden UppdateraPollningsEnheter (UpdatePollingDevices)

UpdatePollingDevices-metoden för MoveLookController instans kontrollerar omedelbart om en spelplatta är ansluten. I så fall påbörjar vi avläsningen av dess tillstånd med Gamepad.GetCurrentReading. Detta returnerar GamepadReading struct, så att vi kan kontrollera vilka knappar som har klickats eller tumpinnar flyttats.

Om spelets tillstånd är WaitForInputlyssnar vi bara efter start-/menyknappen för kontrollanten så att spelet kan återupptas.

Om det är Aktivkontrollerar vi användarens indata och avgör vilken åtgärd som krävs i spelet. Om användaren till exempel flyttade den vänstra analoga pinnen i en specifik riktning låter detta spelet veta att vi måste flytta spelaren i den riktning som pinnen flyttas. För att något ska hända måste förflyttningen av pinnen i en specifik riktning registreras som större än radien för dödzon; annars händer ingenting. Denna döda zonradie är nödvändig för att förhindra "drifting", vilket är när kontrollern plockar upp små rörelser från spelarens tumme när den vilar på styrspaken. Utan döda zoner kan kontrollerna verka för känsliga för användaren.

Tumsticksindata är mellan -1 och 1 för både x- och y-axeln. Följande konstant anger radien för den döda tumstickszonen.

#define THUMBSTICK_DEADZONE 0.25f

Med den här variabeln börjar vi sedan bearbeta användbara tumsticksindata. Förflyttning skulle ske med ett värde från [-1, -.26] eller [.26, 1] på någon av axlarna.

död zon för tumpinnar

Den här delen av UpdatePollingDevices-metoden hanterar vänster och höger tumspakar. Varje pinnes X- och Y-värden kontrolleras för att se om de ligger utanför den döda zonen. Om en eller båda är det uppdaterar vi motsvarande komponent. Om den vänstra tumsticken till exempel flyttas till vänster längs X-axeln lägger vi till -1 till komponenten x i m_moveCommand-vektorn. Den här vektorn används för att aggregera alla rörelser över alla enheter och används senare för att beräkna var spelaren ska röra sig.

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

På samma sätt som den vänstra pinnen styr rörelsen styr den högra pinnen kamerans rotation.

Det högra tumsticksbeteendet överensstämmer med beteendet för musrörelser i konfigurationen av mus- och tangentbordskontroll. Om pinnen ligger utanför den döda zonen beräknar vi skillnaden mellan den aktuella pekarpositionen och var användaren nu försöker leta. Den här ändringen i pekarpositionen (pekarenDelta) används sedan för att uppdatera höjdled och sidorotation av kamerans rotation som vi senare tillämpar i vår Update-metod. pekarenDelta vektorn kan verka bekant eftersom den också används i MoveLookController::OnPointerMoved-metoden för att hålla reda på ändringar i pekarens position för våra mus- och touchinmatningar.

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

Spelets kontroller skulle inte vara kompletta utan möjligheten att avfyra sfärer!

Den här UpdatePollingDevices-metoden kontrollerar också om rätt utlösare trycks in. Om så är fallet, vänds vår m_firePressed egenskap till sant, vilket signalerar till spelet att sfärer ska skjuta.

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

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

Uppdateringsmetoden

För att avsluta saker och ting går vi djupare in i metoden Update. Den här metoden sammanfogar alla rörelser eller rotationer som spelaren har gjort med alla indata som stöds för att generera en hastighetsvektor och uppdatera våra pitch- och yaw-värden för att vår spelloop ska få åtkomst.

Metoden Update startar genom att anropa UpdatePollingDevices för att uppdatera kontrollantens tillstånd. Den här metoden samlar också in indata från en spelplatta och lägger till dess rörelser i m_moveCommand-vektorn.

I vår metod Update utför vi sedan följande indatakontroller.

  • Om spelaren använder rektangeln för flyttstyrenheten fastställer vi sedan ändringen i pekarpositionen och använder den för att beräkna om användaren har flyttat pekaren från kontrollantens döda zon. Om vektor egenskapen m_moveCommand befinner sig utanför den döda zonen, uppdateras den sedan med värdet för den virtuella joysticken.
  • Om någon av indata för rörelsetangentbordet trycks in läggs värdet 1.0f eller -1.0f till i motsvarande komponent i m_moveCommand-vektorn–1.0f för framåt och -1.0f för bakåt.

När alla rörelseindata har beaktats kör vi sedan m_moveCommand vektorn genom vissa beräkningar för att generera en ny vektor som representerar spelarens riktning när det gäller spelvärlden. Vi tar sedan våra rörelser i förhållande till världen och tillämpar dem på spelaren som hastighet i den riktningen. Slutligen återställer vi m_moveCommand-vektorn till (0.0f, 0.0f, 0.0f) så att allt är redo för nästa spelram.

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

Nästa steg

Nu när vi har lagt till våra kontroller finns det en annan funktion som vi behöver lägga till för att skapa ett uppslukande spel: ljud! Musik och ljudeffekter är viktiga för alla spel, så låt oss diskutera lägga till ljud nästa.