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