Worklog RK Rocket #2: Maus, Tastatur, Gamepad

Für RK Rocket habe ich damit begonnen, das Eingabe-System von Seeing# vernünftig auszuprogrammieren. Eingabesystem bedeutet in diesem Fall, dass Seeing# im Hintergrund automatisch die Status-Werte der angebundenen Eingabegeräte unabhängig von der aktuellen Oberflächen-Technologie (WPF, Win.Forms, WinRT) abfragt, in ein gemeinsames Format bring und dann an die aktuellen Spiele-Komponenten weiterleitet. Das klingt erst einmal einfach, ist aber nicht ganz zu unterschätzen. Probleme kommen etwa daher, weil auch jede Oberflächen-Technologie in Windows seine API ein bisschen anders bereitstellt. Weiterhin macht die Architektur von Seeing# selbst die Aufgabe nicht einfacher, da konsequent auf Multithreading, Multi-View und theoretisch auch mehrere Szenen gesetzt wird.

Zunächst einmal musste ich mir überlegen, wie Seeing# Eingabedaten für Spiele-Komponenten eigentlich bereitstellen sollte. Wenn man klassisch Windows-Oberflächen wie Xaml anprogrammiert, würde man sich i. d. R. an die Eingabe-Events hängen – entweder per Code oder per Trigger. Bei einem Spiel erinnere ich mich noch an ältere Tage, bei denen ich relativ viel mit BlitzBasic gemacht habe. Dort war das Abfragen der Eingabe-Geräte nach dem Polling-Prinzip umgesetzt. Man frägt also regelmäßig ab, welchen Status die Eingabegeräte gerade haben. Für ein Spiel macht dieses Prinzip sehr viel Sinn, da es sich technisch um eine Endlosschleife handelt, welche Durchlauf für Durchlauf den Status der Spiele-Objekte um je einen Schritt weiterrechnet. Akademisch könnte man jetzt “Kontinuierliche Simulation mit diskreten Zeitintervallen” dazu sagen [1]. In jedem dieser Schleifendurchläufe frägt das Spiel die aktuell relevanten Status-Werte von den Eingabegeräten ab und reagiert entsprechend. Ein Beispiel, wie es BlitzBasic damals gelöst hat, ist hier zu finden [2]. Bei RK Rocket habe ich es letzten Endes sehr ähnlich umgesetzt, was man aus folgenden Codeausschnitt aus der Player-Klasse sehen kann [3].

/// <summary>
/// Updates this object.
/// </summary>
/// <param name="updateState">State of the update.</param>
protected override void UpdateInternal(SceneRelatedUpdateState updateState)
{
    // Get input states
    MouseOrPointerState mouseState = updateState.DefaultMouseOrPointer;
    GamepadState gamepadState = updateState.DefaultGamepad;
    KeyboardState keyboardState = updateState.DefaultKeyboard;

    // Initialize variables for input control
    bool isFireHit = false;
    float moveDistance = 0f;
    float maxMoveDistance = (float)updateState.UpdateTime.TotalSeconds * Constants.SIM_ROCKET_MAX_X_MOVEMENT;

    // Capture mouse state
    if (mouseState.IsButtonHit(MouseButton.Left)) { isFireHit = true; }
    if (mouseState.MoveDistanceRelative != Vector2.Zero)
    {
        float newMouseX = Constants.GFX_SCREEN_VPIXEL_WIDTH * mouseState.PositionRelative.X;
        moveDistance = newMouseX - m_xPos;
    }

    // Capture keyboard state
    if (keyboardState.IsConnected)
    {
        isFireHit |=
            keyboardState.IsKeyHit(WinVirtualKey.Space) ||
            keyboardState.IsKeyHit(WinVirtualKey.Enter);

        if (keyboardState.IsKeyDown(WinVirtualKey.Left) ||
            keyboardState.IsKeyDown(WinVirtualKey.A) ||
            keyboardState.IsKeyDown(WinVirtualKey.NumPad4))
        {
            moveDistance = -maxMoveDistance;
        }
        else if (keyboardState.IsKeyDown(WinVirtualKey.Right) ||
            keyboardState.IsKeyDown(WinVirtualKey.D) ||
            keyboardState.IsKeyDown(WinVirtualKey.NumPad6))
        {
            moveDistance = maxMoveDistance;
        }
    }
}

Die Update-Methode wird ca. 40-mal pro Sekunden aufgerufen und berechnet immer den jeweils nächsten Zustand des Spiels. An dieser Stelle wird dann beispielsweise gefragt: “Wurde der linke Maus-Button gerade angeklickt?”. Wenn ja, wird ein Geschoss erzeugt. Genau so wird es bei den anderen Eingabegeräten wie Maus, Tastatur und Gamepad auch gemacht. Die Status-Objekte, über die der Status hier jeweils abgefragt werden kann, wird von Seeing# im Hintergrund von den gerade angebundenen Oberflächen gesammelt.

Seeing# InputStates

Die Klassen, welche die Status-Werte der Eingabegeräte einsammeln, befinden sich im Namensraum SeeingSharp.Multimedia.Input [4]. Bezogen auf Maus und Tastatur sind diese Abhängig von der jeweiligen Oberflächentechnologie, in der Seeing# die Grafik ausgibt. In der Grafik unten ist das WinRT oder Win.Forms, weiterhin funktioniert aber natürlich auch WPF. Jede Technologie weicht hier etwas von den anderen ab, was zum Beispiel die genauen Situationen angeht, unter welchen Voraussetzungen die Ereignisse kommen. Ein weiteres Problem ist die Tatsache, dass je nach Technologie andere Enumerationen verwendet werden, um Mausbuttons oder Keyboard-Keys darzustellen. Die Anbindung des Gamepads weicht – glücklicherweise – etwas von den anderen ab, da es unabhängig von der Oberflächentechnologie ist. In Seeing# wird es daher nicht auf View-Ebene, sondern direkt im Kern der Hauptschleife abgefragt.

Wie diese Sachen technisch im Detail funktionieren, sieht man in den Coding-Dateien auf Github.

Verweise

  1. http://www.blitzforum.de/help/KeyDown
  2. http://www.uwe-pret.de/fabrikplanung/doku/asim-leitfaden.pdf
  3. …SeeingSharp/blob/master/Games/RKRocket/Game/_Entities/PlayerRocketEntity.cs
  4. https://github.com/RolandKoenig/SeeingSharp/tree/master/SeeingSharp.Multimedia/Input

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.