Feste Spielgeschwindigkeit



  • hustbaer schrieb:

    Rendern dauert üblicherweise (viel) länger als einen Logik-Step machen.
    Ausserdem können wir ja nicht gut rendern, wenn die Logik noch nicht auf dem aktuellen Stand ist, hm?

    Doch sicher, es wird halt einfach der alte Logikzustand gerendert.



  • this->that schrieb:

    hustbaer schrieb:

    Rendern dauert üblicherweise (viel) länger als einen Logik-Step machen.
    Ausserdem können wir ja nicht gut rendern, wenn die Logik noch nicht auf dem aktuellen Stand ist, hm?

    Doch sicher, es wird halt einfach der alte Logikzustand gerendert.

    Und wo macht das Sinn? Das ist doch Quatsch^4.



  • @this-that
    ...aber dann kommt es zum besagten "Zittern".

    Die Interpolation über alle gerenderten Bilder (wie sie RedPuma beschreibt) ist da die bessere Alternative, denke ich.



  • hustbaer schrieb:

    this->that schrieb:

    hustbaer schrieb:

    Rendern dauert üblicherweise (viel) länger als einen Logik-Step machen.
    Ausserdem können wir ja nicht gut rendern, wenn die Logik noch nicht auf dem aktuellen Stand ist, hm?

    Doch sicher, es wird halt einfach der alte Logikzustand gerendert.

    Und wo macht das Sinn? Das ist doch Quatsch^4.

    Hab ja nicht gesagt das es das sinnvollste is. Nur hast du es so hingestellt, als ginge es garnicht. Außerdem könnte man auch zwischen 2 Logikschritten interpolieren.



  • Danke für eure vielen Tips 🙂
    Ich werde jetzt mal alles in Ruhe ausprobieren und mich wieder melden, wenn ich es geschafft habe (oder auch nicht 😃 ).



  • Ich habe jetzt die Methode von RedPuma benutzt, und sie funktioniert jetzt fast so, wie ich es mir vorgestellt habe. Nur läuft mein Programm halb so schnell wie es soll. Anstatt sich ein Pixel zu bewegen, bewegt sich das Objekt nur 0,5 Pixel in einer Sekunde. Mit 300 Pixeln in 6 Sekunden ist es das gleiche. Das Objekt bewegt sich nur 150 Pixel in 6 Sekunden.
    Hier mal alle relevanten Codeabschnitte:
    timer.h:

    class timer {
    public:
    	void			getResolution(void);
    	void			timerStart(void);
    	void			timerEnd(void);
    	double			getSeconds(void);
    private:
    	LARGE_INTEGER	frequency;
    	LARGE_INTEGER	start;
    	LARGE_INTEGER	end;
    	__int64			elapsed;
    	double			resolution;
    	double			seconds;
    };
    

    timer.cpp:

    void timer::getResolution(void) {
    	QueryPerformanceFrequency(&frequency);
    	resolution = 1.0 / frequency.QuadPart;
    };
    
    void timer::timerStart(void) {
    	QueryPerformanceCounter(&start);
    };
    
    void timer::timerEnd(void) {
    	QueryPerformanceCounter(&end);
    };
    
    double timer::getSeconds(void) {
    	elapsed = end.QuadPart - start.QuadPart;
    	seconds = elapsed * resolution;
    	return seconds;
    };
    

    Ausschnitt aus der Programmschleife:

    time.getResolution();
    while(ProgramIsLooping) {
        if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) {
            //....
        } else {
            time.timerStart();
            render();
            time.timerEnd();
            doLogicSteps(time.getSeconds());
        }
        Sleep(1);
    }
    

    Objekt bewegen:

    if (isKeyDown(VK_UP)) {
    	objects.movePlayer(0, 50.0 * time);
    }
    if (isKeyDown(VK_RIGHT)) {
    	objects.movePlayer(50.0 * time, 0);
    }
    if (isKeyDown(VK_DOWN)) {
    	objects.movePlayer(0, -50.0 * time);
    }
    if (isKeyDown(VK_LEFT)) {
    	objects.movePlayer(-50.0 * time, 0);
    }
    

    Falls es noh jemanden interessiert, hier sind die Werte aus der timer class:

    frequency   = 3579545
    resolution  = 2.7936511484001459[h]-7[/h]
    start       = 102556231840
    end         = 102556361119
    elapsed     = 129279
    seconds     = 0.036116042681402248
    

  • Mod

    weil du nur die zeit vom render aufruf misst, nicht die des ganzen durchgangs.
    ich wuerde konstante zeitabschnitte fuer die logik waehlen, dann hast du keine unvorhersehbaren verhaltensweisen wenn jemand nur 5fps oder 500fps hat.



  • Auch wenn ich den Timer über alles laufen lasse, ändert sich wenig.
    Die Methode von RedPuma wurde zudem doch als die bessere bezeichnet.
    Und um ehrlich zu sein, aus dem Code von Cpp_Junky werde ich nicht wirklich schlau.

    EDIT: Bei der Methode von RedPume, kommt es bei mir auch zum Zittern.



  • this->that schrieb:

    hustbaer schrieb:

    this->that schrieb:

    hustbaer schrieb:

    Rendern dauert üblicherweise (viel) länger als einen Logik-Step machen.
    Ausserdem können wir ja nicht gut rendern, wenn die Logik noch nicht auf dem aktuellen Stand ist, hm?

    Doch sicher, es wird halt einfach der alte Logikzustand gerendert.

    Und wo macht das Sinn? Das ist doch Quatsch^4.

    Hab ja nicht gesagt das es das sinnvollste is. Nur hast du es so hingestellt, als ginge es garnicht. Außerdem könnte man auch zwischen 2 Logikschritten interpolieren.

    Nein, es geht auch nicht.

    Sagen wir unser fixer Logik-Step ist 5ms.
    Sagen wir das Rendern eines Frames braucht auf einem bestimmten Rechner 20ms.

    Dann kann das nicht funktionieren, wenn man max. 1 Logik-Step pro Grafik-Frame macht. Das Game würde mit 1/4 der beabsichtigten Geschwindigkeit laufen, und hätte *nie* die Chance aufzuholen.

    -> kompletter Quargel



  • @trooper:
    Bei dem was RedPuma beschrieben hat wird garnix über alle gerenderten Bilder interpoliert. Das ist einfach die 08/15 Variante mit variabler Logik-Step Zeit. Das funktioniert einigermassen gut, allerdings wird dadurch das Ergebnis von bestimmten Berechnungen von der Framerate abhängig.

    Beispiel: du willst deine Spielfigur springen lassen.
    Dazu setzt du beim Losspringen die Y-Speed auf einen gewissen Wert. Bei jedem Logik-Frame machst du dann ...

    y += y_speed * delta_time;
    y_speed = y_speed - gravity * delta_time;
    
    // do collision detection etc.
    

    Im Prinzip löst man hier ein Integral, nur nicht genau, sondern eben "numerisch". Und dass das Ergebins dieser numerischen Integration von der Step-Size abhängig ist, sollte klar sein.
    Was dabei rauskommt: bei niedriger Framerate kann man höher springen als bei hoher Framerate.
    Wenn man die zwei Zeilen vertauscht, also erst y_speed anpasst und dann erst y dreht sich der Effekt um: man kann bei niedriger Framerate weniger hoch springen.
    Natürlich kann man jetzt anfangen und da eine schlauere Formel reinschreiben, man muss ja nicht das "dümmste" aller numerischen Integrationsverfahren nehmen. Ist allerdings wieder etwas an Aufwand, und würde ich persönlich mir lieber sparen. Und je mehr verschiedene Berechnungen man hat, desto mehr Aufwand wird es.

    Wenn man dagegen mit fixem Logik-Step rechnet, kann man sich das schenken. Dadurch dass die Logik-Framerate üblicherweise recht hoch ist, sind die Ungenauigkeiten auch hübsch klein, es besteht kein Bedarf mit irgendwelchen "schlaueren" Formeln zu rechnen. Und vor allem: es läuft überall gleich, vollkommen unabhängig von der Grafik-Framerate.

    Ich finde das ziemlich praktisch.

    Pikkolini schrieb:

    Auch wenn ich den Timer über alles laufen lasse, ändert sich wenig.
    Die Methode von RedPuma wurde zudem doch als die bessere bezeichnet.
    Und um ehrlich zu sein, aus dem Code von Cpp_Junky werde ich nicht wirklich schlau.

    EDIT: Bei der Methode von RedPume, kommt es bei mir auch zum Zittern.

    Siehe oben.

    Ich halte die Methode mit fixem Logik-Step für einfacher zu implementieren und besser vom Ergebnis her.

    Als Logik-Framerate bietet sich denke ich 300 Hz an. Das ist durch 25, 30, 50 und 60 dividierbar - also praktisch alle "relevanten" Frameraten.



  • Sagen wir mal so, ich hab Spieleprogrammieren praktisch mit dem SourceCode von Half-Life 2 gelernt, zumindest was die Logik angeht. Dort wirds auch so gemacht, die Zeit wird gemessen, dann die Logik verarbeitet und dann gerendert. Bei meinem eigenen OpenGL Spielchen mache ich das genau so. Und ich hatte nie Probleme mit "Zittern" oder ähnlichem. Ich hab also nur aus eigener Erfahrung gesprochen.
    Und falls jetz jemand mit ner Tickrate bei Source kommt, die hat nichts mit dem Rendern an sich zu tun sondern nur mit Updates vom Server. Partikeleffekte z.B. werden komplett Clientseitig berechnet.
    Man muss die Frame-Zeit natürlich bei vielen Berechnungen beachten, ebenso bei der von hustbaer geposteten Sprung-Berechnung. Dort hilft es die Zeit zu speichern an der man anfängt zu springen und dann mit der Differenz zu dieser zu rechnen, also mit der Zeit die seit dem Anfang des Sprungs vergangen ist. Deshalb hat z.B. mein Programm auch zwei Zeitvariablen, einmal die Frame-Zeit (typ. < 0,1s ) und einmal die globale Systemzeit welche ich für solche berechnungen nutze.

    @ Pikkolini: Versuch mal folgendes (pseudocode)

    while(true)
    {
            time.timerEnd();
            time.timerStart();
            doLogicSteps(time.getSeconds());
            render();
    }
    

    Musst allerdings darauf achten dass du die beiden Large-Integer start bzw. end in der Timer-Klasse auf 0 initialisierst damit es beim ersten durchlauf zu keinem Fehler kommt.


  • Mod

    Ja, so ist die evolution :). Erst hat man immer eine fest logikrate angenommen, an die VSync frequenz des monitors angepasst (ich bin mir sicher ihr wisst was das heute fuer folgen hat :D), dann mit variabler logik framerate, da gab es dann auch mit der zeit spiele die in fps bereichen liefen die so tief oder hoch waren dass garnichts mehr geht (z.b. float underflows im physic system bei zu guter fps). Doom3 hat entsprechend die physic auf 60hz limitiert afaik.
    Dann hat man mit dem logik tick weiter gemacht, was so die letzten 5jahre ziemlich weit herum kam.
    jetzt faengt man an den systemen ihre individuellen "ticks" zu geben, merk ich hier schoen beim debuggen, wo ich trotz 20fps beim rendern, sowenige "Frames" in der physic habe, dass mein player wie bei einem network lag dauernt zurueck gesetzt wird und die AI bei den raycast die die physic zurueckliefert soviele frames hatte, dass die raycast resultate schon veraltet sind und entsprechend die AI nie den spieler sieht.

    ich wuerde dennoch weiterhin auf logikframes setzen, da es ein relativ stabiles system bietet. man kann die player camera, den player und vielleicht particle fuers rendern zwischen logikframes interpolieren, damit sieht der spieler immer updates und alles ist stabil.



  • @hustbär
    Zunächst einmal kommt es auf die Art des Spieles an ob die Logiksteps länger als die Renderzeit oder kürzer ist.

    Da die Logiksteps in festen Zeitschritten verarbeitet werden sollen und die Framrate nicht fix sein soll muss zwangsläufig über die Logikschritte interpoliert werden. Sonst wird ein Frame mit dem gleiche Inhalt mehrfach gerendert und das sieht nicht schön aus. Voraussetzung dafür ist natürlich, dass die Logikschritte länger sind als die Framerate ist.

    Spätestens im Multiplayer kannst du Logikschritte mit 300Hz komplett vergessen und musst Dir irgendeine schlaue Idee überlegen, wie du zwischen den Schritten am Besten interpolierst. Selbst um Vorhersagen kommst du dann evtl. nicht drumherum.

    Und RedPuma hat eine Interpolation beschrieben zwischen zwei Positionen. Hier von einem Integral zu sprechen ist schon starker Tobak.

    Interpolation zwischen zwei Punkten:
    Pakt = (Palt-Pneu) * t + Pneu
    (t von 0.0 bis 1.0)

    Er berechnet die Zeit die das letzte Frame gebraucht hat und addiert zu der alten Position den berechneten Versatz ( (Palt-Pneu)/N-Schritte.) N ist die Frameanzahl zwischen den einzelnen Logikschritten. Also wird der Teil: (Palt-Pneu) * t durch eine Addition ersetzt. Whoala und wir haben die Interpolation. 😉

    @rapso
    Dein Post kam eher als meiner.
    "...fuers Rendern zwischen Logikframes interpolieren..."
    Da schreibst du es ja auch. 👍

    @Pikkolini
    Bist Du Dir sicher, dass die Logikschritte völlig losgelöst sind und nicht etwa noch von der Eingabe abhängen, denn wenn es bei Dir zum Zittern kommt ist zwangsläufig irgendwo eine Pause zwischen den Schritten drin. Evtl. ist die Anzahl der Interpolationsschritte auch falsch.

    Gruß
    Trooper


  • Mod

    trooper schrieb:

    Und RedPuma hat eine Interpolation beschrieben zwischen zwei Positionen. Hier von einem Integral zu sprechen ist schon starker Tobak.

    soweit ich das sehe hat redpuma die berechnungsart von z.B. halflife besprochen, diese interpoliert nicht zwischen zwei logikschritten, sondern binden logik und rendering and die aktuelle fps. du interpolierst also nicht, sondern simulierst mit variablen schritweiten.

    Interpolation zwischen zwei Punkten:
    Pakt = (Palt-Pneu) * t + Pneu
    (t von 0.0 bis 1.0)

    Er berechnet die Zeit die das letzte Frame gebraucht hat und addiert zu der alten Position den berechneten Versatz ( (Palt-Pneu)/N-Schritte.) N ist die Frameanzahl zwischen den einzelnen Logikschritten. Also wird der Teil: (Palt-Pneu) * t durch eine Addition ersetzt. Whoala und wir haben die Interpolation. 😉

    das klappt eben nur bei sehr simplen (spricht linearen) bewegungen. hast du z.B. einen sprung wie hustbaer sagte, ist je nach schrittweite ein anderes ergebnis zu erwarten. genau das ist das problem.

    @rapso
    Dein Post kam eher als meiner.
    "...fuers Rendern zwischen Logikframes interpolieren..."
    Da schreibst du es ja auch. 👍

    ich schreibe interpolieren (das macht man zwischen zwei fixen punkten). das ist wirklich was anderes als variable schrittweiten beim simulieren.
    waere beides gleich, wuerde man sich hier ja die diskusion sparen 😉



  • rapso schrieb:

    das klappt eben nur bei sehr simplen (spricht linearen) bewegungen. hast du z.B. einen sprung wie hustbaer sagte, ist je nach schrittweite ein anderes ergebnis zu erwarten. genau das ist das problem.

    RedPuma schrieb:

    [...]ebenso bei der von hustbaer geposteten Sprung-Berechnung. Dort hilft es die Zeit zu speichern an der man anfängt zu springen und dann mit der Differenz zu dieser zu rechnen, also mit der Zeit die seit dem Anfang des Sprungs vergangen ist.

    /Edit: hab nochmal drüber nachgedacht, die Source-Engine simuliert die Spiel-Logik an sich Serverseitig mit einer bestimmten Tickrate (z.B. 66/s). Im Client wird dann bei voller Framerate interpoliert. Das liegt aber eher an der Multiplayer-Eigenschaft der Engine, selbst beim Singleplayer spielt man praktisch auf nem Server der auf dem eigenen PC läuft. Wenn man im Client mit den interpolierten Werten rechnet dann ist das praktisch kein Unterschied zu der Berechnung mit "echten" Zeitwerten.
    Client-seitige Effekte (wie z.B. die angesprochenen Partikeleffekte) werden mit der vollen Framerate des Clients berechnet.
    Und selbst im Server muss natürlich auch mit variablen Zeitwerten gerechnet werden da niemand garantieren kann dass dieser die maximalen 66 ticks/s auch wirklich schafft.



  • @rapso
    Ich denke dass wir aneinander vorbeireden, denn bei mir wird der Sprung in einzelne Schritte unterteilt und nicht als Ganzes berechnet. Zwischen diesen Einzelschritten wird pro Frame interpoliert. Hier kommt bei jedem Schritt noch die Physik hinzu (außerdem kann man ja neben linear auch noch über Splines oder Hermite interpolieren und schon sind alle Kurven sauber selbst bei komplexen Bewegungen).

    Im Multiplayer bekommt der Client nur die Zielkoordinaten des Players und lässt ihn selbstständig hinlaufen (auch über Interpolation). Bei mir klappt das wunderbar ohne "Zittern".

    "...das ist wirklich was anderes..."
    Mathematisch gesehen ist es das gleiche (siehe Beschreibung von mir). 😉
    Und mit Nichten ein Integral. 👎

    //Edit:
    @RedPuma
    Dein Post kam noch vor meinem.


  • Mod

    RedPuma schrieb:

    rapso schrieb:

    das klappt eben nur bei sehr simplen (spricht linearen) bewegungen. hast du z.B. einen sprung wie hustbaer sagte, ist je nach schrittweite ein anderes ergebnis zu erwarten. genau das ist das problem.

    RedPuma schrieb:

    [...]ebenso bei der von hustbaer geposteten Sprung-Berechnung. Dort hilft es die Zeit zu speichern an der man anfängt zu springen und dann mit der Differenz zu dieser zu rechnen, also mit der Zeit die seit dem Anfang des Sprungs vergangen ist.

    /Edit: hab nochmal drüber nachgedacht, die Source-Engine simuliert die Spiel-Logik an sich Serverseitig mit einer bestimmten Tickrate (z.B. 66/s). Im Client wird dann bei voller Framerate interpoliert. Das liegt aber eher an der Multiplayer-Eigenschaft der Engine, selbst beim Singleplayer spielt man praktisch auf nem Server der auf dem eigenen PC läuft. Wenn man im Client mit den interpolierten Werten rechnet dann ist das praktisch kein Unterschied zu der Berechnung mit "echten" Zeitwerten.
    Client-seitige Effekte (wie z.B. die angesprochenen Partikeleffekte) werden mit der vollen Framerate des Clients berechnet.
    Und selbst im Server muss natürlich auch mit variablen Zeitwerten gerechnet werden da niemand garantieren kann dass dieser die maximalen 66 ticks/s auch wirklich schafft.

    die HL2 engine simuliert auf mehreren wegen wenn ich mich recht erinnere was mir ein paar source-engine developer erzaehlten

    wenn ich mich recht entsinne gibt es
    1. auf dem server in fixen zeitschritten logik die leicht voraus laeuft (wegen der hit detection wird wohl die position alle player zu jeden logikstep aufgezeichnet).
    2. auf dem client in fixen zeitschritten logik die in der aktuellen zeit laeuft, also serverzeit - latency
    3. auf dem client die frame-frame logik.

    die meisten dinge werden dabei nur auf der frame-frame logik simuliert da sie nicht spielrelevant sind. die wirklichen entscheidungen macht der server und die logik-ticks(2) auf clientseite dienen eigentlich nur um redundanzdaten vom server nicht zu uebertragen, spricht: reine kompression.

    am ende ist es ein recht komplexes system, denn der server mit festen logiksteps wird von einem client mit variablen logiksteps gefuettert. dabei ist zwar der server der mit entscheidungsgewalt, aber der client kann unsinn einfuettern die der server dann versucht zu erkennen, wenn die clientseitigen fehler ausserhalb einer gewissen tolleranz sind.



  • Wobei wir damit schon beim Netcode sind wo wir eigentlich nicht hin wollten 😉



  • trooper schrieb:

    @hustbär
    Und RedPuma hat eine Interpolation beschrieben zwischen zwei Positionen. Hier von einem Integral zu sprechen ist schon starker Tobak.

    Ich schreibe in dem Zusammenhang auch nicht von einem Integral.

    Vielleicht liest du mein Posting einfach nochmal. Die Berechnung der "Flugbahn" beim Springen ist von der Sache her die numerische Lösung eines Integrals. Also wenn man es so macht wie in meinem Beispiel.

    Ich schreibe im Übrigen sowieso nirgends von Interpolieren, das kommt immer von anderen.
    Interpolieren ist gaga, das macht man erst wenn man muss. Oft genug muss man nicht, dann macht mans nicht.



  • trooper schrieb:

    Ich denke dass wir aneinander vorbeireden, denn bei mir wird der Sprung in einzelne Schritte unterteilt und nicht als Ganzes berechnet. Zwischen diesen Einzelschritten wird pro Frame interpoliert.

    Haben die Einzelschritte bei dir eine fixe Zeit, oder variable Zeit?


Anmelden zum Antworten