Bewegungsanimation, leichtes Ruckeln, aber kein Berechnungsfehler



  • Ich tüftel malwieder, diesmal an eienr Bewegungssimulation. Mein programm hat ein Quadrat das Mittels Tastatureingaben (Pfeiltasten) bewegt werden kann. Mein Problem besteht darin das ich verschiedene berechnungen durch probiert habe, aber bei jeder tritt irgendwann ein richtiger Ruuck auf, dh es sieht so aus als ob das Quadrat sich Plötzlich einige Frames weiter bewegt inenrhalb eines Frames, das ist sehr unschön, ich finde aber keine Lösung wie ich das ganze wegbekomme, ich kann es nur auf ein minimum reduzieren, dann tritt soetwas mal alle paar sekunden auf. Mein Programm arbeitet so, die Logic komt in imemr gleichen Abständen(PerformanceCounter), erfragt die gedrückte taste und ändert dann 4 Variablen, für jede Richtung eine, diese werden dann bei jeder Frame auf die Position des Quadrates gepackt und somit wird das Quadrat Animiert. Zur Berechnung nutze ich die FPS die ich nach jeder Frame neu errechnen lasse. Ich habe dazu schon diverse Rechenmodelle getestet die allerdings alle nicht 100%ig ruckelfrei waren, dh immmrmal traten ruckler auf.

    Hier sind die Berechnungsvarianten die ich bisher durch habe

    //Variante 1
    bewegungsgeschwindigkeit=speedProLogic/framesZurNächstenLogic;
    //bringt je nachdem wieviele Frames es bis zur nächsten Logicrunde sind
    //unterschiedliche Speeds raus
    ////////////////////////////////////////////////////////////
    //Variante 2
    bewegungsgeschwindigkeit=speedProSekunde/FPS;
    //diese variante gibt ein etwas besseres bild, ist aber bei der position nicht
    //so genau da ja je nahc speed mal mehr mal weniger frames zwischen den logics
    //liegen
    ///////////////////////////////////////////////////////////
    //Variante 3
    bewegungsgeschwindigkeit=speedProSekunde/FPS;
    //hier sind alle Werte Double, das fängt Komamstellen ab
    

    Um die Bewegungsungenauigkeiten von Variante2 und 3 auszugleichen hatte ich noch ein 2. Quadrat eingebaut das Speichert wie weit sich das Quadrat bis zum nächsten Logicstep bewegen sollte (da die Logic ja in statischen abständen kommt) und bei jedem logicstep das echte Quadrat auf die werte des Speichers zu legen um so die einigen pixel differenz abzufangen, das ergebniss war jedoch das das Quadrat etwas zittert beim bewegen.

    Wenn ich die FPS aus der Berechnung herausgenomemn habe, also das die Bewegungsgeschwindigkeit imemr gleich ist, egal wieviele FPS der PC chaft (er würde dann langsammer in der Bewegung werden bei niedrigen FPS), dann waren die auftretenden Ruckler weg, sobald die Berechnung die FPS einschließt sind sie da, die Berechnung selbst arbeitet jedoch Tadellos, dazu hat mein PC auch eine absolut konstante FPS so das gelegentliche Ruckler durch sinkende FPS auch nicht auftreten können das Quadrat aber trotzdem gelegentlich sprünge macht.

    Da ich keine Spiele jemals gesehen habe in denen Objekte diese Probleme haben, was mache ich falsch in meinen Berechnungen?


  • Mod

    die fps aendert sich doch die ganze zeit, somit ist es nicht weise anhand der fps die bewegung zu berechnen ;). das einzige was zaehlt ist die zeit.

    die logik laeuft immer ein wenig vor und du speicherst dir den letzten step ab. die darstellung interpoliert dann mit der aktuellen zeit zwischen den beiden logiksteps.
    das hat zudem den vorteil dass die interpolation sehr einfach ist, verglichen mit den 'echten' bewegungs-berechnungen. bei manchen spielen wie z.b. 2d RPG lief die logik manchmal nur 10mal die sekunde durch.



  • rapso schrieb:

    die fps aendert sich doch die ganze zeit, somit ist es nicht weise anhand der fps die bewegung zu berechnen ;). das einzige was zaehlt ist die zeit.

    die logik laeuft immer ein wenig vor und du speicherst dir den letzten step ab. die darstellung interpoliert dann mit der aktuellen zeit zwischen den beiden logiksteps.
    das hat zudem den vorteil dass die interpolation sehr einfach ist, verglichen mit den 'echten' bewegungs-berechnungen. bei manchen spielen wie z.b. 2d RPG lief die logik manchmal nur 10mal die sekunde durch.

    Naja der Faktor Zeit macht mir hier echt Gedanken. Die Frage die sich mir hier steltl ist ja wie bekome ich die zeit vernünftig unter? Nehme ich mein 1. Rechenmodel ind em ich die Frames bis zur nächsten Logic genommen habe dann gibt es diese Ruckler hin und wieder, nur welcher Weg wäre dannd er Richtige, wäre es da ratsammer das die Logic einfahc die zu bewegende Entfernung angibt und sich die Frame dann ihre neue Position jedesmal über die Zeitd e rletzten Frame errechnet? Also in etwa so?

    aktuellerstep=(bewegung/logiczeit)*zeit des letzten frames
    

    Das erscheint mir irgendwie auch nicht so solide zu sein, vor allem wenn die FPS mal schwankt, und das Setzen auf die Position auf die das Quadrat solte ist ja auch ne Sache die sehr schnell zu eienr Art "zittern" führt weil das Quadrat ja dann Grundsätzlich mal den einen oder anderen Pixel am Ziel vorbeischießt.

    EDIT: Diese Variante funktioniert auch nicht. Ich wäre also für einen kleinen Denkanstoß echt dankbar, die Fakten der Bewegung sind Folgende: alle 40ms wird die Logic aufgerufen und fragt die Richtung ab, dann setzt sie eine bewegung in Gang die 12 Pixel pro Logiczyklus beträgt, beim nächsten Logiczyklus soll das Quadrat dann and er Stelle stehen die vom vorherigen Logiczyklus gesetzt wurde, und das tut es bei diveresen Rechnungen nicht, sobald ich das amche gibt es konstant ein leichtes zitetrn im Quadrat, und egal wie ich die Bewegung errechne, es kommen immermal kleine Ruckler. Da die Frames ja änderlich sind wie soll ich die bewegung pro Frame hinbekommenw enn nicht über die Frames bzw Zeit pro Frame (da sscheint ja nicht so ganz zu funktionieren).



  • ich mache das immer wie folgt:

    berechnung der Zeitdiffernz vom aktuellem, und vorherigem Frame ich nenne es mal dt. dt wird immer am anfang eines Frames berechnet, geht so am einfachsten. Die Neue Position meines Spielers ermittle ich num immer mit

    pos += speed*dt;

    Diese berechnung kann man übrigens immer machen, egal ob man sich nun bewegt, oder nicht. Wenn man sich nicht bewegt, muss speed einfach nur auf 0 gesetzt werden. Klappt eigentlich bei mir immer.

    Übrigens wenn pos die Position in Pixeln ist, und dt die Zeit in Sekunden, dann ist speed Pixel pro sekunde, dann kann man die Geschwindigkeit gut schätzen.

    Edit: deine methode, wie dein Programm aufgebaut ist hab ich noch nicht so ganz verstanden, z.B. was macht dein logiczyklus? Kann es sein, dass wenn dein logiczyklus aktiv ist, der rest deines spiels angehalten wird?



  • Der Logiczyklus ist eine Abfrage die Unabhängig von den FPS agiert, vor dem zeichnen jedes Frames wird sie abgefragt, wird aber nur alle 40ms bei mir abgefragt, dh egal wie oft meine Frames kommen ich habe immer fest 25mal/Sekunde eine abfrage der Tastatur egal ob ich 20 oder 60FPS habe. Bei dieser Abfrage legt der Logiczyklus dann fest wie weit und in welche Richtung sich das Quadrat bis zum nächsten Zyklus bewegen soll, und diese Bewegung wird dann von den Frames zwischen den Zyklen Animiert.

    EDIT:

    Deine Variante habe ich auchmal versucht, allerdings fand ich sie nicht sehr zufriedenstellend weil es imermal leichte Zeitschwankungen gibt und vor alelm bei dieser Berechnungsvariante das Quadrat nicht da ankommt wos hin soll, die Frames zwischen den Logiczyklen sind ja unetrschiedlich und somit müsste ich mich da anpassen, aber egal wie ichs dreh und wend eich bekomem keine Flüssige Animation hin, entweder das Quadrat macht irgendwann nen ruck, der für mich jetzt entgültig unerklärlich ist da ich das Programm eben von ner FPS berechnung auf einen FPS Counter umgestellt habe, oder das Quadrat zitetrt halt wenn ich die Fehlerkontrolle einschalte die es an die stelle setzt wo es sein sollte.



  • Ich denke immer noch, dass es etwas mit deinen Logic zyklen zu tun hat. Werden in den Logic zyklen die FPS berechnet? Vieleicht liegt es ja da dran?

    Also ich will dir ja jetzt nicht meine Lösung aufdrängen, aber ich weis zu 100% dass sie ruckelfrei funktioniert, weil ich sie schon mehr als einmal getestet habe. Hier mal mit etwas mehr Quelltext:

    while(!quit){
        static double time = 0, at = 0, dt = 0;
        at = time; //alte Zeit speichern
        time = getTime(); //Zeit in Sekunden vom Programmstart
        dt = time-at;
    
        if(keydown())
            speed = 1;
        else
            speed = 0;
    
        pos += dt*speed;
    }
    

    das ganze ist jetzt sehr eindimensional, aber es Funktioniert, kein Zweifel. Grade dadurch, dass man die zeit in die Berechnung mit einfließen lässt bügelt man übrigends die Zeitschwankungen aus, so dass es am ende eine sauberue Bewegung ohne hackeln oder unregelmäßigkeiten gibt, auch bei schwankenden FPS.

    Wenn du immer noch nicht überzeugt bist, poste doch bitte mal einen möglichst kleinen Teil deines Quelltextes, denn so richtig weiß ich immer noch nicht wie du die Sache angehst.



  • ich mach das so:

    ich hab ne klasse die gamestates handelt, der kann man einfach per set-methode sagen wie lang ein logic-tick sein soll, und die klasse führt die logik-funktionen eines gamestates nur aus, wenn so ein tick eben vorbei ist. so muss ich nicht überall eine global verfügbare variable haben die mir sagt, wie viel zeit vergangen ist. das sieht etwa so aus:

    class mygamestate : public gamestate
    {
    public:
      mygamestate()
      {
         //mygame isn pointer auf die gamestate verwaltungsklasse, die von selbiger gesetzt wird wenn das gamestate zum manager hinzugefügt wird.
         myGame->SetLogicUpdateInterval(0.2f); //jede 0.2te sekunde die logikfunktionen updaten
      }
    
      void Render();
      void Input();
      void Update()
      {
         irgendein_sprite->Move(0.5f,0.0f); //muss mir keine gedanken um zeit machen.
      }
    };
    
    class gamestatemanager
    {
       public:
          void Run()
          { 
              while(myGameIsRunning)
              {
                  myCurrentGamestate->Input();
                  if(zeit vorüber)
                     myCurrentGamestate->Update();
                  myCurrentGamestate->Render();
              }
          }
    };
    

    natürlich nicht wirklich so, das ist ja hässlich. aber man sollte sehen wie ich das meine.


  • Mod

    Krux schrieb:

    ...Klappt eigentlich bei mir immer.
    ...

    nein, das klappt nur bei streng linearen bewegungen. sobald man beschleunigung, oder kurven hat, wird die bewegung je nach framerate sehr unterschiedlich ausfallen.
    will man dass die bewegung unabhaengig von der framerate immer 'gleich', also so wie geplant, ablaufen, muss man in festen zeitschritten rechnen (oder recht komplexe mehrdimensionale formeln nutzen).

    btw. er bezieht sich vermutlich mit logictick auf diesen thread: http://www.c-plusplus.net/forum/viewtopic-var-t-is-21275.html 😉



  • Ja auf den beziehe ich mich, die sache mit der Logic ist auch nett, nur die Bewegung ist es halt leider nicht. Sag mir dochmal bitet Rapso wie du das so Berechnest, irgendwie muß man ja die Geschwindigkeit in das Frame bekommen, ich habs jetzt auch shcon so versucht das ich errechnet habe wieviel Zeit seit der letzten Logic vergangen ist und damit die Bewegung auf die 40ms verteilt habe, Gut das Quadrat komtm damit eigentlich da an wo ich es gerne hätte, aber das Bild wirkt nicht wirklich gleichmäßig. Und vor allem in Schrägen Bewegungen sieht das wirklich nicht schön aus und cih muß ehrlich sagend as Thema treibt mich echt zum Wahnsinn, mache ich alel Timer aus sieht es schön Flüssig aus (gut is klar gleiche Bewegung jedes Frame), sobald ich meine ganzen Rechnungen anwerfe (die zT echt nicht gerade wenig Variablen beinhalten) wird die Bewegung total ungleichmäßig da ja zwischend en Logicticks nicht imemr gleichviele Frames sind und somit die Geschwindigkeit des Quadrates pro Sekudne imemr gleich ist aber eben nicht pro Frame da varriert sie.



  • rapso meinte wohl auch einfach, die update zyklen komplett auszulagern und nur alle paar millisekunden zu updaten, je nachdem wie es dir beliebt.

    also nicht sowas wie du gerade machst:

    void update()
    {
    spieler.move(speedvector*time_elapsed);
    };
    

    sondern so:

    void update()
    {
       if(time_over)
          spieler.move(speedvector);
    }
    


  • Naja so ist es im Grunde ja auch der Zyklus ist aus einfachheits gründen beim rendern mit dabei hat aber ne abfrage ob er Zündet und gibt dann die bewegung aus, Hauptproblem ist halt die Ausgegebene Bewegung auf die Frames die bis zum Nächsten Zyklus komemn aufzuteilen so das das Quadrat eben nicht springt sondern sich gleichmäßig bewegt, also das Programm mithilfe der Speedanweisung und Zeitvariablen errechnet wohin das ganze bewegt werden muß, jedoch sind die Ergebnisse die ich bisher erreiche alels andere als flüssig da ständig kleinere Ruckler auftreten.


  • Mod

    das update und das rendern sollten zwei getrennte funktion(s streange) sein. was wenn man mal einen server macht der nicht rendert? was wenn man das update irgendwann in nem extra thread machen will, was wenn man was anzeigen will waehrend pause ist, sodass keine logikupdates gemacht werden etc.

    while(true)
    {
      CurrentTime ==..mytimerfunction()... ;//WICHTIG! zeit sollte einmal genommen werden weil sie sich sonst noch im loop mal aendern koennte und alle berechnungen kaputt machen koennte
      //logic...
      .
      .
      .
      CRenderData RD((LogicTime-CurrentTime)/LogicStepSize,Camera,...);
      Render(RD);
    }
    

    fluechtigkeitsfehler ausgenommen sollte man so die fuers rendern wichtigen informationen an den renderaufruf uebergeben koennen. beachte LogicTime>=CurrentTime, deswegen waere der blendwert zwischen zwei logikschritten beim ersten renderaufruf vermutlich (fast) 1.f und kurz bevor der naechste logic schritt stattfindet (fast) 0.f .

    irgendwelche klarheiten?



  • Naja im großen und ganzen ist mir das schon klar wie du das meinst, wobei mir das 1.f und 0.f nicht so ganz klar ist da ich das jetzt gerade mit Zeit nicht in Verbingung bringe. Hier mal die variante die ich benutze zzt vielleicht bringt das ein bischen mehr klärung in die sache.

    //Das hier befidnet sich in der Renderfunktion, der Aufrug erfolgt aus einer
    //Schleife heraus die erst Windowsmessages abarbeitet und dann rendern lässt
    
            static RECT wurfel={500,400,550,450};//Variable für die Position des Quadrates
    	static RECT referenzwurfel={500,400,550,450};//Referenzvariable für die Position des Quadrates
    
            //Counter für die Logicaufrufe und FPS
    	static double count=0;
    	static int count2=0;
    
            //Variablen die Bewegungsbefehle der Logic aufnehmen
    	static double left=0;
    	static double top=0;
    	static double right=0;
    	static double bottom=0;
    
            Variablen für Quadratspositionen in Gleitkomazahlen
    	static double wurfelbottom=450;
    	static double wurfelright=550;
    	static double wurfeltop=400;
    	static double wurfelleft=500;
    
            //Zeit für die Quadratbewegung
    	static double time=0;
    
            //Zwischenrechnungen
    	static double lefttmp=0;
    	static double righttmp=0;
    	static double toptmp=0;
    	static double bottomtmp=0;
    
            //aktuelle zeit holen
    	QueryPerformanceCounter((LARGE_INTEGER*)&logictiming);
    	logiczwischenrechnung=static_cast<int>(logictiming*skala);
    
            //if schleife, Logictimer wurde einmal zum start des programms noch
            //bevor die Schleife anfing geholt
    	if(logictimer<=logiczwischenrechnung)
    	{
                    //tiemr für bewegung resetten
    		time=0;
    
                    //Quadrat und Quadratskoordinaten in Kommastellen auf das
                    //referensQuadrat setzen
    		wurfel.bottom=referenzwurfel.bottom;
    		wurfel.left=referenzwurfel.left;
    		wurfel.right=referenzwurfel.right;
    		wurfel.top=referenzwurfel.top;
    
    		wurfelbottom=referenzwurfel.bottom;
    		wurfelleft=referenzwurfel.left;
    		wurfelright=referenzwurfel.right;
    		wurfeltop=referenzwurfel.top;
    
                    //Zwischenrechnungsvariablen resetten
    		lefttmp=0;
    		righttmp=0;
    		toptmp=0;
    		bottomtmp=0;
    
                    //if/else abfrage ob mehr als eine Logicschleife komen würde,
                    //setzt entsprechend einen multiplikator der die
                    //geschwindigkeit die bis zur nächsten logic gefahren werden
                    //muß hoch (kommt nur bei unter 25fps zur geltung)
    		int speedmulti=0;
    		if(logictimer+40<logiczwischenrechnung)
    		{
    			speedmulti=(static_cast<int>(logiczwischenrechnung)-logictimer)/40;
    			logictimer=logictimer+(speedmulti*40);
    		}
    		else
    		{
    			speedmulti=1;
    			logictimer+=40;
    		}
    
                    //Speedberechnung
    		int speed=12*speedmulti;
    		int speed2=12*speedmulti;
    
    		int frames=1;
    
                    //Bewegungsvariablen resetten
    		left=0;
    		top=0;
    		right=0;
    		bottom=0;
    
                    //if-else if Block (gekürzt) weist den bewegungsvariablen die
                    //Werte zu und setzt da referenzquadrat auf den wert den es bei
                    //der nächsten logic haben soltle
    		if(KEYDOWN(VK_LEFT) && KEYDOWN(VK_UP))
    		{
    			left-=(speed/frames);
    			right-=(speed/frames);
    			top-=(speed/frames);
    			bottom-=(speed/frames);
    
    			referenzwurfel.left-=speed2;
    			referenzwurfel.right-=speed2;
    			referenzwurfel.top-=speed2;
    			referenzwurfel.bottom-=speed2;
    		}
    		//kleienr Counter mit dem ich die Logicticks überwache
    		count2+=frames;
    	}
            //wenn kein logictick dran ist der das Quadrat eh auf den referenzpunkt
            //setzt wird es hier bewegt
    	else
    	{
                    //aktuelle Zeit holen und die zeit seit dem letzten logictick errechnen
    		long long timer;
    		QueryPerformanceCounter((LARGE_INTEGER*)&timer);
    		double tmp=skala*timer;
    		tmp=tmp-(logictimer-40);
    
                    //temporäre rechenvariablen die ausrechnen wie weit das Quadrat
                    //seit dem letzten logictick gekomemn sein müsste
    		double lefttmp2=(left/40.0)*tmp;
    		double righttmp2=(right/40.0)*tmp;
    		double toptmp2=(top/40.0)*tmp;
    		double bottomtmp2=(bottom/40.0)*tmp;
    
                    //double quadrat koordinaten bewegen(tmp2-tmp) tmp2 ist die
                    //absolute bewegung die seit der letzten log erfolgen soll, tmp
                    //ist die bewegung die seitd er letzten logic bereits erfolgt ist
    		wurfeltop+=(toptmp2-toptmp);
    		wurfelleft+=(lefttmp2-lefttmp);
    		wurfelright+=(righttmp2-righttmp);
    		wurfelbottom+=(bottomtmp2-bottomtmp);
    
                    //bereits bewegten wert neu setzen
    		lefttmp=lefttmp2;
    		righttmp=righttmp2;
    		toptmp=toptmp2;
    		bottomtmp=bottomtmp2;
    
                    //und die akteullen korodinaten auf das Quadrat übertragen
    		wurfel.top=static_cast<int>(wurfeltop);
    		wurfel.left=static_cast<int>(wurfelleft);
    		wurfel.right=static_cast<int>(wurfelright);
    		wurfel.bottom=static_cast<int>(wurfelbottom);
    	};
    //Ab hier wird geblitett und gezeiochnet usw
    

    Mir ist vollkommen klar das ich heir für ein 50x50 Quadrat unmengen Variablen benutze nur um es zu verschieben. Ich habe mich für Double Werte entschieden weil cih dachte das amchtd as ganze etwas flüssiger. Das ist die letzte variante die ich letzte Nahct gebaut habe, das Ruckeln ist zwar damit weitestgehend weg, dummerweise bewegt sichd er würfel damit irgendwie wie eine raupe, dh wenn ich mich zb in die shcräge bewege kann ich trotz vsync den würfel 2x sehen was ohne diese funktion und ohne rechnerei nicht passiert (habe ein referenzprogramm für die optik das einfach nen festen wert pro frame bewegt) und ich komme der Lösung (eienr schönen bewegung) nicht gerade näher stattdessen wird da sganze immer größer. Diese logic/bewegungsgeschichte hier liese sich auch in eine eigene Funtkion auslagern, da das programm aber sowieso nur ein testprogramm ist macht es für mich wenig sinn das ganze jetzt auszulagern da hier eh nur ein Quadrat bewegt werden soll.



  • Gut nun hab ichs, jetzt wird mir auch einiges klar. Ich hab das die ganze zeit falsch verstanden und mit der Vergangenenzeit komplett Rumgerechnet statt einfach in ner Schleife Wert x pro vergangene Zeit y zu adden, nu funktioniert alles einwandfrei und der code ist um einiges kürzer, danke nochmal für den Denkanstoß an alle.


Anmelden zum Antworten