Render Wissen - flexible Framerate Management

  • Hi,

    Vorwort:
    Da mittlerweile die GDI+ Projekte/Programme immer zahlreicher werden,
    sowie mich die Lust gepackt hat dieses Wissen zu teilen, in der Hoffnung
    auf weiterhin gute Skripte.

    Ebenfalls zu notieren:
    Ich habe dieses Thema, bzw. Technik einfach nur so getauft.
    Sie wird weltweit als Render Basis für jedwege Engine verwendet (es gibt auch andere Arten, aber dies ist die Geläufigste).


    Hauptteil:
    Programmiert man ein simples 2D Spiel, in welchem sich einfach nur eine Figur/Sprite
    bewegt.
    So würde man sporadisch gesehen folgenden AutoIt-Code schreiben:

    Spoiler anzeigen
    [autoit]


    Global Const $g_fStepCount = 5.0 ;soviele Pixel soll sich die Figur bewegen
    Global $g_fPlayerPositionX = 150.0, ;X-Position der Spielfigur
    $g_fPlayerPositionY = 150.0 ;Y-Position der Spielfigur

    [/autoit] [autoit][/autoit] [autoit]

    While Game_Running
    Render_Game() ;Rendert/Zeichnet das Spiel
    Sleep(xyz) ;Lässt das Programm pausieren, um die Framerate zu "drücken", Resourcenverbrauch zu mindern.
    WEnd

    [/autoit] [autoit][/autoit] [autoit]

    Func Render_Game()
    $g_fPlayerPositionX += $g_fStepCount
    $g_fPlayerPositionY += $g_fStepCount

    [/autoit] [autoit][/autoit] [autoit]

    Render_Player()
    EndFunc

    [/autoit]

    Obiger Code ist zwar nicht falsch, was mache man allerdings, wenn das Skript nur als decompile-resistente .exe
    vorliegt, sprich der Code nicht mehr veränderbar ist, der Sleep()-Wert konstant ist.

    Wozu Sorgen machen?
    Nun schreibt man ein Programm für einen PC mit 2.0 GHz und alles klappt, stellt sich diese Frage nicht.
    Sollte das Programm allerdings nun auf einen überschnellen PC mit 300.0 GHz ausgeführt werden,
    so wird der Render_Game()-Code entsprechend schneller ausgeführt.
    Aus "lächerlichen" 20 FPS können rasch 200 FPS werden.
    Dies bedeutet nun, dass sich die Spielfigur pro Frame um 5 Pixel bewegt, der Code 10x so oft aufgerufen
    wird. D.h. der Charakter bewegt sich nun statt 100 Pixel (=5*20) um 1000 Pixel (=5*20*10) pro Sekunde.
    Ein Fehler welcher nicht auftreten soll und darf.

    Wie umgehe ich dies?
    In dem man einen Timer in die Schleife einbaut und nach jedem Render() Aufruf prüfe wieviel Sekunden!!!
    vergangen sind.
    Diesen Wert übergibt man dann dem nächsten Render() Aufruf.

    Spoiler anzeigen
    [autoit]


    Global Const $g_fStepCount = 5.0 ;soviele Pixel soll sich die Figur bewegen
    Global $g_fPlayerPositionX = 150.0, ;X-Position der Spielfigur
    $g_fPlayerPositionY = 150.0 ;Y-Position der Spielfigur

    [/autoit] [autoit][/autoit] [autoit]

    Local $timer = 0
    Local $fSecsPassed = 0.0
    While Game_Running
    $timer = TimerInit() ;Initialisiere den Timer

    Render_Game($fSecsPassed) ;Rendert/Zeichnet das Spiel

    [/autoit] [autoit][/autoit] [autoit]

    $fSecsPassed = TimerDiff($timer) / 1000.0
    WEnd

    [/autoit] [autoit][/autoit] [autoit]

    Func Render_Game($fTime)
    $g_fPlayerPositionX += $g_fStepCount * $fTime
    $g_fPlayerPositionY += $g_fStepCount * $fTime

    [/autoit] [autoit][/autoit] [autoit]

    Render_Player() ;Zeichnet die Spielfigur
    EndFunc

    [/autoit]

    Obiger Code führt dazu, dass die Spielfigur im 1. Frame um 0 Pixel bewegt wird und ab dem 2. Frame um
    $g_fStepCount - Pixel pro Sekunde bewegt wird.
    Wie nun der Teil mit dem Sleep() gelöst wird bleibt jedem selbst überlassen.


    Und nun das Ganze mal mit einem Beispiel:

    Spoiler anzeigen


    Skript-Aufbau:

    Spoiler anzeigen


    Das Beispiel zeichnet einfach nur ein Rechteck, welches sich konstant um 35.0 Grad/Sekunde um seinen Mittelpunkt dreht.

    1.) Das "Hauptprogramm" wird in einer "main()"-Funktion gehalten.
    2.) Zum "Erstellen"/"Zerstören" werden "Create"/"Destroy"-Funktionen genutzt.
    3.) Zum "Zeichnen" eine "Draw()"-Funktion.
    4.) Zum "Bewegen" der Szene eine "Update()"-Funktion.
    5.) Rückgabewerte in obigen Funktionen sind immer: 0 - Fehler. 1 - Erfolg; Bei Fehlschlag enthält @error nähere Informationen.
    6.) Erweiterte Rückgabe, wird durch ByRef realisiert.
    7.) Berechnung der Rotationsmatrix wird in der "Render"-Funktion erledigt.
    8.) Berechnung der Anzahl der Grade, also um wieviel Grad sich das Rechteck beim nächsten "Render"-Aufruf rotieren soll, in der "Update"-Methode.
    9.) Handles welche einen Wert von 0 haben, müssen/sollen nicht Dispose'ed() werden.

    Skript:

    Spoiler anzeigen
    [autoit]


    #cs ----------------------------------------------------------------------------

    [/autoit] [autoit][/autoit] [autoit]

    AutoIt Version: 3.3.6.1
    Author: Dennis a.k.a Ealendil/CentuCore

    [/autoit] [autoit][/autoit] [autoit]

    Script Function:
    Example for "flexible Framerate Management".

    [/autoit] [autoit][/autoit] [autoit]

    Copyright:
    To nobody and all. At the same moment. ;)

    [/autoit] [autoit][/autoit] [autoit]

    #ce ----------------------------------------------------------------------------

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    #include <GDIPlus.au3> ;um überhaupt zeichnen zu können

    [/autoit] [autoit][/autoit] [autoit]

    Opt("MustDeclareVars", 1)

    [/autoit] [autoit][/autoit] [autoit]

    If $CMDLine[0] Then
    Local $TmpCMDLine[$CMDLine[0] - 1] ;erstellt ein 1-dimensionales Array mit sovielen "Spalten", wie es Command Line Parameter gibt.
    For $i = 1 To $CMDLine[0]
    $TmpCMDLine[$i - 1] = $CMDLine[$i] ;kopiere die Parameter von $CMDLine nach $TmpCMDLine
    Next
    main($CMDLine[0], $TmpCMDLine)
    Else
    main($CMDLine[0], Int(0))
    EndIf
    Exit(@error)

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    Func main($NumCMDParams, $CMDParams)

    [/autoit] [autoit][/autoit] [autoit]

    ;Daten für das Hauptfenster
    Local Const $GUIWidth = 500
    Local Const $GUIHeight = 500

    [/autoit] [autoit][/autoit] [autoit]

    ;Daten für das zu zeichnende Rechteck
    Local Const $fRectWidth = 150.0
    Local Const $fRectCenterX = $GUIWidth / 2.0
    Local Const $fRectCenterY = $GUIHeight / 2.0

    [/autoit] [autoit][/autoit] [autoit]

    Local $hGUI, $hFrontbufferGraphics, $hBackbufferBitmap, $hBackbufferGraphics
    Local $hTranslateMatrix = 0

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    ;Starte GDI+
    If Not _GDIPlus_Startup() Then Return SetError(1, 0, 0)

    [/autoit] [autoit][/autoit] [autoit]

    If Not Create("GDI+ Example by Ealendil", $GUIWidth, $GUIHeight, $hGUI, $hFrontbufferGraphics, $hBackbufferBitmap, $hBackbufferGraphics) Then

    [/autoit] [autoit][/autoit] [autoit]

    ;Fehler beim erstellen der Szene
    Destroy($hGUI, $hFrontbufferGraphics, $hBackbufferBitmap, $hBackbufferGraphics)
    _GDIPlus_Shutdown()
    Return SetError(-1, 0, 0)
    EndIf

    [/autoit] [autoit][/autoit] [autoit]

    ;berechne Offsets um das Rechteck korrekt zu verschieben
    ;Der Mittelpunkt des Rechtecks entspricht dem Mittelpunkt des Fensters
    Local Const $fTranslateOffsetX = $GUIWidth / 2.0
    Local Const $fTranslateOffsetY = $GUIHeight / 2.0

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    Local Const $fNumDegPerSec = 35.0
    Local $fDegToRotate = 0.0
    Local $fTime = 0.0
    Local $timer = TimerInit()
    While GUIGetMsg() <> (-3)

    [/autoit] [autoit][/autoit] [autoit]

    ;übermale/lösche Szene mit Weiß
    _GDIPlus_GraphicsClear($hBackbufferGraphics, 0xFFffFFff)

    [/autoit] [autoit][/autoit] [autoit]

    ;zeichne Szene in den Backbuffer
    Render($fTime, $hBackbufferGraphics, $fTranslateOffsetX, $fTranslateOffsetY, $fDegToRotate, $fNumDegPerSec, $fRectWidth)

    [/autoit] [autoit][/autoit] [autoit]

    ;präsentiere die gezeichnete Szene
    _GDIPlus_GraphicsDrawImage($hFrontbufferGraphics, $hBackbufferBitmap, 0, 0)

    [/autoit] [autoit][/autoit] [autoit]

    ;setze vergangene Sekunden und initialisiere den Timer neu
    $fTime = TimerDiff($timer) / 1000.0
    $timer = TimerInit()
    WEnd

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    Destroy($hGUI, $hFrontbufferGraphics, $hBackbufferBitmap, $hBackbufferGraphics)
    Return SetError(0, 0, 1)
    EndFunc

    [/autoit] [autoit][/autoit] [autoit]

    Func Render(Const $fSecsPassed, $hGraphics, Const $fTranslateOffsetX, Const $fTranslateOffsetY, ByRef $fDegToRotate, Const $fNumDegPerSec, Const $fRectWidth)

    [/autoit] [autoit][/autoit] [autoit]

    ;erstelle Rotations/Translations Matrix
    Local $l_hMatrix = _GDIPlus_MatrixCreate()
    If Not $l_hMatrix Then Return SetError(1, 0, 0)

    [/autoit] [autoit][/autoit] [autoit]

    ;berechne Anzahl an Grad, um welche das Rechteck in derzeitigen Frame rotiert werden soll
    $fDegToRotate += $fNumDegPerSec * $fSecsPassed
    _GDIPlus_MatrixTranslate($l_hMatrix, $fTranslateOffsetX, $fTranslateOffsetY)
    _GDIPlus_MatrixRotate($l_hMatrix, $fDegToRotate)

    [/autoit] [autoit][/autoit] [autoit]

    ;setze die berechnete Matrix und zeichne das Rechteck
    _GDIPlus_GraphicsSetTransform($hGraphics, $l_hMatrix)
    _GDIPlus_GraphicsDrawRect($hGraphics, -($fRectWidth / 2.0), -($fRectWidth / 2.0), $fRectWidth, $fRectWidth)

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    ;lösche die Matrix
    _GDIPlus_MatrixDispose($l_hMatrix)

    [/autoit] [autoit][/autoit] [autoit]

    Return SetError(0, 0, 1)
    EndFunc

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    ; #FUNCTION# ====================================================================================================================
    ; Name...........: Create
    ; Description ...: Creates the given resources
    ; Syntax.........: Create(Const $GUIName, Const $GUIWidth, Const $GUIHeight, ByRef $HGUIOut, ByRef $hFrontbufferOut, ByRef $hGUIBitmapOut, ByRef $hBackbufferOut)
    ; Parameters ....: $GUIName - The window's name.
    ; $GUIWidth - The width for the window.
    ; $GUIHeight - The height for the window.
    ; $hGUIOut - A handle to the created window.
    ; $hFrontbufferOut - A handle to a Graphics object.
    ; $hGUIBitmapOut - A handle to a Bitmap object.
    ; $hBackbufferOut - A handle to a Graphics object for "$hGUIBitmapOut"
    ; Return values .: Success - 1
    ; Failure - 0
    ; @error - 1 = Couldn't create the window-
    ; - 2 = Couldn't create the frontbuffer Graphics object.
    ; - 3 = Couldn't create the backbuffer Bitmap object.
    ; - 4 = Couldn't create the backbuffer Graphics object.
    ; Author ........: Dennis (Ealendil/CentuCore)
    ; ===============================================================================================================================
    Func Create(Const $GUIName, Const $GUIWidth, Const $GUIHeight, ByRef $hGUIOut, ByRef $hFrontbufferGraphicsOut, ByRef $hBackbufferBitmapOut, ByRef $hBackbufferGraphicsOut)
    $hGUIOut = 0
    $hFrontbufferGraphicsOut = 0
    $hBackbufferBitmapOut = 0
    $hBackbufferGraphicsOut = 0

    [/autoit] [autoit][/autoit] [autoit]

    ;erstelle das Hauptfenster
    $hGUIOut = GUICreate($GUIName, $GUIWidth, $GUIHeight)
    If Not $hGUIOut Then Return SetError(1, 0, 0)
    GUISetState(@SW_SHOW, $hGUIOut)

    [/autoit] [autoit][/autoit] [autoit]

    ;erstelle Frontbuffer Graphics Objekt
    $hFrontbufferGraphicsOut = _GDIPlus_GraphicsCreateFromHWND($hGUIOut)
    If Not $hFrontbufferGraphicsOut Then Return SetError(2, 0, 0)

    [/autoit] [autoit][/autoit] [autoit]

    ;erstelle Backbuffer Bitmap Objekt
    $hBackbufferBitmapOut = _GDIPlus_BitmapCreateFromGraphics($GUIWidth, $GUIHeight, $hFrontbufferGraphicsOut)
    If Not $hBackbufferBitmapOut Then Return SetError(3, 0, 0)

    [/autoit] [autoit][/autoit] [autoit]

    ;erstelle Backbuffer Graphics Objekt
    $hBackbufferGraphicsOut = _GDIPlus_ImageGetGraphicsContext($hBackbufferBitmapOut)
    If Not $hBackbufferGraphicsOut Then Return SetError(4, 0, 0)

    [/autoit] [autoit][/autoit] [autoit]

    Return SetError(0, 0, 1)
    EndFunc

    [/autoit] [autoit][/autoit] [autoit]

    ; #FUNCTION# ====================================================================================================================
    ; Name...........: Destroy
    ; Description ...: Destroys the given resources.
    ; Syntax.........: Destroy(ByRef $hGUI, ByRef $hFrontbufferGraphics, ByRef $hGUIBitmap, ByRef $hBackbufferGraphics)
    ; Parameters ....: $hGUI - Handle to a window
    ; $hFrontbufferGraphics - A handle to a Graphics object.
    ; $hGUIBitmap - A handle to a Bitmap object.
    ; $hBackbufferGraphics - A handle to a Graphics object.
    ; Return values .: Success - 1
    ; Failure - None!
    ; Author ........: Dennis (Ealendil/CentuCore)
    ; Remarks .......: Each parameter will be set to zero after deleting.
    ; ===============================================================================================================================
    Func Destroy(ByRef $hGUI, ByRef $hFrontbufferGraphics, ByRef $hBackbufferBitmap, ByRef $hBackbufferGraphics)

    [/autoit] [autoit][/autoit] [autoit]

    ;Zerstöre Backbuffer's Graphics Objekt
    If $hBackbufferGraphics Then
    _GDIPlus_GraphicsDispose($hBackbufferGraphics)
    $hBackbufferGraphics = 0
    EndIf

    [/autoit] [autoit][/autoit] [autoit]

    ;Zerstöre Backbuffer Bitmap Objekt - enthält die Daten des Backbuffers
    If $hBackbufferBitmap Then
    _GDIPlus_BitmapDispose($hBackbufferBitmap)
    $hBackbufferBitmap = 0
    EndIf

    [/autoit] [autoit][/autoit] [autoit]

    ;Zerstöre das Frontbuffer Graphics Objekt
    If $hFrontbufferGraphics Then
    _GDIPlus_GraphicsDispose($hFrontbufferGraphics)
    $hFrontbufferGraphics = 0
    EndIf

    [/autoit] [autoit][/autoit] [autoit]

    ;Zerstöre das Fenster
    If $hGUI Then
    GUIDelete($hGUI)
    $hGUI = 0
    EndIf

    [/autoit] [autoit][/autoit] [autoit]

    Return 1
    EndFunc

    [/autoit]


    Nachwort:
    Diese Technik soll helfen, dass Skripte auf jedem PC gleich "flüssig" laufen,
    also jede Figur sich gleich weit pro Tastendruck oder ähnliches bewegt.

    Ich hoffe ich habe es verständlich erklärt.
    Sollten Fragen auftreten, scheut euch nicht diese zu stellen.


    LG,
    Dennis a.k.a Ealendil

    EDIT: Beispiel hinzugefügt!

    5 Mal editiert, zuletzt von Ealendil (7. Mai 2012 um 11:41)

  • Informativ, wenn auch ein bisschen kurz ^^. Ich verwende das selbe Prinzip (wenn auch ausgefeilter) schon seit einiger Zeit in meinen animierten GDI+ Anwendungen. Siehe dazu meine Signatur ;).
    Die Sleep Funktion eignet sich allerdings eher nicht so gut für diese, im Millisekunden Bereich variierende, Zeitspanne. Mögliche Lösungen sind das Workaround von Marsi in seinem Thread dazu und die HighPrecisionSleep Funktion, welche im englischen Forum zu finden ist.

  • name22:
    1)Danke für dein Feedback.
    Kurz ist es, aber mMn. benötigt es nicht mehr.

    2)Versteh meine Frage nicht falsch.
    Inwiefern meinst du ausgefeilter?

    Kannte ZwDelayExecution() nur vom "HörenSagen", habe allerdings noch keine genaue Beschreibung zu dieser Funktion gefunden.
    Kennst du/jemand eine Detaillierte?

    Einmal editiert, zuletzt von Ealendil (6. Mai 2012 um 00:10)

  • Zitat

    Inwiefern meinst du ausgefeilter?


    Nunja.. Deine Beispiele bestehen ja nur aus Pseudocode und verwenden die Sleepfunktion, die dafür eher weniger geeignet ist. Ich verwende ZwDelayExecution() in ausführbaren Scripts. Das meinte ich ;).

    Zitat

    Kannte ZwDelayExecution() nur vom "HörenSagen", habe allerdings noch keine genaue Beschreibung zu dieser Funktion gefunden.
    Kennst du/jemand eine Detaillierte?


    Was verstehst du denn nicht bzw. was wüsstest du gern? ZwDelayExecution ist eine Funktion der ntdll.dll von Windows, die den Scriptablauf, durch einen Aufruf per DLLCall, für eine bestimmte Zeitspanne pausiert. Die Zeit wird in Einheiten von 0.1 Microsekunden angegeben. Also wenn ich 2500 Millisekunden pausieren will, dann gebe ich an die Funktion den Wert 2500*1000*10=25000000 weiter.

  • name22: Ich meinte die Parameter und möglichen Rückgabewerte von ZwDelayExecution.
    Das eine Funktion, welche "warten" im Namen trägt etwas dergleichen macht, dachte ich mir. ;)
    Trotzdem danke für deine Erklärung.

    @Skincke: Weiß nicht ob du meinst, dass du es durch mich eingebaut hast oder nicht.
    Falls ja, Freut mich, wenn ich helfen konnte.
    Falls nein, sei gewiss ich habs dir nicht gestohlen.

    Hab mir dein Update gerade angeschaut und gleich nen Post in deinem Thread geschrieben.

    3 Mal editiert, zuletzt von Ealendil (6. Mai 2012 um 01:04)

  • Moment :D
    Ich glaube ich habe irgend was hier nicht so ganz verstanden... :huh: Will mir der erste Post jetzt also sagen, dass das Ausführen der Funktion Sleep() mit dem Parameter 1000 für eine Pause von 1000ms auf einem 'schelleren' Rechner nun nicht 1000ms lang dauert??? 8| Worin liegt dann der Sinn der Funktion Sleep() wenn sie nicht auf jedem Rechner, egal wie schnell oder langsam, genau die gleiche Pause bewirkt?! Also ich weiß ja nicht wie Sleep() intern funktioniert, aber ich bin bisher eigentlich davon ausgegangen, dass 'in der Kiste' :D eine Art 'Taktgeber' existiert, welcher dann die Zeit vorgibt??! ?( Naja vielleicht hab ich mich auch einfach mal wieder 'verdacht'... :D

    LG
    Christoph :)

  • Die Funktion Sleep pausiert das Script an dieser Stelle mehr oder weniger genau für die angegebene Zeit (das sollte größtenteils unabhängig vom PC sein). Du übersiehst aber, dass alle anderen Funktionen in der Hauptschleife nicht wie durch Zauberei ohne Zeitverlust ausgeführt werden. Da steckt ein gewisser Rechenaufwand dahinter, der abhängig von der Hardware die Funktion mehr oder weniger verlangsamt. Sleep schläft aber trotzdem immer noch solange wie angegeben und berücksichtigt nicht den Zeitverlust der anderen Funktionen. Und um das zu kompensieren verwendet man ein System wie das aus diesem Thread.