www.codeworx.org/c++ tuts /Kongos rand() Tutorials, 3: Low-Level Timing

Tutorial 3: Low-Level Timing

Warum Timer?

Als ich meinen neuen Computer bekam (PIII 700Mhz), wollte ich unbedingt eines meiner Lieblingsspiele von meinem alten Computer, auch auf meinen Neuen spielen. Als ich jedoch das Spiel das erste Mal startete, war ich ziemlich überrascht. Das Spiel war um einiges schneller geworden, als auf meinem alten Computer. Der Grund hierfür war schnell gefunden: Das Spiel führte jedes Mal ein Update der Spielfiguren aus, wenn es neu gerendert (gezeichnet) wurde. Auf meinem alten Computer waren dies natürlich einige Frames weniger, als auf meinem Neuen. Hätte der Programmierer jedoch ein Timing verwendet, um die Figuren zu bewegen, wäre das Spiel auf meinem alten und auf meinem neuen Computer gleichschnell gewesen.

Timer sind also da, um Animationen auf jedem Computer gleichschnell ablaufen zu lassen.

Timer in einer Schleife

Die Verwendung von Timern ist eigentlich ganz einfach. Die erforderlichen Schritte sind:

1. Zeit herausfinden, die seit dem letzten Aufruf der Renderfunktion vergangen ist.

2. Alle Spielobjekte für die vergangene Zeit bewegen.

3. Die aktuelle Zeit speichern, um den Zeitunterschied zwischen den Aufrufen der Renderfunktion zu finden.

Der WM_TIMER

Viele, die schon ein bisschen Ahnung von der Windows Programmierung haben, denken jetzt sicher an die WM_TIMER Nachricht. Bei dieser Art des Timing verwendet man das Nachrichtensystem von Windows, um Timing zu realisieren. Dieser Ansatz ist für uns jedoch falsch. Die Priorität für die WM_TIMER Nachricht ist sehr gering. Stehen mehrere Nachrichten an, werden zuerst die verarbeitet, die eine höhere Priorität besitzen. Dadurch rutscht die WM_TIMER Nachricht immer weiter nach hinten. Dadurch ist diese Art des Timings sehr ungenau.

Zum Glück gibt es zwei Performance Timer, die für unsere Arbeit genau richtig sind.

Der Multimedia Timer

Der erste Timer ist in dem ‚mmsystem.h’ - Header definiert. Um diesen Timer zu verwenden, muss man diesen Header einfügen und die Bibliothek ‚winmm.lib’ zum Projekt hinzufügen.

DWORD timeGetTime();

Die Funktion timeGetTime() gibt uns Zugriff auf einen Timer der auf jedem PC auf der Welt verfügbar ist. Dieser Timer arbeitet auf die Millisekunde (1/1.000stel einer Sekunde) genau. Diese Funktion gibt uns zwar keinen Wert, mit dem man die Tageszeit herausfinden kann, jedoch gibt sie uns die Zeit zurück, seitdem Windows läuft. Und das ist alles was wir benötigen: Genaue Zeiten, die man vergleichen kann.

Der High Resolution Performance Counter

Der Performance Counter ist in der Hardware implementiert und läuft mit 3.19 Mhz. Das gibt uns eine Auflösung von ca. 1 Mikrosekunde (1/1.000.000stel einer Sekunde). Diesen Timer werden wir verwenden, wenn er zur Verfügung steht. Leider besitzen vor allem ältere Computer diesen Timer nicht. Darum müssen wir überprüfen, ob dieser Timer überhaupt zur Verfügung steht.

Um diesen Timer zu verwenden, brauchen wir insgesamt zwei Funktionen. Auch diese sind in dem Header ‚mmsystem.h’ definiert.

BOOL QueryPerformanceFrequency(LONGLONG *lpFrequency);

Diese Funktion erwartet als Parameter einen 64-bit Integer, indem die Frequenz des Timers gespeichert wird (Ticks pro Sekunde). Wenn diese Funktion einen anderen Wert als 0 zurückgibt, existiert ein Timer. Bei 0 konnte man keinen Hardware Timer finden. Diese Funktion muss man nur einmal bei der Initialisierung aufrufen.

Vielleicht wunderst du dich, warum wir die Frequenz des Timers benötigen. Wir wissen ja genau, dass er mit 3.19 Mhz läuft. Die Antwort ist, dass wir hiermit unser Programm sicher für die Zukunft machen. Vielleicht ändert sich die Auflösung des Timers einmal. Unser Programm jedoch läuft dann immer noch genau, da unsere Berechnungen relativ zu der Frequenz sind.

Die zweite Funktion müssen wir bei jedem Durchlauf der Renderfunktion aufrufen:

BOOL QueryPerformanceCounter(LONGLONG *lpTime);

Wie auch die erste Funktion, nimmt diese einen 64-bit Integer als Parameter und speichert die aktuelle Zeit in ihm. Wie auch bei dem Multimedia Timer, ist diese Zeit die System Zeit. Man kann nicht die Tageszeit durch diese Funktion herausfinden.

Schauen wir uns jetzt das ganze in einen Programm an. Der Code zeit die Initialisierung, die man treffen muss, um einen Timer zum laufen zu bringen.

LONGLONG Frequency, CurrentTime, LastTime;
double TimeElapsed, TimeScale;
bool CounterAvailable = false;
...
...
if (QueryPerformanceFrequency( (LARGE_INTEGER*) &Frequency) 
{
    CounterAvailable = true;
    TimeScale = 1.0/Frequency;
    QueryPerformanceCounter( (LARGE_INTEGER*) &LastTime);
}
else 
{
    LastTime = timeGetTime();
    TimeScale = 0.001;
}

Als erstes versuchen wir einen Hardware Counter zu initialisieren. Wenn die Funktion keinen 0 Wert zurückgibt, setzen wir eine Variable TimeScale. Wir dividieren 1.0 durch die Frequenz. Dadurch bekommen wir einen Mulitplikator, der die vergangene Zeit in Sekunden konvertiert. Wenn kein Hardware Counter existiert, verwenden wir den Multimedia Timer.

Um das Timing zu realisieren, müssen wir in der Renderfunktion folgenden Code aufrufen.

if (CounterAvailable) 
   QueryPerformanceCounter((LARGE_INTEGER*) &CurrentTime); 

else 
   CurrentTime = timeGetTime(); 
   TimeElapsed = (CurrentTime – LastTime) * TimeScale; 
   LastTime = CurrentTime; 


Das ist alles was nötig ist, um Timing zu realisieren. Beachte, die Variable TimeElapsed beinhaltet die vergangene Zeit in Sekunden. Also der Wert 0.5 wäre eine halbe Sekunde.

Copyright (c) by Kongo

Bei Fragen, Beschwerden, Wünschen schreib an kongo@codeworx.org

Tutorial vom 09.07.2001