Das perfekte Timing
-
Hallo liebe Leute,
ich bin auf der Suche nach DEM Timing für Games.
**Ansprüche an den Timer:
- hohe Auflösung/Genauigkeit (-> QueryPerformanceCounter())
- auf allen Rechnern soll das Spiel gleich schnell laufen
- Multiplayertauglichkeit
- keine Unsynchonitäten
- bei zu langsamen Spielern soll das Spiel laggen, nicht langsamer werden, die anderen Spieler sollen davon nicht gestört werden- das Spiel muss flüssig sein/aussehen
- es sollte möglichst wenig interpoliert werden müssen, wichtige Berechnungen (Kollision etc.) sollten so oft wie möglich ausgeführt werden
**
Falls ich noch wichtige Ansprüche an den Timer vergessen habe, dann sagt's bitte.Vielleicht kennt ihr ein Buch, einen Artikel oder ein Tutorial von dem ihr sagen könnt, dass dort ein sehr gut durchdachter Timer entwickelt/benutzt wird. Was ist zu vermeiden, was ist unbedingt notwendig etc. (gibt es verschiedene Methoden die einzelnen Punkte betreffend, wo liegen die Vor- und wo die Nachteile dieser Methoden).
Am Ende soll - wenn es ihn gibt - der perfekte Timer für alle erdenklichen Spiele herauskommen. Ich will mir 'ne richtig schöne Klasse schreiben (in Cpp), welche ich dann für alle meine folgenden Projekte benutzen kann.
Mal sehen ob das klappt, dann haut mal rein.
Btw: Ich habe hier im Forum auch schon ein paar gute Diskussionen gelesen, aber am Ende kam nie was Konkretes dabei heraus.
-
Ich glaube kaum, dass es DAS Timing für Games gibt. Irgendwo muss man immer Abstriche machen.
Multiplayertiming ist nicht unbedingt einfach, weil es meines Wissens nach keinen Weg gibt, Synchronisation zu verhindern. Die wird immer nötig sein.
Da müsste man auch mit zwei verschiedenen Ansätzen arbeiten. Der Server sollte einem anderen Ansatz folgen als die Clients, da der ja z.B. keine Grafik braucht und sich rein im I/O und Logik kümmern kann.
Die Clients wiederum werden die Grafik ja sehr wohl berücksichtigen müssen, dazu ihre eigenen Logikberechnungen und die Updates vom Server auch noch.Bin mal gespannt, was zu diesem Thema hier noch kommt, gerade von den Kings dieses Unterforums
-
Nimm die genauesten Funktionen zur Zeitmessung, die deine Plattform anbietet. Miss nicht öfter als notwendig. Akkumuliere nicht mehr Messergebnisse als nötig, damit Ungenauigkeiten sich nicht aufsummieren.
Benutze außerhalb der Zeitmessung ausschließlich abstrakte Zeiteinheiten ("Ticks") mit konstanter Länge, z. B. 1 Tick = 1/60 Sekunden.
Der Timer sollte sich der gewählten Rate so gut wie möglich annähern und Abweichungen so gut es geht kompensieren. Beachte dabei, dass du die Tickrate im Laufe der Entwicklung vielleicht anpassen willst.Siehe die Möglichkeit vor, verschiedene Programmteile mit unterschiedlichen Tickrates zu betreiben. Die Spielelogik muss nur wenige Male pro Sekunde aktualisiert werden, Physik und Animation hingegen sehr oft.
Vermeide es, Werte mit der vergangenen Zeit zu multiplizieren. Dein Timer sorgt schon dafür, dass deine Tickhandler möglichst exakt aufgerufen werden, du kannst also von einer konstanten Zeitspanne pro Durchlauf ausgehen.So hast du ein super flexibles Timing, mit dem man auch einfach mal die Zeit zentral beschleunigen oder verlangsamen kann. Oder auch mal fixe Timesteps einsetzen, die unabhängig von der realen Zeit mit einer festen Rate tickern, um ein ruckelfreies Video zu capturen.
dust schrieb:
- es sollte möglichst wenig interpoliert werden müssen
Sehe ich anders, gerade bei der Bildschirmdarstellung und Netzwerkübertragung kannst du mit Interpolation bzw. Extrapolation erheblich Performance und Bandbreite einsparen. Logik und Darstellung sind unterschiedliche Baustellen und werden unterschiedlich getaktet. Um die langsamere Logik flüssig darzustellen, kommst du um Interpolation nicht herum.
-
Hallo dust,
das sind interessante Anforderungen. Momentan leite ich die Entwicklung einer "modernen" Game-Engine, daher kenne ich diese Anforderungen genau.
http://ultimatespaceproject.de/Aber eins noch vorweg:
Ein Objekt bewegt sich stetig, es wird aber nur diskret dargestellt.
Andersrum gesehen bewegt sich ein Objekt nicht stetig, nur weil du es 1 Million mal pro Sekunde aktualisierst.Natürlich ist es gewünscht den View, die Darstellung so oft wie möglich zu aktualiseren, aber das kann man auch auf 100/200 Hz limitieren.
Die schon genannten "Ticks" ergeben sich aus dem Frame-Limit, dennTick-Dauer = 1 / FPS_Count
Dies beschreibt aber nur ein synchrones Spiel-Modell.
Ein Ereignis geschieht dann zu einem Tick und nicht zu einem Zeitpunkt.
Damit verringert sich die Zeit-Auflösung eben auf eine Tick-Dauer.Die Anzahl der Game-Ticks zu erhöhen ist hier meiner Meinung nach nicht die Lösung. Man muss das Spiel komplett asynchron konzipieren, d.h. die verschiedenen Spiel-Componenten laufen unterschiedlich schnell.
Mit dem "gleichschnell" auf allen Rechner meinst wohl folgendes :
Zeitlich äquidistante Ausführung des Render-Vorgangs
und
keine Vorteile durch höhrere Frameraten
(z.B. Rundungsfehler bei FPS_Count-basierten Berechnungen).Noch eine kurze Anmerkung zum Netzwerk:
Die Netzwerk-Latenz ist sehr viel größer als alle anderen Latenzen im Spiel.
Zugegeben, 100FPS == 10ms Verzögerung, sind mit guten DSL-Anbietern schon zu erreichen. Aber es gibt ja auch noch die "RoundTrip-Time", mit der Verabeitungszeit auf beiden Seiten der Verbindung, kommst du also auf :RoundTripTime = 2*Ping + 2*Verarbeitungszeit
mit 10ms für Ping und 10ms == 100Hz ergibt sich 40ms.
Hier wirst du ohne Interpolation und andere Tricks nicht weiter kommen.Ich hoffe, dir weitergeholfen zu haben.
Greetz nurF
-
diese frage wurde hier schon (unter anderem von mir) sehr oft beantwortet.
einfach hier nach logictick suchen dann findest du z.B.
http://www.c-plusplus.net/forum/viewtopic-var-t-is-21275man sollte grundsaetzlich versuchen logik und rendering voneinander zu trennen, nicht nur um eine saubere implementierung zu haben, sondern auch um ein stabileres system zu schreiben. selbst dann wirst du noch muehe haben die eigenheiten von float-genauigkeit und verschiedenen interpolationen bei zeitschritten herr zu werden. (besonders wenn physic laeuft).
-
pock schrieb:
Nimm die genauesten Funktionen zur Zeitmessung, die deine Plattform anbietet.
Das ist IMO für sich schonmal ein Problem.
Unter Windows gibt es z.B. QueryPerformanceCounter, mit dem man sehr fein aufgelöste Werte bekommt. Allerdings sollte man laut MSDN QueryPerformanceCounter garnicht verwenden um längere Intervalle zu messen. Auch kann man auf einigen CPU/Windows-Version/Patch-Level Kombinationen beobachten dass die von QueryPerformanceCounter gelieferten Werte "springen" (also auch z.B. manchmal zurückspringen, oder einfach mal für ein paar ms "einfrieren" oder ähnliches). Auf anderen CPU/Windows-Version/Patch-Level Kombinationen funktioniert es dagegen relativ schön.
Davon abgesehen gibt's dann eigentlich nurnoch timeGetTime bzw. GetTickCount, mit denen man bei den meisten Windows-Versionen eine Genauigkeit von bis zu 2ms bekommen kann (mag sein dass man auf einigen Windows-Versionen sogar auf 1ms runter kommt). timeGetTime/GetTickCount eignen sich schön zum Messen längerer Intervalle, und laufen sehr stabil. Springen grundsätzlich nie zurück etc.
Wenn man mit 2ms auskommt, dann kann man das Problem abhaken. Wenn man aber für irgendetwas geneuere Messwerte haben möchte, und auch will dass es zuverlässig auf allen PCs läuft, dann hat man ein Problem.
Soweit ich weiss ist es gängige Praxis, sich für diesen Fall, eigenen Code zu schreiben, der QueryPerformanceCounter in Kombination mit timeGetTime/GetTickCount verwendet, um dann einen eigenen Zeitwert zu ermittlen. Dazu muss man einige Plausibilitäts-Checks einbauen, und diverse Workarounds implementieren, falls z.B. QueryPerformanceCounter "rumspringt" oder ähnliches. Auch kann man z.B. sicherstellen, dass QueryPerformanceCounter immer auf der selben logischen CPU ausgeführt wird.
Wenn man wirklich gute Ergebnisse bekommen möchte ist das an und für sich schonmal nicht trivial.IMO wäre es nicht schlecht sich sowas zu basteln, und das dann per SourceForge o.ä. verfügbar zu machen. Zumindest dieser Teil wäre etwas, was man in vielen Spielen weiterverwenden kann. Falls es ein ähnliches Projekt schon gibt, wäre ich interessiert, also immer her mit den Links
-
GetTickCount() liefert im Schnitt nur alle 16 Millisekunden einen neuen Wert (habe ich bisher auf keinem Rechner anders erlebt und auch nirgends anders gelesen), weshalb es fuer FPS > 60 nicht unbedingt gut zu gebrauchen ist, je nach dem, wie man Zeit in seinem Spiel verwendet. Insofern ist eine Kombination dann wohl wirklich das Beste.
-
Powerpaule schrieb:
GetTickCount() liefert im Schnitt nur alle 16 Millisekunden einen neuen Wert (habe ich bisher auf keinem Rechner anders erlebt und auch nirgends anders gelesen), weshalb es fuer FPS > 60 nicht unbedingt gut zu gebrauchen ist, je nach dem, wie man Zeit in seinem Spiel verwendet. Insofern ist eine Kombination dann wohl wirklich das Beste.
Hm. Du hast Recht. Man muss timeGetTime verwenden - mit timeGetTime kommt man auf 1 bzw. 2 ms runter.
(Ich hatte das falsch in Erinnerung, dachte die Auflösung von GetTickCount wird auch besser wenn man timeBeginPeriod verwendet -- ist aber zumindest unter XP SP3 nicht so)
-
Danke schonmal für eure Beteiligung.
@rapso: Genau das hatte ich schonmal gelesen, etwas verwirrend, sollte man nun öfter Rendern als Logik berechnen oder umgekehrt? Und sollte man beides begrenzen/ausbremsen (und auf jeweils welche Werte?)?
Was haltet ihr von folgendem Tutorial (incl. Teil 2)?
http://cgempire.com/forum/tutorials-101/beginners-tutorial-time-steps-simulation-time-679.htmlDa der Code dort nicht formatiert ist, habe ich wenigstens die beiden Klassen aus dem 2. Teil, um welche es letztendlich geht grob für euch formatiert:
class CTimer { public: //Class constructor CTimer() : m_dStartAppTime(0), m_dLastAppTime(0), m_dDeltaTime(0), m_dCurrentAppTime(0) { //Calculate the CPU counter frequency LARGE_INTEGER frequency; if(!QueryPerformanceFrequency(&frequency)) { assert("QueryPerformanceFrequency failed!" == 0); } m_dFrequency = static_cast<double>(frequency.QuadPart) * 0.001f; m_dInvFrequency = 1.0f / m_dFrequency; m_dStartAppTime = GetCurrentTickCount(); }; void Update() { //Record the last time and calculate the new application time in seconds m_dLastAppTime = m_dCurrentAppTime; m_dCurrentAppTime = (GetCurrentTickCount() - m_dStartAppTime) * 0.001f; //(1/1000) m_dDeltaTime = (m_dCurrentAppTime - m_dLastAppTime); }; double GetAppTime() const { return(m_dCurrentAppTime); }; double GetDeltaTime() const { return(m_dDeltaTime); }; float GetAppTimeF() const { return(static_cast<float>(m_dCurrentAppTime)); }; float GetDeltaTimeF() const { return(static_cast<float>(m_dDeltaTime)); }; private: double m_dFrequency; double m_dInvFrequency; double m_dStartAppTime; double m_dCurrentAppTime; double m_dLastAppTime; double m_dDeltaTime; double GetCurrentTickCount() { LARGE_INTEGER iCounter; if(!QueryPerformanceCounter(&iCounter)) { assert("QueryPerformanceCounter failed!" == 0); } return(static_cast<double>(iCounter.QuadPart) * m_dInvFrequency); }; };
class CSimTime { public: //Constructor CSimTime() : m_fSimSpeed(1.0f), m_fSimTime(0.0f), m_fDeltaSimTime(0.0f) {}; void SetSimulationSpeed(const float fSimulationSpeed) { m_fSimSpeed = fSimulationSpeed; }; //Generate the new simulation time void Update(const float fDeltaTime) { float fLastSimTime = m_fSimTime; //Calculate the Simulation Time m_fSimTime += fDeltaTime * m_fSimSpeed; //The change in Simulation Time m_fDeltaSimTime = m_fSimTime - fLastSimTime; }; float GetSimTime() const { return(m_fSimTime); }; float GetDeltaSimTime() const { return(m_fDeltaSimTime); }; private: float m_fSimSpeed; float m_fSimTime; float m_fDeltaSimTime; };
-
@dust:
Der Informationsgehalt des Codes den du gepostet hast beschränkt sich auf "es gibt eine Funktion namens QueryPerformanceCounter, und die verwenden wir jetzt lustig".Und nu?
-
dust schrieb:
@rapso: Genau das hatte ich schonmal gelesen, etwas verwirrend, sollte man nun öfter Rendern als Logik berechnen oder umgekehrt? Und sollte man beides begrenzen/ausbremsen (und auf jeweils welche Werte?)?
logik wird in festen zeitabschnitten aufgerufen, z.b. 25mal pro sekunde (bei z.b. Baldurs Gate kann man das in den optionen sogar festlegen).
bei jedem logikschritt nimmst du dann an, dass z.b. 1/25 der sekunde, also 40ms vergangen sind.
die framerate begrenzt du nicht, sondern renderst weiterhin so oft wie moeglich durch, dabei interpolierst du immer zwischen dem letzten logikschritt und dem den du aktuell durchgerechnet hast.
-
rapso schrieb:
die framerate begrenzt du nicht, sondern renderst weiterhin so oft wie moeglich durch, dabei interpolierst du immer zwischen dem letzten logikschritt und dem den du aktuell durchgerechnet hast.
D.h. also, dass die Logik der Darstellung immer x Millisekunden voraus ist ( 0 < x < 25ms )?
-
dust schrieb:
@rapso: Genau das hatte ich schonmal gelesen, etwas verwirrend, sollte man nun öfter Rendern als Logik berechnen oder umgekehrt?
pock schrieb:
Die Spielelogik muss nur wenige Male pro Sekunde aktualisiert werden, Physik und Animation hingegen sehr oft. [...] Um die langsamere Logik flüssig darzustellen, kommst du um Interpolation nicht herum.
Was haltet ihr von folgendem Tutorial (incl. Teil 2)?
Ich habe nur deinen geposteten Code gelesen. Dort wird QueryPerformanceCounter blind vertraut, was nicht ratsam ist. Die Funktion hat mitunter unerklärliche Ausreißer nach oben und unten, so dass auch mal eine negative Zeitdifferenz auftreten kann. Das muss abgefangen werden.
Ich würde evtl. auch ein Fallback auf timeGetTime (und timeBeginPeriod) vorsehen, falls es keinen PerformanceCounter gibt.
Der Thread zur Zeitmessung sollte auf jeden Fall mit SetThreadAffinityMask einer CPU fest zugeordnet werden, das sorgt für mehr Stabilität der gemessenen Werte.
-
mad_martin schrieb:
rapso schrieb:
die framerate begrenzt du nicht, sondern renderst weiterhin so oft wie moeglich durch, dabei interpolierst du immer zwischen dem letzten logikschritt und dem den du aktuell durchgerechnet hast.
D.h. also, dass die Logik der Darstellung immer x Millisekunden voraus ist ( 0 < x < 25ms )?
jap, die logik ist immer ein wenig voraus, genau wie der input der logik voraus ist und dein rendern dem gpu treiber voraus ist und der treiber der gpu selbst...
das kann schon einiges an latenz sein, aber das merkt man eher weniger, da viele dinge mit absicht sogar noch traegheit bekommen um ein massegefuehl zu erreichen. selbst bei quake3 hat man das
-
pock schrieb:
... Die Funktion hat mitunter unerklärliche Ausreißer nach oben und unten, so dass auch mal eine negative Zeitdifferenz auftreten kann. ...
das ist ein bug der durch multicore cpus verursacht wird und wird mit einem patch behoben. performance counter sollten durchgaengig stabile und genaue ergebnisse liefern.
-
rapso schrieb:
pock schrieb:
... Die Funktion hat mitunter unerklärliche Ausreißer nach oben und unten, so dass auch mal eine negative Zeitdifferenz auftreten kann. ...
das ist ein bug der durch multicore cpus verursacht wird und wird mit einem patch behoben. performance counter sollten durchgaengig stabile und genaue ergebnisse liefern.
Hab schon öfter gelesen und gehört, dass trotzdem unlogische Werte rauskommen können. Selbst gesehen hab ich es allerdings nicht, ich würde es dennoch abfangen und die Messung auf eine CPU begrenzen.
-
grundsaetzlich, aus portabilitaet und performance gruenden sollte man sehr gekapselt timings nehmen.
stellt euch vor ihr wollt mal ein video capturen, eines mit bester qualitaet, bei dem euer bester pc weniger als 10fps schafft und dazu dann noch packen und ablegen. dann ist es sehr gut die moeglichkeit zu haben es von der realen zeit unabhaengig laufen zu lassen.
habt ihr in der Engine eine LogicTime() und eine RenderTime() funktion die euch von einer member die zeit zurueckgeben und die einmal pro renderdurchlauf bzw logikschritt geupdated werden, ist es ein klacks der ganzen engine vorzugaukeln dass ihr z.b. mit konstant 30fps oder 60fps rendert und den ganzen system-call oeverhead habt ihr auch nicht (und das ist nicht gerade wenig, meist je genauer der counter ist).das gleiche gilt uebrigens fuer randomize funktionen. die sollten auch sehr kontrolliert ablaufen, falls man multithreading hat und von anfang an ein sauberes software-design anstrebt, sollte man pro thread einen randomizer anlegen usw.
-
Würdet ihr im Konstruktor der Timerklasse ...
SetThreadAffinityMask(GetCurrentThread(), 1);
aufrufen, um Problemen mit MultiCoreProzessoren aus dem Weg zu gehen? MS empfiehlt ja keinen eigenen Thread für das Timing zu benutzen, andererseits ist es vielleicht auch nicht ratsam den Mainthread (in dem auch das Timing läuft) auf nur einem Kern laufen zu lassen.
-
Hey rapso,
ich hoffe ich hab‘ verstanden wie dein Timing funktioniert, ich fass mal zusammen:
Du rechnest 25 mal pro Sekunde aus, wie die Scene in jeweils 40 ms aussehen muss (=LogicTick) und schaust dann bei jedem Rendern (welches du nicht bremst) wieviel Zeit seit dem letzten LogicTick vergangen ist (maximal die 40 ms) setzt danach die Positionen der Objekte. Wenn wir als Beispiel von einem Objekt ausgehen, was sich geradlinig und mit konstanter Geschwindigkeit bewegt, dann speicherst du wahrscheinlich immer die letzte (im vorletzten LogicTick berechnet) und die neue Position (im letzten LogicTick errechnet) des Objekts und setzt es - wenn beispielsweise 20 ms von 40 ms zum nächsten LogicTick vergangen sind - genau in die Mitte zwischen letzter - und neuer Position.
Richtig so? Vielleicht kannst du noch etwas näher auf das eigehen, was du Animatoren nennst, vielleicht auch mit bisserl Code? Wär' dir sehr dankbar.Und nochmal zu deinem Source aus http://www.c-plusplus.net/forum/viewtopic-var-t-is-21275-and-postdays-is-0-and-postorder-is-asc-and-start-is-10.html :
int LastTick=GetTickCount(); while(!g_bQuit) { while(LastTick<GetTickCount()) { g_pAktualModule->LogicTick(); LastTick+=TICKTIME; if(LastTick+10000<GetTickCount()) LastTick=GetTickCount()+TICKTIME; } Render(); if(PeekMessage(&msg,hWnd,0,0,PM_NOREMOVE)!=0) { g_bQuit = !((bool)(PeekMessage(&msg,hWnd,0,0,PM_REMOVE))); TranslateMessage(&msg); DispatchMessage(&msg); } }
Ich nehme an, dass …
if(LastTick+10000<GetTickCount()) LastTick=GetTickCount()+TICKTIME;
… zum Abfangen des Wrap-Arounds ist welcher nach 49,7 Tagen eintritt, und dass TICKTIME die erwähnten 40 ms sind, oder?
Danke!
-
dust schrieb:
Würdet ihr im Konstruktor der Timerklasse ...
SetThreadAffinityMask(GetCurrentThread(), 1);
aufrufen, um Problemen mit MultiCoreProzessoren aus dem Weg zu gehen? MS empfiehlt ja keinen eigenen Thread für das Timing zu benutzen, andererseits ist es vielleicht auch nicht ratsam den Mainthread (in dem auch das Timing läuft) auf nur einem Kern laufen zu lassen.
Im ctor der Timerklasse hat das auf jeden Fall nix verloren.