Spielschleife unter Windows

8

Ich habe verschiedene "grundlegende" Spieleschleifen gesehen, sowohl aus Büchern wie "Einführung in die 3D-Spielprogrammierung mit DX11" oder dem Online-Tutorial (Rastertek), und wenn wir alle ausgefallenen Dinge zur Steuerung der Framerate und mehr entfernen, sehen sie alle ungefähr so ​​aus:

while (msg != WM_QUIT) // 
{
    if (PeekMessage(&msg,0,0,0,PM_REMOVE)
    {
        // Translate and dispatch message
    }
    else
    {
        // Do update, rendering and all the real game loop stuff
    }
}

Wenn Sie es so ausführen, funktioniert es perfekt, aber das, was ich nicht verstehe, ist das andere . Wenn ich naiv auf die Schleife schaue, könnte ich denken, dass wenn das System weiterhin Ereignisse sendet (Tastendrücke, Mausklicks und mehr), der else- Teil niemals ausgeführt wird, irre ich mich? (Ja !! Ich bin es, aber genau das ist der Punkt, ich verstehe nicht warum!).

Ich würde gerne verstehen, warum die Schleife so funktioniert. Wenn die Nachrichten eng mit dem Fenster verbunden wären (Größe ändern, verschieben, schließen), würde ich es verstehen, aber sie sind es nicht. Nehmen wir an, ich drücke eine beliebige Taste, eine WM_KEYDOWN-Nachricht wird wiederholt gesendet und eine Nachricht wird gespäht, sodass der else-Teil niemals ausgeführt werden kann

Ich habe nicht viel Erfahrung mit der Win32-Bibliothek und mir fehlt wahrscheinlich etwas ^^.

Vielen Dank für Ihre Zeit und viel Spaß beim Codieren!

Edoardo Sparkon Dominici
quelle

Antworten:

9

Ich habe nicht gesehen, dass in diesem Fall ein anderes verwendet wird (das von Ihnen erwähnte Rastertek-Tutorial verwendet kein anderes).

Meine Vermutung wäre, wenn Sie versuchen würden, die Größe Ihres Fensters zu ändern, würde das Spiel-Rendering einfrieren und beweisen, dass der else-Teil niemals ausgeführt wird, da die Größenänderungsnachricht ständig gesendet würde.

Eine gemeinsame Schleife hätte diese Struktur.

while (true) 
{
    while (PeekMessage(&msg,0,0,0,PM_REMOVE)
    {
        // Translate and dispatch message
    }

    if (msg.message == WM_QUIT)
    {
        break;
    }

   // Do update, rendering and all the real game loop stuff

}
Syntac_
quelle
Ja, tut mir leid, meine Schuld, das Rasterek-Tutorial macht genau das, was Sie sagen. Aber das von mir bereitgestellte Codebeispiel wird in "Beginn der 3D-Spielprogrammierung mit DX11" verwendet
Edoardo Sparkon Dominici
3
Das Vermeiden des Anderen kann zu Problemen führen, indem wichtige Windows-Meldungen wie die für die Maus- oder Tastatureingabe verzögert werden. Wenn Sie zwei Key-Down-Nachrichten in einem Frame erhalten, möchten Sie wahrscheinlich beide verarbeiten, bevor Sie sie beispielsweise rendern.
Adam
Ja sorry, es sollte eine Weile dauern.
Syntac_
Ein bisschen abseits des Themas hier, aber ich gehe davon aus, dass im obigen Beispiel die msg-Deklaration in der while (true) -Anweisung fehlt. Richtig? Ist dies nicht der Fall, schlägt die if-Anweisung fehl, wenn zwei oder mehr Nachrichten vorhanden sind und die WM_QUIT-Nachricht nicht die letzte in der Warteschlange ist.
Dadgron
1
PeekMessage gibt WM_QUIT immer erst zurück, wenn die Nachrichtenwarteschlange leer ist, wodurch garantiert wird, dass die letzte Nachricht tatsächlich WM_QUIT ist. msdn.microsoft.com/en-us/library/windows/desktop/…
Syntac_
4

Ich denke, Sie müssen verstehen, dass die Nachrichtenverarbeitung viel schneller abläuft als die Rate, mit der Nachrichten gesendet werden.

Für den von Ihnen angegebenen Beispielcode finden Sie hier eine Schätzung der Zeitabläufe:

while (msg != WM_QUIT) // 
{
    if (PeekMessage(&msg,0,0,0,PM_REMOVE)
    {
        // Translate and dispatch message
        // [A] --> This takes a very small amount of time (epsilon ms)
    }
    else
    {
        // Do update, rendering and all the real game loop stuff
        // [B] --> This takes way more time, ~16 ms at 60 fps for instance
    }
}

Ich habe die Dokumentation nicht danach durchsucht, aber Windows spammt wahrscheinlich nicht ständig Nachrichten: Sie müssen in regelmäßigen Abständen eingehen, sonst würden sie das System überladen. Möglicherweise verwenden sie die Tastaturaktualisierungsrate, wenn Sie eine Taste gedrückt halten. Vielleicht verwenden sie die Bildschirmaktualisierungsrate.

Nehmen wir an, alle t ms kommt eine neue Nachricht , die einigermaßen größer als epsilon ist . Der [A]Teil der Schleife verarbeitet alle gestapelten Nachrichten sehr schnell: Er wird für epsilon ms ausgeführt und erneut gestartet, sodass keine neuen Nachrichten eingehen können. Sobald alle Nachrichten verbraucht sind, wird der [B]Teil ausgeführt und neue Nachrichten werden wieder gestapelt.

Hier ist eine Illustration, um zu versuchen, meinen Standpunkt zu verdeutlichen, [An]die Verarbeitung der [Mn]von Windows gesendeten Nachrichten und [Bn]die tatsächlichen Frame-Updates:

Game:    --[A1][B1----------][A2][A3][A4][B2----------][A5][A6][A7][B3------->

            ^       ^       ^       ^       ^       ^       ^      ^      
Windows: ..[M1]....[M2]....[M3]....[M4]....[M5]....[M6]....[M7]....[M8]....

Hier können Sie sehen, dass [A]das Spiel noch genügend Zeit hat, um seine Frames zu aktualisieren , solange die Dauer der Aufenthalte im Vergleich zur Nachrichtenrate relativ klein ist (hier ist sie bereits sehr übertrieben).

Dies erklärt, warum eine solche Spielschleifenstruktur ein normales Spiel nicht verhindert. Aber die Lösung, die CoderScott in seiner Antwort vorgeschlagen hat, macht diesen Ausführungsfluss viel expliziter, daher sollte er meiner Meinung nach bevorzugt werden.

Laurent Couvidou
quelle
1

if / else in Ihrer Schleife verarbeitet Windows-Nachrichten vor allem. Die Schleife wird nicht zu lange blockiert (natürlich, wenn Ihr WndProc gut ist). Normalerweise gibt es nicht so viele Nachrichten. Ich glaube nicht, dass es einen besseren Weg gibt, um zu verhindern, dass das Fenster nicht mehr reagiert und FPS speichert. Wenn Sie Ihren Nachrichtenverarbeitungscode gut halten, denke ich, dass Ihre Schleife großartig sein wird.

Hier ist mein Code, ich glaube, er wird für Sie interessant sein und er ist ziemlich brauchbar. Aber eines Tages werde ich einen neuen codieren. Zumindest ist es viel besser als viele andere. Für ein spielbares Spiel benötigen Sie auf jeden Fall eine stabile Update-Rate (). Windows-Nachrichten werden es nicht brechen.

// Game loop paramters.
const int TICKS_PER_SECOND = 70; // Update() rate per second
const int SKIP_TICKS = 1000 / TICKS_PER_SECOND;
const int MAX_FRAMESKIP = 10;

Run () Funktion:

DWORD next_game_tick = GetTickCount();
int loops;
MSG msg = {0};
m_bIsGameRunning = true;
while( msg.message != WM_QUIT && m_bIsGameRunning )
{
    if( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
        g_pWindow->HandleMessage(msg); // translate and dispatch the message
    else
    {
        // *Update*
        loops = 0;
        while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP)
        {
            Update();

            next_game_tick += SKIP_TICKS;
            loops++;
        }

        // *Draw*
        Render();
    }
}
m_bIsGameRunning = false;
Loryan55
quelle
Schauen Sie sich die VS 2013 Win32- Spielvorlage und insbesondere StepTimer an .
Chuck Walbourn
1

Nein, du liegst nicht falsch. Eigentlich stimmt das - wenn es zu viele Ereignisse gibt, wird das andere nie oder zumindest nicht schnell genug ausgeführt. Wenn Ihre UI-Methoden zu viel Zeit benötigen, um das Ereignis zu verarbeiten, kann dies die OpenGL-Aktualisierung sperren. Zunächst sollten Sie überprüfen, warum die Verarbeitung von Ereignissen so viel Verarbeitung erfordert. Manchmal kann es jedoch zu Spam durch Ereignisse kommen (z. B. schnelle Mausbewegungen mit hohen Aktualisierungsraten), und selbst die einfache Verarbeitung benötigt zu viel Zeit, um mit dem OpenGL-Frame übereinzustimmen Timings aktualisieren.

Diese Schleife ist wirklich sehr schlecht und hat mich getroffen, als ich sie aus vielen Quellen im Internet kopiert habe (beachten Sie, dass dies ein falscher Ansatz ist ):

while( true ) {
    if ( PeekMessage( &m_msg, m_hWnd, 0, 0, PM_REMOVE ) ) {
        if ( WM_QUIT == m_msg.message ) {
            break;
        } else {
            TranslateMessage( &m_msg );
            DispatchMessage( &m_msg );
        }
    } else {
        processFrame();
    }
}

Das Richtige ist wie folgt:

while( !finished ) {
    DWORD currentTick = GetTickCount();
    DWORD endTick = currentTick + 1000/FRAMES_PER_SECOND;

    while (currentTick < endTick) {
        if ( PeekMessage( &m_msg, m_hWnd, 0, 0, PM_REMOVE ) ) {
            if ( WM_QUIT == m_msg.message ) {
                finished = true;
                break;
            } else {
                TranslateMessage( &m_msg );
                DispatchMessage( &m_msg );
            }
            currentTick = GetTickCount();
        } else {
            break;
        }
    }

    processFrame();
}

Wenn also die Prozedur für Verarbeitungsereignisse mit zusätzlicher Unterbrechung zu viel verbraucht, wird eine erzwungene Aktualisierung des OpenGL-Frames durchgeführt.

Marcin Skoczylas
quelle
-1

Ich arbeite an einer OpenGL Shader Engine von www.MarekKnows.com und das Framework ist wie folgt eingerichtet: Eine Engine-Klasse, die Teil einer statischen Bibliothek mit anderen hilfreichen und wichtigen Klassen ist. Dann gibt es das Hauptstartprojekt, in dem sich die Spielklasse zusammen mit den Spieleigenschaften befindet. Diese Struktur dient dazu, den gesamten Engine-Code vom Spielcode zu trennen. Das Schöne an diesem Setup ist, dass es wiederverwendet werden kann, um eine Vielzahl von Spielen zu erstellen, indem es einfach von der Engine lib erbt. Es ist noch nicht vollständig, da jede Woche ein anderes Segment oder Klassenobjekt aufgenommen wird.

Diese Hierarchie basiert auf der Ordner- und VS-Filterstruktur:

Projekt - Static Lib Engine

  • Animation
    • AnimationManager: Erbt von Singelton

  • Motor
    • AssetStorage: Erbt von Singleton - Speichert alle Spiel-Assets und verwaltet deren Speicher
    • Batch: Speichert Vao- und Vbo-Informationen in Batches, bevor sie auf der GPU gerendert werden
    • BatchManager: Erbt von Singleton - Verwaltet Stapel, fügt bei Bedarf zur Warteschlange hinzu und leert Buckets, wenn Stapel vollständig an die GPU gesendet werden
    • BlockProcess & BlockThread - Hilfsklassen für Prozesse und Threads
    • Engine: Erbt von Singleton - Abstrakte Basisklasse zum Spiel **
    • ExceptionHandler - Handles Alle Ausnahmen arbeiten eng mit der Logger- und Konsolenausgabe zusammen.
    • FontManager: Erbt von Singleton - verarbeitet alle in den Speicher zu ladenden Schriftarten und bereitet sie vor dem Rendern vor und speichert sie im AssetStorage.
    • Logger: Erbt von Singleton
    • Einstellungen: Erbt von Singleton Verschiedene für die Engine erforderliche Einstellungen: Physik-Aktualisierungsrate, isWindowed, randomNumGen, gamePixelSize, openglVersion usw.
    • Singleton - Basisklasse
    • Dienstprogramm - Dienstprogramm Funktionen, die nützlich sind - Konvertieren von Funktionen von Zahlen, Vektoren usw. in Zeichenfolgen und umgekehrt.
    • CommonStructs - Header Nur grundlegende Strukturen und Aufzählungen, die vielen Objekten gemeinsam sind

  • FileHandler
    • FileHandler: Basisklasse
    • MkoFileReader: Erbt von FileHandler - Liest MKO-Objektdateien ein und analysiert Daten
    • TextFileReader: Erbt von FileHandler - Liest Textdateien ein
    • TextFileWriter: Erbt von FileHandler - Schreibt in Textdateien
    • TextureFileReader: Erbt von FileHandler - Liest Texturdateien ein (derzeit unterstützt - tga & png)

  • GUI

    • Gui.h - Helfer * .h-Datei, um alle GUI-Elemente einfach einzuschließen
    • GuiCompositeElement: Erbt von GuiElement, abstrakte Basisklasse
    • GuiElement: Abstrakte Basisklasse
    • GuiImage: Erbt von GuiRenderable
    • GuiImageElement: Eigenständige Klasse, die mit Gui-Objekten verwendet wird
    • GuiLabel: Erbt von GuiText
    • GuiLayout: Erbt von GuiRenderable - Abstract Base Class
    • GuiLayoutAbsolute: Erbt von GuiLayout
    • GuiLayoutGrid: Erbt von GuiLayout
    • GuiLayoutSlices: Erbt von GuiLayout
    • GuiLayoutStack: Erbt von GuiLayout
    • GuiLoader: Wird in eine Textdatei geladen und analysiert Daten, um alle GUI-Elemente zu laden und zu rendern
    • GuiRenderable: Erbt von GuiCompositeElement - Abstract Base Class
    • GuiScreen: Erbt von GuiCompositeElement - Kann untergeordnete Knoten haben, hat aber keine Eltern, da dies das übergeordnete Element der obersten Ebene ist
    • GuiText: Erbt von GuiRenderable
    • GuiTextArea: Erbt von GuiText

    MKO - Diese MKO-Objekthierarchie stützt sich stark auf Vorlagen

    • Array2d: Erbt von BaseMko
    • BaseMko: Abstrakte Basisklasse
    • FontFile: Erbt von BaseMko
    • Image2d: Erbt von VisualMko
    • Surface3d: Erbt von BaseMko
    • VisualMko: Erbt von BaseMko, abstrakte Basisklasse

    Shader - Diese Shader-Hierarchie stützt sich stark auf Vorlagen

    • ShaderManager: Erbt von Singleton, verwaltet Shader
    • ShaderProgramSettings
    • ShaderVariable

Projekt starten - Spiel

  • Spiel: Erbt von der Engine
  • Eigenschaften: - Eigenschaften, die nur für das aktuelle Spiel gelten

Mit diesem Rahmen werde ich die Nachrichtenschleife für Sie demonstrieren

void Engine::start() {
    while( true ) {
        if ( PeekMessage( &m_msg, m_hWnd, 0, 0, PM_REMOVE ) ) {
            if ( WM_QUIT == m_msg.message ) {
                break;
            } else {
                TranslateMessage( &m_msg );
                DispatchMessage( &m_msg );
            }
        } else {
            processFrame();
        }
    }
}

Wenn Sie mehr von diesem Code sehen möchten, müssen Sie www.MarekKnows.com besuchen, um seine wunderbar gut strukturierten und klar erklärten Video-Tutorials für die Spieleentwicklung mit OpenGL zu erhalten. Aus Gründen des Kopierrechts werde ich nur kleine Ausschnitte als Vorschläge an andere weitergeben, die ihnen bei einem bestimmten Fallproblem helfen können. Ich kann dies nicht als meinen eigenen Quellcode beanspruchen, aber mit dem Befolgen seines Kursaufbaus wird alles persönlich von Hand geschrieben und persönlich getestet. Es wird nicht kopiert und eingefügt! Fast alles, was ich über Spieleprogrammierung gelernt habe, ist zusammen mit einigen anderen Quellen in Mareks Videos akkreditiert.

Ich kann versuchen, dies so gut wie möglich aus dem, was ich gelernt habe, und der Ausführung des Codes zu erklären. Irgendwo im Programm wird die Funktion Engine :: start () aufgerufen. Das erste, was passiert, ist, dass wir in eine Endlosschleife gehen; Innerhalb dieser while-Schleife prüfen wir, ob Nachrichten aus Windows gesehen wurden, und ob dies der Fall ist, prüfen wir, ob die WM_QUIT-Nachricht von einem anderen Ort aus gesetzt wurde. Wenn es gesetzt wurde, brechen wir aus der while-Schleife aus und kehren von dieser Funktion zurück. Die Anwendung wird den Speicher bereinigen, das Fenster zerstören und schließen. Andernfalls rufen wir dann TranslateMessage () & DispatchMessage () auf. Wenn PeekMessage () false zurückgibt, gehen wir in die else-Anweisung und rufen proccessFrame () auf.

Auf meinem Computer rendere ich mit diesem Framework grundlegende GUI-Objekte mit etwa 100 bis 120 Bildern pro Sekunde. Ich habe eine EVGA NVidia GeForce GTX 750Ti 2 GB mit Übertaktung und Kühlmittel mit zwei Lüftern unter Windows 7 64 Bit mit 8 GB RAM auf einem Intel Core 2 Quad Extreme 3.0 GHz.

Wir haben uns noch nicht mit umfangreichen Animationen, Physik, Partikelsystemen und Kollisionserkennungen befasst. Wir haben jedoch all das in einer seiner älteren Videoserien behandelt, in denen die ältere GameEngine auf OpenGL v1.0 basierte und alles auf der CPU erledigt wurde. Mit dieser Serie arbeiten wir jetzt an der Verwendung von Shadern und der GPU. Ich bin mir sicher, dass wir mit dieser Rahmenarbeit problemlos ein vollständiges 2D- oder 3D-Spiel auf einem ähnlichen System wie meinem mit vollen 40 - 60 FPS ausführen können.

Eine weitere gute Quelle zum Lernen ist www.GeometricTools.com und der Buchabschnitt . Vor ein paar Jahren habe ich ihr Buch gekauft: 3D Game Engine Design (2. Auflage) von David H. Eberly, Die Morgan Kaufmann-Serie für interaktive 3D-Technologie, und es ist eine großartige Lektüre. Ich hatte noch keine Gelegenheit, ihr neuestes Buch zu kaufen: GPGPU-Programmierung für Spiele und Wissenschaft von David H. Eberly, möchte es aber bald besitzen, da es mit der modernen Hardware- und Softwareentwicklung relevanter, aktueller und aktueller ist. Viel Glück!

Francis Cugler
quelle