Freigeben über


Hinzufügen von Steuerelementen

Hinweis

Dieses Thema ist Teil der Erstellen eines einfachen UWP-Spiels (Universelle Windows-Plattform) mit DirectX-Tutorial-Reihe. Das Thema unter diesem Link legt den Kontext für die Reihe fest.

[ Aktualisiert für UWP-Apps unter Windows 10. Artikel zu Windows 8.x finden Sie im Archiv. ]

Ein gutes UWP-Spiel (Universelle Windows-Plattform) unterstützt eine Vielzahl von Schnittstellen. Ein potenzieller Spieler kann Windows 10 auf einem Tablet ohne physische Tasten, auf einem PC mit einem Gamecontroller oder auf einem topmodernen Gaming-PC mit Hochleistungsmaus und Gaming-Tastatur verwenden. In unserem Spiel werden die Steuerelemente in der MoveLookController-Klasse implementiert. Diese Klasse aggregiert alle drei Eingabetypen (Maus und Tastatur, Toucheingabe und Gamepad) in einem einzigen Controller. Das Endergebnis ist ein First-Person-Shooter, der standardmäßige Bewegungs-/Blicksteuerungen verwendet, die mit mehreren Geräten funktionieren.

Hinweis

Weitere Informationen zu Steuerelementen finden Sie unter Bewegungs-/Blicksteuerungen für Spiele und Touchsteuerungen für Spiele.

Ziel

An diesem Punkt haben wir ein Spiel, das gerendert wird, aber wir können unseren Spieler nicht umherbewegen oder auf die Ziele schießen. Wir werden uns ansehen, wie unser Spiel Bewegungs-/Blicksteuerungen für First-Person-Shooter für die folgenden Eingabetypen in unserem UWP-DirectX-Spiel implementiert.

  • Maus und Tastatur
  • Toucheingabe
  • Gamepad

Hinweis

Wenn Sie den neuesten Spielcode für dieses Beispiel nicht heruntergeladen haben, gehen Sie zu Direct3D-Beispielspiel. Dieses Beispiel ist Teil einer großen Sammlung von UWP-Featurebeispielen. Anweisungen zum Herunterladen des Beispiels finden Sie unter Beispielanwendungen für die Windows-Entwicklung.

Gängige Verhaltensweisen von Steuerelementen

Touchsteuerungen und Maus-/Tastatursteuerungen weisen eine sehr ähnliche Kernimplementierung auf. In einer UWP-App ist ein Zeiger einfach ein Punkt auf dem Bildschirm. Sie können ihn verschieben, indem Sie die Maus bewegen oder den Finger auf dem Touchscreen bewegen. Daher können Sie sich für einen einzelnen Satz von Ereignissen registrieren und sich keine Gedanken darüber machen, ob der Spieler eine Maus oder einen Touchscreen verwendet, um den Zeiger zu bewegen und den Zeiger zu drücken.

Wenn die MoveLookController-Klasse im Beispielspiel initialisiert wird, wird sie für vier zeigerspezifische Ereignisse und ein mausspezifisches Ereignis registriert:

Ereignis BESCHREIBUNG
CoreWindow::PointerPressed Die linke oder rechte Maustaste wurde gedrückt (und gehalten), oder die Touchoberfläche wurde berührt.
CoreWindow::PointerMoved Die Maus wurde verschoben, oder es wurde eine Ziehaktion auf der Touchoberfläche ausgeführt.
CoreWindow::PointerReleased Die linke Maustaste wurde losgelassen, oder das Objekt, das die Touchoberfläche kontaktiert, wurde angehoben.
CoreWindow::PointerExited Der Zeiger wurde aus dem Hauptfenster bewegt.
Windows::Devices::Input::MouseMoved Die Maus wurde um eine bestimmte Distanz bewegt. Wir sind aber nur an Deltawerten der Mausbewegungen und nicht an der aktuellen x-y-Position interessiert.

Diese Ereignishandler werden so eingestellt, dass sie mit der Überwachung von Benutzereingaben beginnen, sobald der MoveLookController im Anwendungsfenster initialisiert wird.

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

    ...
}

Vollständiger Code für InitWindow kann auf GitHub angezeigt werden.

Um festzustellen, wann das Spiel auf bestimmte Eingaben lauscht, verfügt die MoveLookController-Klasse unabhängig vom Controllertyp über drei controllerspezifische Zustände:

State Beschreibung
None Dies ist der initialisierte Zustand für den Controller. Alle Eingaben werden ignoriert, da das Spiel keine Controllereingabe erwartet.
WaitForInput Der Controller wartet darauf, dass der Spieler eine Nachricht vom Spiel bestätigt, indem er entweder einen linken Mausklick, ein Touchereignis oder die Menüschaltfläche auf einem Gamepad verwendet.
Aktiv Der Controller befindet sich im aktiven Spielmodus.

WaitForInput-Zustand und Anhalten des Spiels

Das Spiel wechselt in den WaitForInput-Zustand, wenn das Spiel angehalten wurde. Dies geschieht, wenn der Spieler den Zeiger außerhalb des Hauptfensters des Spiels bewegt oder die Pausetaste drückt (die P-Taste oder die Start-Taste auf einem Gamepad). MoveLookController registriert den Tastendruck und informiert die Spielschleife, wenn sie die IsPauseRequested-Methode aufruft. Wenn IsPauseRequested jetzt true zurückgibt, ruft die Spielschleife WaitForPress für die MoveLookController-Instanz auf, um den Controller in den Zustand WaitForInput zu versetzen.

Sobald es sich im WaitForInput-Zustand befindet, verarbeitet das Spiel fast keine Eingabeereignisse des Spiels mehr, bis es zum Active-Zustand zurückkehrt. Die Ausnahme ist die Pausetaste, mit deren Betätigung das Spiel wieder zum aktiven Zustand zurückkehrt. Abgesehen von der Pausetaste, damit das Spiel wieder zum Active-Zustand zurückkehrt, muss der Spieler ein Menüelement auswählen.

Der „Active“-Zustand

Während des Active-Zustands verarbeitet die MoveLookController-Instanz Ereignisse von allen aktivierten Eingabegeräten und interpretiert die Absichten des Spielers. Daher aktualisiert sie die Geschwindigkeit und Blickrichtung der Ansicht des Spielers und teilt die aktualisierten Daten mit dem Spiel, nachdem Update aus der Spielschleife aufgerufen wurde.

Alle Zeigereingaben werden im Active-Zustand mit unterschiedlichen Zeiger-IDs nachverfolgt, die verschiedenen Zeigeraktionen entsprechen. Wenn ein PointerPressed-Ereignis empfangen wird, ruft MoveLookController den vom Fenster erstellten Zeiger-ID-Wert ab. Die Zeiger-ID stellt einen bestimmten Eingabetyp dar. Beispielsweise kann es auf einem Multitouchgerät mehrere verschiedene aktive Eingaben gleichzeitig geben. Die IDs werden verwendet, um nachzuverfolgen, welche Eingabe der Spieler verwendet. Wenn sich ein Ereignis im Bewegungsrechteck des Touchscreens befindet, wird eine Zeiger-ID zugewiesen, um Zeigerereignisse im Bewegungsrechteck nachzuverfolgen. Andere Zeigerereignisse im Feuerrechteck werden separat mit einer separaten Zeiger-ID nachverfolgt.

Hinweis

Eingaben von der Maus und dem rechten Ministick eines Gamepads verfügen auch über IDs, die separat behandelt werden.

Nachdem die Zeigerereignisse einer bestimmten Spielaktion zugeordnet wurden, ist es an der Zeit, die Daten zu aktualisieren, die das MoveLookController-Objekt mit der Hauptspielschleife teilt.

Beim Aufruf verarbeitet die Update-Methode im Beispielspiel die Eingabe und aktualisiert die Geschwindigkeits- und Blickrichtungsvariablen ( m_velocity und m_lookdirection), die die Spielschleife dann durch Aufrufen der öffentlichen Velocity- und LookDirection-Methoden abruft.

Hinweis

Weitere Details zur Update-Methode finden Sie später auf dieser Seite.

Die Spielschleife kann testen, ob der Spieler feuert, indem die IsFiring-Methode für die MoveLookController-Instanz aufgerufen wird. Der MoveLookController überprüft, ob der Spieler auf einem der drei Eingabetypen die Schusstaste gedrückt hat.

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

Betrachten wir nun die Implementierung der drei Steuerelementtypen im Detail.

Hinzufügen relativer Maussteuerelemente

Wenn Mausbewegungen erkannt werden, möchten wir diese Bewegung verwenden, um den neuen Neigungs- und Schwenkwinkel der Kamera zu bestimmen. Dazu implementieren wir relative Maussteuerelemente, bei denen wir den relativen Abstand behandeln, um den sich die Maus bewegt hat – das Delta zwischen dem Start der Bewegung und dem Stopp – im Gegensatz zur Aufzeichnung der absoluten x-y-Pixelkoordinaten der Bewegung.

Dazu erhalten wir die Änderungen in den X (horizontale Bewegung)- und Y (vertikale Bewegung)-Koordinaten, indem wir die Felder MouseDelta::X und MouseDelta::Y im Argumentobjekt Windows::Device::Input::MouseEventArgs::MouseDelta untersuchen, das vom MouseMoved-Ereignis zurückgegeben wird.

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

Hinzufügen der Touchunterstützung

Touchsteuerelemente eignen sich hervorragend für die Unterstützung von Benutzern mit Tablets. Dieses Spiel sammelt Touch-Eingaben, indem es bestimmte Bereiche des Bildschirms abgrenzt, die jeweils auf bestimmte Aktionen im Spiel ausgerichtet sind. Die Toucheingabe dieses Spiels verwendet drei Zonen.

Touchlayout verschieben

Die folgenden Befehle fassen das Verhalten unserer Touchsteuerung zusammen. Benutzereingabe | Aktion :------- | :-------- Bewegungsrechteck | Die Toucheingabe wird in einen virtuellen Joystick konvertiert, bei dem die vertikale Bewegung in Vorwärts-/Rückwärtspositionsbewegung übersetzt wird und horizontale Bewegung in Links-/Rechtspositionsbewegung übersetzt wird. Feuerrechteck | Eine Kugel feuern. Toucheingabe außerhalb des Bewegungs- und Feuerrechtecks | Ändern Sie die Drehung (Neigungs- und Schwenkwinkel) der Kameraansicht.

MoveLookController überprüft die Zeiger-ID, um zu bestimmen, wo das Ereignis aufgetreten ist, und führt eine der folgenden Aktionen aus:

  • Wenn das PointerMoved-Ereignis im Bewegungs- oder Feuerrechteck aufgetreten ist, aktualisieren Sie die Zeigerposition für den Controller.
  • Wenn das PointerMoved-Ereignis irgendwo im restlichen Bildschirm aufgetreten ist (definiert als Blicksteuerelemente), berechnen Sie die Änderung des Neigungs- und Schwenkwinkels des Blickrichtungsvektors.

Sobald wir unsere Touchsteuerungen implementiert haben, zeigen die Rechtecke, die wir zuvor mit Direct2D gezeichnet haben, den Spielern an, wo sich die Bewegungs-, Feuer- und Blickzonen befinden.

Touchsteuerelemente

Sehen wir uns nun an, wie wir jedes Steuerelement implementieren.

Bewegungs- und Feuercontroller

Das Bewegungscontrollerrechteck im unteren linken Quadranten des Bildschirms wird als Steuerkreuz verwendet. Wenn Sie den Daumen innerhalb dieses Bereichs nach links und rechts bewegen, bewegt sich der Spieler nach links und rechts, während die Kamera mit Daumenbewegungen nach oben und unten vor und zurück bewegt wird. Nach der Einrichtung feuert das Tippen auf den Feuercontroller im unteren rechten Quadranten des Bildschirms eine Kugel ab.

Die Methoden SetMoveRect und SetFireRect erstellen unsere Eingaberechtecke, wobei zwei 2D-Vektoren verwendet werden, um die Positionen der oberen linken und unteren rechten Ecke des Rechtecks auf dem Bildschirm anzugeben.

Die Parameter werden dann m_fireUpperLeft und m_fireLowerRight zugewiesen, die uns helfen, zu bestimmen, ob der Benutzer den Bildschirm innerhalb der Rechtecke berührt.

m_fireUpperLeft = upperLeft;
m_fireLowerRight = lowerRight;

Wenn die Größe des Bildschirms geändert wird, werden diese Rechtecke auf die entsprechende Größe neu gezeichnet.

Nachdem wir nun unsere Steuerelemente abgegrenzt haben, ist es an der Zeit, zu bestimmen, wann ein Benutzer sie tatsächlich verwendet. Dazu richten wir einige Ereignishandler in der MoveLookController::InitWindow-Methode dafür ein, wenn der Benutzer den Zeiger drückt, bewegt oder loslässt.

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

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

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

Zunächst bestimmen wir mit der OnPointerPressed-Methode, was passiert, wenn der Benutzer anfänglich innerhalb des Bewegungs- oder Feuerrechtecks drückt. Hier überprüfen wir, wo er ein Steuerelement berührt und ob sich ein Zeiger bereits in diesem Controller befindet. Wenn dies der erste Finger ist, der das jeweilige Steuerelement berührt, gehen wir wie folgt vor.

  • Speichern Sie die Position des Touchdowns in m_moveFirstDown oder m_fireFirstDown als 2D-Vektor.
  • Weisen Sie die Zeiger-ID m_movePointerID oder m_firePointerID zu.
  • Setzen Sie das entsprechende InUse-Flag (m_moveInUse oder m_fireInUse) auf true, da wir jetzt über einen aktiven Zeiger für dieses Steuerelement verfügen.
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;
                ...
            }
        }
        ...

Nachdem wir nun festgestellt haben, ob der Benutzer eine Bewegungs- oder Feuersteuerung berührt, sehen wir, ob der Spieler Bewegungen mit dem gedrückten Finger ausführt. Mit der MoveLookController::OnPointerMoved-Methode überprüfen wir, welcher Zeiger bewegt wurde, und speichern dann seine neue Position als 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;
    }
    ...

Sobald der Benutzer seine Gesten innerhalb der Steuerelemente vorgenommen hat, gibt er den Zeiger frei. Mit der MoveLookController::OnPointerReleased-Methode bestimmen wir, welcher Zeiger freigegeben wurde, und führen eine Reihe von Zurücksetzungen durch.

Wenn das Bewegungssteuerelement losgelassen wurde, gehen wir wie folgt vor.

  • Setzen Sie die Geschwindigkeit des Spielers in alle Richtungen auf 0, um zu verhindern, dass er sich im Spiel bewegt.
  • Wechseln Sie m_moveInUse auf false, da der Benutzer den Bewegungscontroller nicht mehr berührt.
  • Setzen Sie die Bewegungszeiger-ID auf 0, da im Bewegungscontroller kein Zeiger mehr vorhanden ist.
if (pointerID == m_movePointerID)
{
    // Stop on release.
    m_velocity = XMFLOAT3(0, 0, 0);
    m_moveInUse = false;
    m_movePointerID = 0;
}

Wenn das Feuersteuerelement losgelassen wurde, wechseln wir das m_fireInUse-Flag auf false und die Feuerzeiger-ID auf 0, da es keinen Zeiger mehr in der Feuersteuerung gibt.

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

Blickcontroller

Wir behandeln Zeigerereignisse für Touchgeräte für die nicht verwendeten Bereiche des Bildschirms als den Blickcontroller. Wenn Sie den Finger durch diese Zone ziehen, ändert sich der Neigungs- und Schwenkwinkel (Drehung) der Spielerkamera.

Wenn das MoveLookController::OnPointerPressed-Ereignis auf einem Touchgerät in diesem Bereich ausgelöst wird und der Spielzustand auf Activegesetzt ist, wird ihm eine Zeiger-ID zugewiesen.

// 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 weist der MoveLookController die Zeiger-ID für den Zeiger, der das Ereignis ausgelöst hat, einer bestimmten Variablen zu, die dem Blickbereich entspricht. Im Fall einer Berührung im Blickbereich wird die m_lookPointerID-Variable auf die Zeiger-ID gesetzt, die das Ereignis ausgelöst hat. Außerdem wird eine boolesche Variable, m_lookInUse, festgelegt, um anzugeben, dass das Steuerelement noch nicht losgelassen wurde.

Sehen wir uns nun an, wie das Beispielspiel das PointerMoved-Touchscreen-Ereignis handhabt.

Innerhalb der MoveLookController::OnPointerMoved-Methode überprüfen wir, welche Art von Zeiger-ID dem Ereignis zugewiesen wurde. Wenn es m_lookPointerID ist, berechnen wir die Änderung der Position des Zeigers. Anschließend wird dieses Delta verwendet, um zu berechnen, wie viel sich die Drehung ändern soll. Schließlich befinden wir uns an einem Punkt, an dem wir die m_pitch und m_yaw aktualisieren können, um die Spielerdrehung zu ändern.

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

Der letzte Teil, den wir betrachten, ist, wie das Beispielspiel das PointerReleased-Touchscreen-Ereignis handhabt. Nachdem der Benutzer die Touchgeste beendet und den Finger vom Bildschirm entfernt hat, wird MoveLookController::OnPointerReleased initiiert. Wenn die ID des Zeigers, der das PointerReleased-Ereignis ausgelöst hat, die ID des zuvor aufgezeichneten Bewegungszeigers ist, setzt MoveLookController die Geschwindigkeit auf 0, da der Spieler den Blickbereich nicht mehr berührt.

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

Hinzufügen von Maus- und Tastaturunterstützung

Dieses Spiel verfügt über das folgende Steuerelementlayout für Tastatur und Maus.

Benutzereingabe Aktion
W Spieler vorwärts bewegen
H Spieler nach links bewegen
E Spieler rückwärts bewegen
S Spieler nach rechts bewegen
X Ansicht nach oben bewegen
Leertaste Ansicht nach unten bewegen
P Das Spiel anhalten
Mausbewegung Ändern der Drehung (Neigungs- und Schwenkwinkel) der Kameraansicht
Linke Maustaste Feuern einer Kugel

Um die Tastatur zu verwenden, registriert das Beispielspiel zwei neue Ereignisse: CoreWindow::KeyUp und CoreWindow::KeyDown, innerhalb der MoveLookController::InitWindow-Methode. Diese Ereignisse handhaben das Drücken und Loslassen einer Taste.

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

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

Die Handhabung der Maus unterscheidet sich etwas von der der Fingereingabesteuerung, obwohl sie einen Zeiger verwendet. Um das Steuerelementlayout auszurichten, dreht MoveLookController die Kamera, wenn die Maus bewegt wird, und feuert, wenn die linke Maustaste gedrückt wird.

Dies wird in der OnPointerPressed-Methode des MoveLookController gehandhabt.

In dieser Methode überprüfen wir, welche Art von Zeigergerät mit der Windows::Devices::Input::PointerDeviceType-Enumeration verwendet wird. Wenn das Spiel Active ist und der PointerDeviceType nicht Touch ist, wird davon ausgegangen, dass es sich um eine Mauseingabe handelt.

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;

Wenn der Spieler das Drücken einer der Maustasten beendet, wird das CoreWindow::PointerReleased-Mausereignis ausgelöst, wobei die MoveLookController::OnPointerReleased-Methode aufgerufen wird und die Eingabe abgeschlossen ist. An diesem Punkt werden keine Kugeln mehr gefeuert, wenn die linke Maustaste gedrückt wurde und jetzt losgelassen wird. Da der Blick immer aktiviert ist, verwendet das Spiel weiterhin denselben Mauszeiger, um die laufenden Blickereignisse zu verfolgen.

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;

Sehen wir uns nun den letzten Steuerungstyp an, den wir unterstützen: Gamepads. Gamepads werden getrennt von den Touch- und Maussteuerelementen behandelt, da sie das Zeigerobjekt nicht verwenden. Aus diesem Grund müssen einige neue Ereignishandler und Methoden hinzugefügt werden.

Hinzufügen von Gamepad-Unterstützung

Für dieses Spiel wird die Gamepadunterstützung durch Aufrufe der Windows.Gaming.Input-APIs hinzugefügt. Dieser Satz von APIs bietet Zugriff auf Gamecontrollereingaben wie Rennlenkräder und Flugsticks.

Nachfolgend finden Sie unsere Gamepad-Steuerelemente.

Benutzereingabe Aktion
Linker Analogstick Spieler bewegen
Rechter Analogstick Ändern der Drehung (Neigungs- und Schwenkwinkel) der Kameraansicht
Rechter Auslöser Feuern einer Kugel
„Start/Menü“-Taste Anhalten oder Fortsetzen des Spiels

In der InitWindow-Methode fügen wir zwei neue Ereignisse hinzu, um festzustellen, ob ein Gamepad hinzugefügt oder entfernt wurde. Diese Ereignisse aktualisieren die m_gamepadsChanged-Eigenschaft. Dies wird in der UpdatePollingDevices-Methode verwendet, um zu überprüfen, ob sich die Liste der bekannten Gamepads geändert hat.

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

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

Hinweis

UWP-Apps können keine Eingaben von einem Gamecontroller empfangen, während sich die App nicht im Fokus befindet.

Die UpdatePollingDevices-Methode

Die UpdatePollingDevices-Methode der MoveLookController-Instanz überprüft sofort, ob ein Gamepad verbunden ist. Wenn ja, beginnen wir mit dem Lesen des Zustands mit Gamepad.GetCurrentReading. Dadurch wird die GamepadReading-Struktur zurückgegeben, sodass wir überprüfen können, auf welche Schaltflächen geklickt oder ob die Ministicks bewegt wurden.

Wenn der Zustand des Spiels WaitForInput lautet, lauschen wir nur auf die Start-/Menüschaltfläche des Controllers, damit das Spiel fortgesetzt werden kann.

Wenn er Active ist, überprüfen wir die Eingabe des Benutzers und bestimmen, welche Spielaktion ausgeführt werden muss. Wenn der Benutzer beispielsweise den linken Analogstick in eine bestimmte Richtung bewegt hat, teilt dies dem Spiel mit, dass wir den Spieler in die Richtung bewegen müssen, in die der Stick bewegt wird. Die Bewegung des Sticks in eine bestimmte Richtung muss als größer als der Radius der inaktiven Zone registriert werden. Andernfalls geschieht nichts. Dieser Radius der inaktiven Zone ist erforderlich, um „Driften“ zu verhindern. Dies ist der Fall, wenn der Controller kleine Bewegungen vom Daumen des Spielers aufnimmt, während er auf dem Stick ruht. Ohne inaktive Zonen kann die Steuerung für den Benutzer zu sensibel erscheinen.

Die Ministickeingabe liegt zwischen -1 und 1 für die x- und die y-Achse. Die folgende Konstante gibt den Radius des inaktiven Ministicks an.

#define THUMBSTICK_DEADZONE 0.25f

Mit dieser Variablen beginnen wir dann mit der Verarbeitung von aktionenfähigen Ministickeingaben. Die Bewegung würde mit einem Wert von [-1, -0,26] oder [0,26, 1] auf beiden Achsen auftreten.

inaktive Zone für Ministicks

Dieser Teil der UpdatePollingDevices-Methode handhabt die linken und rechten Ministicks. Die X- und Y-Werte jedes Sticks werden überprüft, um festzustellen, ob sie sich außerhalb der inaktiven Zone befinden. Wenn die für einen oder beide der Fall ist, aktualisieren wir die entsprechende Komponente. Wenn beispielsweise der linke Ministick entlang der X-Achse nach links verschoben wird, fügen wir der x-Komponente des m_moveCommand-Vektors -1 hinzu. Dieser Vektor wird verwendet, um alle Bewegungen auf allen Geräten zu aggregieren und später zu berechnen, wohin sich der Spieler bewegen soll.

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

Ähnlich wie der linke Stick die Bewegung steuert, steuert der rechte Stick die Drehung der Kamera.

Das Verhalten des rechten Ministicks entspricht dem Verhalten der Mausbewegung in unserem Maus- und Tastatursteuerungs-Setup. Wenn sich der Stick außerhalb der inaktiven Zone befindet, berechnen wir die Differenz zwischen der aktuellen Zeigerposition und der Stelle, die der Benutzer nun anschauen möchte. Diese Änderung der Zeigerposition (pointerDelta) wird dann verwendet, um den Neigungs- und Schwenkwinkel der Kameradrehung zu aktualisieren, der später in unserer Update-Methode angewendet wird. Der pointerDelta-Vektor sieht möglicherweise vertraut aus, da er auch in der MoveLookController::OnPointerMoved-Methode verwendet wird, um die Änderung der Zeigerposition für unsere Maus- und Toucheingaben nachzuverfolgen.

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

Die Steuerelemente des Spiels wären nicht vollständig, ohne die Fähigkeit, Kugeln zu feuern!

Diese UpdatePollingDevices-Methode überprüft auch, ob der rechte Trigger gedrückt wird. Wenn dies der Fall ist, wird unsere m_firePressed-Eigenschaft auf „true“ gesetzt und signalisiert dem Spiel, dass Kugeln gefeuert werden sollen.

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

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

Die Update-Methode

Um zum Schluss zu kommen, befassen wir uns mit der Update-Methode. Mit dieser Methode werden alle Bewegungen oder Drehungen zusammengeführt, die der Spieler mit allen unterstützten Eingaben erstellt hat, um einen Geschwindigkeitsvektor zu generieren und unsere Neigungs- und Schwenkwerte zu aktualisieren, sodass unsere Spielschleife auf sie zugreifen kann.

Die Update-Methode fängt an, indem sie UpdatePollingDevices aufruft, um den Zustand des Controllers zu aktualisieren. Diese Methode sammelt auch alle Eingaben von einem Gamepad und fügt seine Bewegungen zum m_moveCommand-Vektor hinzu.

In unserer Update-Methode führen wir dann die folgenden Eingabeprüfungen aus.

  • Wenn der Spieler das Bewegungscontrollerrechteck verwendet, bestimmen wir dann die Änderung der Zeigerposition und verwenden diese, um zu berechnen, ob der Benutzer den Zeiger aus dem inaktiven Bereich des Controllers bewegt hat. Wenn er sich außerhalb des inaktiven Bereichs befindet, wird die m_moveCommand-Vektoreigenschaft dann mit dem Wert des virtuellen Joysticks aktualisiert.
  • Wenn eine der Bewegungstastatureingaben betätigt wird, wird ein Wert von 1.0f oder -1.0f in der entsprechenden Komponente des m_moveCommand-Vektors hinzugefügt –1.0f für vorwärts und -1.0f für rückwärts.

Nachdem alle Bewegungseingaben berücksichtigt wurden, lassen wir dann den m_moveCommand-Vektor durch einige Berechnungen laufen, um einen neuen Vektor zu generieren, der die Richtung des Spielers in Bezug auf die Spielwelt darstellt. Dann nehmen wir unsere Bewegungen in Bezug auf die Welt und wenden sie auf den Spieler als Geschwindigkeit in diese Richtung an. Schließlich setzen wir den m_moveCommand-Vektor auf (0.0f, 0.0f, 0.0f) zurück, sodass alles für den nächsten Spielframe bereit ist.

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ächste Schritte

Nachdem wir nun unsere Steuerelemente hinzugefügt haben, müssen wir ein weiteres Feature hinzufügen, das wir benötigen, um ein immersives Spiel zu entwickeln: Ton! Musik- und Soundeffekte sind für jedes Spiel wichtig. Lassen Sie uns also als Nächstes das Hinzufügen von Ton besprechen.