Das perfekte Timing
-
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.
-
Im ctor der Timerklasse hat das auf jeden Fall nix verloren.
Sondern? Und warum nicht?
-
dust schrieb:
Im ctor der Timerklasse hat das auf jeden Fall nix verloren.
Sondern?
Ich hab nie behauptet dass es einfach wäre und ich die Silver-Bullet für Timing hätte
Und warum nicht?
Weil es ein ganz grober Seiteneffekt ist. Löscht du Files im ctor einer String-Klasse? Nein? Eben.
-
Erklär mal genauer, was da schlimm dran ist, ich blick's nicht.
-
dust schrieb:
Richtig so?
jap, das ist gut zusammengefasst!
Vielleicht kannst du noch etwas näher auf das eigehen, was du Animatoren nennst, vielleicht auch mit bisserl Code? Wär' dir sehr dankbar.
oh, sorry, ja, so nannte man an einer engine an der ich gearbeitet habe die objekte an nodes von einem scenegraph, die die nodes dann animiert/bewegt hatten.
Und nochmal zu deinem Source
… zum Abfangen des Wrap-Arounds ist welcher nach 49,7 Tagen eintritt, und dass TICKTIME die erwähnten 40 ms sind, oder?Danke!
nein, das ist resettet den counter falls 10s lang keine logik lief.
die idee ist ja, falls du mal z.b. nur 5fps hast, dass die logik dann trotzdem noch x-mal pro sekunde durchlaeuft, dann hast du fast kein nuetzliches spiel mehr, also nen shooter waere damit echt unbrauchbar ;).
andererseits koenntest du trotzdem fuer eine simulation die logik mit z.b. 250logikschritten pro sekunde in einem "fast forward" modus rechnen lassen, es gibt einige spiele die dann nur noch wenige fps anzeigten.ab einem gewissen punkt sollte man das sein lassen. z.b. gibt es notebooks deren festplatte ausgeht nach ein paar sekunden, stell dir vor jemand spielt einen shooter und nun will windows ploetzlich wieder ein ausgelagertes speicherstueck nachladen, startet die hdd, das spiel steht fuer nen moment (kann manchmal 20s dauern).
wenn dann wieder dein spiel weiterlaeuft/rechenzeit bekommt, willst du nicht dass fuer die letzten 20s alles durchgerechnet wird, weil die logik meint dass real 20s vergangen sind und der spieler dann erst wieder ein frame sieht wenn in-game 20s vorbei sind.
wenn er dann vor nem gegner stand, wird er vermutlich tod sein und sich eventuell aufregenin so nem fall geht man einfach davon aus dass keine reale zeit vergangen ist und nur ein logiktick noetig ist.
manche spiele gehen bei sowas auch mal in den pausemodus und erlauben dem spieler dann ruhig weiter zu spielen.ebenfalls ist es oft ueblich, dass ein spiel nicht sofort losgeht nach dem pausemodus, sondern erstmal der spieler das spiel sieht, die situation erkennt und nach 3s bis 5s erst die logik einsetzt (wichtig vor allem bei z.b. rennspielen). da kannst du auch einfach den "LastTick" mit +5s initialisieren.
zum interpolieren:
man muss nicht immer und alles interpolieren, man muss die dinge nicht 100% akkurat interpolieren.
stell dir vor du hast 100 baelle die in einem raum quer durch die gegend springen und sich auch gegenseitig beruehren koennen (also physikalisch korrekt). eventuell ist das extrem aufwendig zu simulieren und die hardware auf der es laufen soll schafft das garnicht mit 60fps mit der das spiel laufen sollte.
dann kannst du trotz komplexester, nicht linearer simulation der bewegungen (die vielleicht nur 10mal die sekunde durchlaeuft) berechnen, diese mit 60fps linear interpolieren.
particle kannst du eventuell sogar nur mit nem shader interpolieren.
die camerabewegung des spielerst solltest du aufwendiger interpolieren, also z.b. die winkel ausrichtung jedesmal interpolieren und dann die view matrix neu errechnen.
die bewegung der gegner kannst du interpolieren indem du simpel jedes element einer matrix fuer sich interpolierst zwischen den zustaenden. natuerlich ist das nicht akkurat und bei rotationen hast du leichte verzerrungen, aber das merkt man nicht.
animationen an sich von koerpern musst du vielleicht garnicht interpolieren, du kannst aber die beiden zustaende nutzen um einen motionblur vector zu errechnen (siehe KillZone2 oder Crysis).das haengt auch immer vom spiel ab. bei einem RTS mag es nicht viel ausmachen wenn die einzelnen einheiten nur 20mal pro sekunde geupdated werden. bei nem shooter faellt eventuell doch auf wenn es keine 100mal sind.
-
Ich habe mal eine Frage bezüglich der Abkopplung zwischen Logik und Darstellung. Wie genau realisiert ihr das am Beispiel eines Szenengraphs, der für die Darstellung sorgt? Sind logikabhängige Knoten bei euch dort "double-buffered" ausgelegt? Oder ist der Szenengraph komplett doppelt vorgehalten? Wenn Logik und Rendering nicht synchron laufen und bespielsweise auf 2 Threads verteilt werden, wie verhindert ihr, dass der Render-Thread denselben Status zweimal anzeigen muss, weil der Logikthread gerade den nächsten Status des Szenengraphs aktualisiert? Oder darf der Renderer auch einen Szenengraph zeichnen, bei dem der "Zeitstempel" der einzelnen Knoten nicht synchron ist, weil der Logikthread gerade darauf werkelt?
Fragen über Fragen^^
Viele Grüße,
MichaelPS: Oder "interleaved" ihr das Rendern und die Logik nur? Will meinen, beispielsweise 2x Logik -> 1x Rendern -> 5x Logik 1x Rendern ... etc.