2D Plattformer - Springen / Physik

Hag2bard

Bekanntes Mitglied
Halli Hallo :) ,
ich habe meine jump und meine Fall Logik nun einfach in meinem moveTimer integriert und die Performance ist bisher sehr gut.
Der Umbau hat max 10 Minuten gedauert. Ich baue das ganze nun noch zu einem Runnable um.
Das Problem war wahrscheinlich dass die Klasse Timer arbeitet wie sie halt arbeitet, threadsicher ist und mich das viel Performance gekostet hat.

Ein Timer ist aber dennoch ein neuer Thread oder?

edit:

Ich dachte dass Threads ein weniger schneller sind, aber ich habe mit einem Thread hier ernsthafte Probleme. Der hat statt vorher 64FPS nun um die 11.500.000 FPS und dementsprechend scheint er bei mir auch nicht mehr nur 300 Pixel hoch sondern 7.382.245 Pixel.
Irgendwas muss ich mir da überlegen :D
 
Zuletzt bearbeitet:

httpdigest

Top Contributor
Zu der ganzen "Frames per Second"-Thematik als Performance-Metrik lässt sich auch nur folgendes sagen:
Nutze nicht FPS als Metrik für die Performance! Sondern verwendet "Frame Time", also nicht FPS sondern SPF (oder eher Milliseconds per Frame).

Das Problem mit FPS ist, es ist eine nicht-lineare Metrik und man sollte nicht aus allen Wolken fallen, wenn man vorher 1000 FPS hatte und jetzt etwas rendert und "nur" noch 500 FPS hat.
Siehe: https://www.mvps.org/directx/articles/fps_versus_frame_time.htm
 

Hag2bard

Bekanntes Mitglied
Ich weiß die PaintComponent Methode ist nicht das optimalste, aber gehen wir mal davon aus, dass ich diese momentan noch so stehen lasse. Zähle ich dann in dieser Methode, wie oft sie aufgerufen wird um einen Aussagewert zu haben über die FPS bzw die FrameTime?
 
Y

yfons123

Gast
du nimmst den wert der seit dem letzten frame vergangen ist.. das ist der enizige aussagekräftige wert

schau dir mal Time.deltaTime in unity an.. bzw wie das auch immer in Javafx bei fxgl heißt
 

Hag2bard

Bekanntes Mitglied
Ich stehe auf dem Schlauch.
Dieser Code sollte doch korrekt sein, oder nicht?:

Java:
    @Override
    protected void paintComponent(Graphics g) {
        g.drawString(String.valueOf(System.nanoTime()-last_time), 50, 80);
        last_time = System.nanoTime();
        .....
            ...
        }
 
Y

yfons123

Gast
System.nanoTime()
das rufst du 2 mal zu unterschiedllichen zeitpunkten auf.. das ist abgesehen davon dass es nicht performant ist auch noch fehlerhaft

was sind denn die werte die dir gdrawstring ausgibt? müssen ja kleine sein
 

Hag2bard

Bekanntes Mitglied
Es schwankt zwischen 7-8ms. Wenn ich aber das Fenster verschiebe oder der Charakter läuft, dann ist die FrameTime höher, bei bis zu 12ms.

Java:
    @Override
    protected void paintComponent(Graphics g) {
        last_time = System.nanoTime();
        super.paintComponent(g);
        paintLayer(g, mapLayer1);                                                                                       //Layer1 wird gezeichnet
        paintLayer(g, mapLayer2);                                                                                       //Layer2 wird gezeichnet
        paintHero(g, mario.getDirection(), mario.getFeetPosition(), mario.getPositionX(), mario.getPositionY());                   //Mario wird gezeichnet, je nach Position, Blickrichtung, Fußposition (Lauf, Sprung, Fall)

        if (counter > 10) {
            delta_time = (System.nanoTime() - last_time) / 1000000;
            counter = 0;
        }
        g.drawString("FrameTime: " + delta_time, 50, 50);

        counter++;
    }
 
Y

yfons123

Gast
natürlcih ist deine framerate schlechter wenn du etwas tust

trotz deines game zykluses musst du ja alles durchlaufen da du nicht multithreaded bist
dh das nächste frame kann erst gezeichnet werden wenn das letzte fertig ist... mit 12ms hast du 83fps das sollte reichen ..swing ist keine game engine
 

Hag2bard

Bekanntes Mitglied
Aber deine Aussage lautet nicht Java ist kein C++ oder?
Kann man mit einer Java Game Engine bessere Werte erzielen?
Wenn ja, was machen diese anders?

Meine GameLoop läuft in einem extra Thread, sonst habe ich keine Threads verwendet.
Wahrscheinlich wird euch übel wenn ihr meinen Code seht aber ich wusste nicht, wie ich es anstellen soll.

Auszüge aus Code, bei denen ich skeptisch bin:

Java:
public class Canvas extends JPanel {
   
    ...
               
public Canvas() {
        physics = new Physics(this);
        physics.startAnimation();
}
}
...

 public Physics(Canvas canvas) {
        this.canvas = canvas;
        animationRunnable = new Runnable() {
            @Override
            public void run() {
                while (isRunning) {
                    if (mario.getSpeed() > 0) {
                        if (isJumping || isFalling) {
                            mario.setFeetPosition(2);                                                                   //FeetPosition 2 beim Jump heißt, dass Mario rechts guckt
                        }
                        if (mario.getPositionX() >= mario.getFinalPositionX() && !isJumping && !isFalling) {            //Wenn 16 Pixel/halber Block erreicht dann feetPosition changen
                            mario.changeFeetPosition();
                            mario.refreshFinalPositionX();
                        }
                    }
                    if (mario.getSpeed() < 0) {
                        if (isJumping || isFalling) {
                            mario.setFeetPosition(1);                                                                   //Wenn Mario links guckt, muss das entsprechende Bild gezeichnet werden
                        }
                        if (mario.getPositionX() <= mario.getFinalPositionX() && !isJumping && !isFalling) {            //Wenn 16 Pixel/halber Block erreicht dann feetPosition changen
                            mario.changeFeetPosition();
                            mario.refreshFinalPositionX();
                        }
                    }
                    mario.move(mario.getSpeed());
                    //////////////////////////////////// MOVETIMER VORBEI
                    if (isJumping) {
                        mario.setDirection(Direction.UP);
                        timerDrivenJumpMethod();
                    }
                    if (isFalling) {
                        mario.setDirection(Direction.DOWN);                                                             //Wenn Mario fällt, dann setze Direction auf "down"
                        timerDrivenFallMethod(jumpDownTimerSpeed);
                    }
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        animationThread = new Thread(animationRunnable);

    }


    public void startAnimation() {
        animationThread.start();
    }
 
Y

yfons123

Gast
welche sprache du hernimmst ist halt erstmal komplett egal

nur um mal anzumerken... unity hat 3 render pipelines,

unreal engine hat ein paar gigabyte an code
da steckt schon ein bissel mehr fine tuning für renderen und zeichnen dahinter als wie bei swing wo es egal ist, sehr wahrscheinlich liegen die performance probleme an der paint methode und was swing macht

unity hatte auch genau sowas ( imGUI ) das auch jedes frame gezeichnet wurde und es hat einfach nur performance gefressen und wird nur noch im editor ( wo es keine performance gibt ) benutzt
 

Hag2bard

Bekanntes Mitglied
Aber wenn ich das Konzept halbwegs durchblickt habe, dann würde man auf dem Bildschirm nichts sehen, wenn paintComponent für eine Sekunde aussetzen würde, oder nicht?
Wie also soll gewährleistet werden, dass nicht jedes Frame gezeichnet wird?
Ich stoße paintComponent mit einem repaint() an, sobald sich der Charakter 1 Pixel bewegt.
Aber weniger als das ist doch eigentlich garnicht möglich, oder?
 
Y

yfons123

Gast
t einem repaint() an, sobald sich der Charakter 1 Pixel bewegt.
da ist dein performance fresser... du hast es falsch rum gemacht

warum solltest du bei jedem pixel deinen kompletten bildschirm neu zeichnen?

wenn du deinen charakter zb mit Vektor( 0 , 10 ) dh 10 nach rechts bewegen willst

dann würde die berechnung so aussehen

character.Move(new Vector2(input.x * last_time, input.y * last_time);
somit skalierst du den vektor runter auf die frame rate... und nicht anders rum.. du hast es verkehrt herum gemacht
 

Hag2bard

Bekanntes Mitglied
Ich werde aus deiner Antwort nicht schlau.
Ich habe gerade mal testweise in meine paintComponent Methode eine if Bedingung eingebaut, die das Bild und den Charakter nur zeichnen soll, wenn der boolean repaint auf true ist und nach dem Zeichnen den boolean dann direkt auf false gesetzt. So zeichnet er nur wenn sich der Charakter bewegt. Und zwar die neue Position des Charakters und den dadurch veränderten Hintergrund. Alle repaint() habe ich mit repaint = true ersetzt. Leider war das ganze mega ruckelig, weil er die paintComponent Methode von sich aus nur recht selten aufruft.
Also muss er doch bei jedem Pixel den der Charakter sich bewegt repaint() ausführen, sonst ruckelt es.

Aus der Berechnung werde ich auch nicht schlau.

edit: Wenn ich die super.paintComponent(g) Zeile aus meiner paintComponent Methode rauslasse, gewinne ich eine ms FrameTime.
Was macht denn dieser Aufruf?
Ich versteh auch nicht warum man das ganze mit irgendwelchen Vektoren machen muss. Ich möchte meinen Charakter doch einfach nur in gewisse Richtungen bewegen. Und das funktioniert ja auch soweit, wenn ich nach Rechts drücke, bewegt er sich nach rechts, positionX wird erhöht und nach jeder Erhöhung (nach jedem Pixel) wird neu gezeichnet, damit die Bewegung flüssig aussieht.
Für was brauche ich hier jetzt einen Vektor?
 
Zuletzt bearbeitet:

mihe7

Top Contributor
Ich werde aus deiner Antwort nicht schlau.
Es geht vermutlich darum, dass die Geschwindigkeit der Bewegungen von der Zeit abhängig sein sollen.

Du möchtest z. B. ein Sprite von Position (150, 300) innerhalb einer Sekunde nach (350, 300) (hier in Pixel) bewegen, d. h. mit einer Geschwindigkeit von 200 Pixel/s. Dann darf es keine Rolle spielen, ob der Rechner 20 FPS oder 200 FPS schafft: nach einer Sekunde muss die Bewegung abgeschlossen sein.

Wenn der Rechner pro Frame 10ms benötigt, er also 100 FPS schafft, dann musst Du je Frame das Sprite um 2 Pixel bewegen. Schafft er nur 50 FPS, musst Du das Sprite um 4 Pixel bewegen usw.

Und ja: je geringer die FPS, desto höher die Wahrscheinlichkit, dass die Bewegung "rucklig" wird :)

Ich versteh auch nicht warum man das ganze mit irgendwelchen Vektoren machen muss.
Du musst gar nichts explizit mit Vektoren machen, sie vereinfachen halt einiges.
 

Blender3D

Top Contributor
Ich dachte dass Threads ein weniger schneller sind, aber ich habe mit einem Thread hier ernsthafte Probleme. Der hat statt vorher 64FPS nun um die 11.500.000 FPS und dementsprechend scheint er bei mir auch nicht mehr nur 300 Pixel hoch sondern 7.382.245 Pixel.
11.500.000 FPS Du hast wohl den besten Computer aller Zeiten. ;)
Nein da machts Du grundlegend was falsch beim Zählen. Du zählst die Bilder die gezeichnet werden. Das machst Du immer in der Methode
Java:
public void render( Graphics g ){
    ..
    frames++;  
}
nachdem alles erledigt ist. Im GameLoop überprüfe ob 1000 ms vergangen sind und sichere die Anzahl der gezeichneten Frames in einer Variable.
Java:
public void run(){
    ..
    if (System.currentTimeMillis() - frameTime > 1000) {
                frameCount = frames;
                frames = 0;
                frameTime = System.currentTimeMillis();
    }
}
 

Blender3D

Top Contributor
Du möchtest z. B. ein Sprite von Position (150, 300) innerhalb einer Sekunde nach (350, 300) (hier in Pixel) bewegen, d. h. mit einer Geschwindigkeit von 200 Pixel/s. Dann darf es keine Rolle spielen, ob der Rechner 20 FPS oder 200 FPS schafft: nach einer Sekunde muss die Bewegung abgeschlossen sein.
Das stimmt.
Die Schwierigkeit ist die Framerate konstant zu halten. Das geht nicht immer.
Java:
public void run() {
    ..
        int sleepTime = 12; // ca 80 fps
    while (running) {   
        update();  // vebrauchte Zeit für Update
        render(); // verbrauchte Zeit für Zeichnen
        ..
        Thread.sleep(sleepTime ); // hier muss die update und render Zeit abgezogen werden
        // falls seepTime < 0 gibt der Thread nicht ab
        // --> periodisch ist dies mittels Thread.yield() anzustoßen
        // außerdem müssen die verlorenen Frames gezählt werden, um update entsprechend oft
        // nachzuholen. Dadurch bewegen sich die Objekte mit der gewünschten Zeit auch wenn
        // beim Zeichnen nur 70 fps erreicht wurden.
Wie Du siehst ist Optimierung beim GameLoop essentiell.
Man kann zwar auf den 1ten Blick eine flüssige Animation erreichen. Sobald aber im Hintergrund Resourcen von der Virtuell Machine gebraucht werden kommt es zum Ruckeln. Die Anzeige für die fps ist für die Entwicklung des Spieles sehr wichtig.
z.B.
Alles läuft flüssig.
Ich erweitere meine Gamelogik --> update benötigt länger wie bisher. Beim Testen merke ich aber nicht, dass ständig Frames verlorengehen, da erst in einigen Minuten eine Reaktion erflolgt.
 

Hag2bard

Bekanntes Mitglied
Auf die Antworten werde ich noch eingehen, momentan bin ich aber noch etwas am Ausprobieren an meinem alten Code.
In paintComponent() rufe ich 4 Methoden auf.
Java:
            super.paintComponent(g);
          paintLayer(g, mapLayer1);                                                                                       //Layer1 wird gezeichnet
          paintLayer(g, mapLayer2);                                                                                       //Layer2 wird gezeichnet
          paintHero(g, mario.getDirection(), mario.getFeetPosition(), mario.getPositionX(), mario.getPositionY());        //Mario wird gezeichnet, je nach Position, Blickrichtung, Fußposition (Lauf, Sprung, Fall)

Diese Methoden rufe ich nun von einer selbst erstellten doRepaint(); Methode auf.
Das Graphics Objekt aus der Methode paintComponent gebe ich an eine Instanzvariabel weiter, insofern sie noch NULL ist.

Java:
 protected void paintComponent(Graphics g) {
     ...
                 if (graphics == null) {
            System.out.println("Graphics Objekt wurde gesetzt");
            graphics = g;
                 }
Alle repaint() in meinem Code wurden durch doRepaint() ersetzt.


Das Problem ist aber, dass das doRepaint() nicht funktioniert. Er springt in die Methode rein und kann auf das graphics-Objekt zugreifen, aber das Bild wird dennoch nur dann neu gezeichnet wenn die paintComponent() Methode aufgerufen wird, z.B.: durch Minimieren/Maximieren des Fensters.
Wieso funktioniert das nicht?
Ich habe versucht das paintComponent() zu übergehen, da ich schauen wollte ob das Performance bringt und weil ich einfach wissen wollte, wie oft eigentlich das manuelle repaint() während der Laufzeit aufgerufen wird.
Das Ergebniss:

Java:
 public void move(int speed) {                           //Bewegt Charakter in angegebener Geschwindigkeit (- oder +)
    positionX += speed;
    if (speed != 0) {
        canvas.doRepaint();
    }

Ich habe die If Anweisung eingebaut, weil er sonst das repaint() sehr oft aufgerufen hätte.
Klar, bei Geschwindkeit 0 muss nicht neu gezeichnet werden.

Aber eigentliche Frage von mir:
Wieso funktioniert meine doRepaint() Methode nicht?
 

KonradN

Super-Moderator
Mitarbeiter
Wieso funktioniert meine doRepaint() Methode nicht?
Die Fenster werden nur in den paint / paintComponent Methoden gemalt. Du kannst nicht mitten drin da etwas malen. Das Graphics Objekt ist nur während der Methode, der es als Parameter übergeben wurde, gültig.

Das Graphics Objekt wird nach dem Durchlaufen der Malroutinen auch freigegeben. (Dazu dient die Methode dispose() )

Was ich da etwas vermisse ist das implementieren von (Auto)Closable. Das hätte ich etwas erwartet, aber das ist nicht der Fall. Das wäre ein gutes und sicheres Zeichen, dass diese Objekte eine klar begrenzte Lebenszeit haben und man nicht auf solche Ideen kommen sollte (Aber im Java Umfeld gibt es da deutlich weniger Bewusstsein für sowas. Das ist bei .Net mit dem IDisposable deutlich anders meiner Meinung nach).
 

Hag2bard

Bekanntes Mitglied
Danke für die interessante Aufklärung.
Ok das ergibt tatsächlich Sinn.
Ich habe, ich weiß nicht mehr wo, davon gelesen, dass es nicht so sinnvoll ist die paintComponent() Methode zu benutzen.
Wie mach ich das denn anders?
 

KonradN

Super-Moderator
Mitarbeiter
Ich weiss jetzt nicht was Du in welchem Zusammenhang gelesen hast. Generell kann es problematisch sein, im UI Thread alle Maloperationen durchzuführen.

In #31 habe ich ausgeführt, wie ich das bei einem Programm von mir (vor ca. 20 Jahren?) mal gemacht habe. Ich habe im eigentlichen Thread ein Bild gemalt und dieses dann dem UI Thread zur Verfügung gestellt / repaint aufgerufen. Der UI Thread hat also nur eine Sache gemacht: Das Bild dargestellt. Das geht sehr fix.

Das alles kann man dann auch gut optimieren:

a) Wenn Du z.B. ein Jump & Run Spiel hast, dann kannst Du den Level mit allen statischen Elementen einmal malen in einem größeren Bild um dann nur den Ausschnitt zu kopieren in das zu malende Bild.

b) Du kannst Objekte in Gruppen unterteilen. Jede Gruppe muss nur gemalt werden, wenn sich die Position eines Elements verändert hat. Und man kann ggf. mit mehreren Threads arbeiten die dann jeweils eine Gruppe von Objekten in ein eigenes Bild malen um dann am Ende nur noch die Bilder zusammen zu führen. (Das kann wohl gut helfen bei guten Rechnern und sehr vielen Objekten.)

Das sind aber dann weitergehende Überlegungen. In erster Linie wäre die Überlegung, ob Du den Ansatz wie in #31 übernehmen möchtest.
 

Hag2bard

Bekanntes Mitglied
Der Ansatz ist eine Überlegung wert, ich werde das mal erstmal auf Papier bringen.
Aber ein paar Fragen habe ich noch:

Wenn du davon sprichst mehrere Bilder zu haben,

1. in welcher Form sollen die denn sein? BufferedImage oder Graphics?
2. Wie kopiere(klone?) ich diese Bilder? Einfach mit dem Zuweisungsoperator = ?
3. Das Synchronisieren müsste ich dann mal ausprobieren und hier nochmal nachhaken ob ich es richtig gemacht habe
 

KonradN

Super-Moderator
Mitarbeiter
1. Das wären dann BufferedImages - zum malen lässt Du dir dann eine Graphics Instanz geben.
2. nein, du malst die Bilder wie gewohnt mit Hilfe eines Graphics Objekts. Und BufferedImage hat einen Konstruktor
 

Hag2bard

Bekanntes Mitglied
Meine Klasse die alles berechnet soll laut deiner Aussage auch die Bilder malen.
Ist dieser Ansatz richtig:

Java:
    private Thread physicsThread;
    private Canvas canvas;

    public Canvas() {    //Konstruktor Start
        canvas = this;
physicsThread = new Thread(new Runnable() {
    @Override
    public void run() {
        physics = new Physics(canvas);
    }
});
physicsThread.start();
        physics.startAnimation();  //Läuft diese Anweisung im physicsThread?

Wie im Code kommentiert, werden Methoden welche ich auf das physics Objekt aufrufe durch den PhysicsThread gehandelt?
 

KonradN

Super-Moderator
Mitarbeiter
Dein Code sagt mit nichts und ich verstehe gerade auch nicht, was Du da machst. Du hast in der Regel eine Gameloop und die macht alles: Änderungen berechnen, Erzeugung der Darstellung, ggf. kurze Pause und dann alles erneut.
 

Hag2bard

Bekanntes Mitglied
Ich stehe gerade vor einem Problem.
Ich möchte gerade den Code dafür schreiben, dass in mein buffImageC (das Bild was gerade gemalt wird siehe Post 31) gemalt wird.
Wie stell ich das an?
Mit dem Befehl graphics.drawImage kann ich ja nur das Graphics Objekt malen lassen, aber wie greife ich auf das BufferedImage zu?
Meine Idee war ein neues Graphics Objekt erstellen und darauf dann zu zeichnen und das in ein BufferedImage umwandeln.
Aber ich kann kein Graphics Objekt erstellen, da dies eine Abstrakte Klasse ist.

Also wie handle ich nun, dass in ein BufferedImage gezeichnet wird?
 

KonradN

Super-Moderator
Mitarbeiter
Ich verstehe gerade Dein Problem nicht.

Du kannst für Deine Spielszene ein BufferedImage erzeugen. Dazu hat die Klasse Konstruktoren. Du musst halt die Größe vom Control haben auf dem du das dann malen willst. (Also nicht immer neue BufferedImages erzeugen sondern diese immer wiederverwenden!)

Wenn Du in das BufferedImage malen willst, dann erzeugst Du Dir ein Graphics Objekt mittels createGraphics Aufruf.
Wenn das Malen des Bildes fertig ist, dann rufst Du auf dem graphics Objekt dispose() auf.

In paint / paintComponent kannst Du dann einfach das vorher gemalte Bild malen. Dazu nutzt du das graphics Objekt, das Du als Parameter bekommen hast und rufst z.B. drawImage auf.
 

Blender3D

Top Contributor
Ich weiss jetzt nicht was Du in welchem Zusammenhang gelesen hast. Generell kann es problematisch sein, im UI Thread alle Maloperationen durchzuführen.
Er bezieht sich auf meinen Post #46.
Dabei geht es um aktives Rendern ( ohne paintComponent ) und passives Rendern mit paintComponent.
Für möglichst konstante Frameraten ist passives Rendern nicht so gut geeignet.
Hier ein Beispiel zum Testen. Das Spiel verwendet aktives Rendern.
https://www.java-forum.org/thema/spielesammelthread.123839/#post-1288802
 

Hag2bard

Bekanntes Mitglied
Ich habe jetzt mal versucht den Vorschlag von KonradN aus Post #31 umzusetzen, aber ich habe sehr sehr große Performance-Probleme und Grafikfehler.

Beim Bewegen passiert folgendes:

1655203222441.png
Außerdem ist es nun sehr sehr träge.

Also diese Vorgehensweise scheint für 2D Spiele ungeeignet zu sein.

Java:
    @Override
    protected void paintComponent(Graphics g) {
        graphics = g;
        super.paintComponent(g);
        if (physics.getBuffImageB() != null) {                                                                          //Liegt neues Bild in B vor?
            if (!physics.isDoRepaintActive) {
                System.out.println("Neues Bild liegt zum painten bereit!!!!!!!!!!!!!!");
                physics.setBuffImageD(physics.getBuffImageA());                                                             //Altes Bild wird zum Malen nach D geschoben
                physics.setBuffImageA(physics.getBuffImageB());                                                             //Da neues Bild in B, wird dies zu A
                physics.setBuffImageB(null);                                                                                //Bild B wurde zu A, also ist B wieder null
            }
        }
        g.drawImage(physics.getBuffImageA(), 0, 0, null);       
    }

Dieses hin und her verschieben der Objekte klang für mich auch erstmal sehr gut, aber leider leidet die Performance darunter.
 

KonradN

Super-Moderator
Mitarbeiter
Gerade Grafikfehler können da eigentlich nicht auftreten. Das dürfte also ein sicheres Zeichen sein, dass Du da etwas falsch gemacht hast.

Aus dem Code, den Du da zeigst, ist aber auch nicht ablesbar, was z.B. die Performance-Probleme verursachen könnte.
 

Blender3D

Top Contributor
Also diese Vorgehensweise scheint für 2D Spiele ungeeignet zu sein.
Ich kann Dich wiederum nur auf meinen Post #46 verweisen.
https://www.java-forum.org/thema/2d-plattformer-springen-physik.197673/#post-1312547
Des Weiteren wenn Du unbedingt bei paintComponent bleiben willst, dann solltest Du die Variante vom DuckyGame wählen.
@Override protected void paintComponent(Graphics g) { graphics = g; super.paintComponent(g);
Du speicherst hier das Graphics Objekt in eine (?) Klassenvariable. Das ist keine gute Idee. Der Aufruf von paintComponent() wird durch z.B. repaint() und anderen Ereignissen veranlasst. Damit das dort verwendete Graphics Objekt unbeschwert intern bearbeitet werden kann.
Das bedeutet der Zugriff darauf sollte innerhalb von paintComponent passieren. Du reichst es aber nach außen und greifst wahrscheinlich darauf zu. ---> Daher könnten Deine Artefakte stammen.
 
Zuletzt bearbeitet:

Hag2bard

Bekanntes Mitglied
Nein, ich möchte nicht unbedingt bei paintComponent bleiben, ich möchte das Spiel auf Performance trimmen.
Die Klassenvariable habe ich nun entfernt, sie war ungenutzt.

Wie soll ich denn die Threads verwenden? Soll dieser Verschiebevorgang in einem neuen Thread passieren?
 

KonradN

Super-Moderator
Mitarbeiter
Soll dieser Verschiebevorgang in einem neuen Thread passieren?
Bei meiner Idee finden die Verschiebevorgänge automatisch statt, wenn die Zugriffe darauf erfolgen. Damit mehrere Threads da parallel keinen Unsinn machen können, müssen diese Zugriffe teilweise in synchronisierten Blöcken stattfinden. Und Variablen, die von mehreren Threads verwendet werden, sollten volatile sein.

Der Status deiner Gameloop ist dem UI Thread dabei egal. Also ob da gerade ein neues Bild gemalt wird oder nicht (oder was immer du in physics.isDoRepaintActive hinterlegst) ist dabei also uninteressant.

Ich kann Dir später einmal eine kleine Klasse zeigen, wie es aussehen könnte (ungetestet - habe kein Spiel mehr, in dem ich sowas testen könnte). Das was ich Dir bieten kann sind nur meine Erfahrungen damals mit meinen Lösungen, die halt auch eine saubere Aufteilung ermöglicht haben.


ABER: Immer bedenken: Meine Erfahrung ist ggf. veraltet und nicht optimal. Die Aussagen von @Blender3D solltest Du auf jeden Fall versuchen (im Detail) zu verstehen. Damit - gerade in Zusammenhang mit existierenden Spielen / Codes - dürftest Du auch sehr weit kommen können vermute ich.
 

Hag2bard

Bekanntes Mitglied
Ich versuche mich mal nach dem Post von @Blender3D zu richten, da ich so wirklich nicht mehr weiter komme.
Das Artefakte auftauchen verstehe ich nicht und außerdem flimmert das Bild.
 

Hag2bard

Bekanntes Mitglied
Ich habe den Code erstmal wieder zurückgebaut.
Wenn ich wüsste wie eine Versionsverwaltung in intelliJ funktioniert, könnte ich das alte posten.
 

KonradN

Super-Moderator
Mitarbeiter
Wenn ich wüsste wie eine Versionsverwaltung in intelliJ funktioniert, könnte ich das alte posten.
Versionsverwaltung ist erst einmal unabhängig von der IDE. Die IDEs haben lediglich (meist per Plugins) eine Unterstützung für diverse Versionsverwaltungen.

Das Tool, das derzeit wohl am meisten eingesetzt wird, ist git. Das ist eine freie Software zu der es auch zwei (gute) freie Bücher gibt. Der Einstieg ist relativ einfach so man alleine arbeitet vor allem auf einem System. Etwas komplexer wird es nur, wenn man anfängt zu mergen.
 

KonradN

Super-Moderator
Mitarbeiter

Hag2bard

Bekanntes Mitglied
Hallo,

ich habe mir gestern mal ein wenig Ruhe gegönnt vor diesem Problem "Java und Spiele".
Ich habe das Ding mit der Versionsverwaltung hinbekommen.

Ich habe 3 wichtige Klassen und habe es momentan wieder so gebaut, wie in Post #31 beschrieben.
Problem hierbei:
Flimmern
Hier ist mein Code:


Canvas.java
Java:
package Pokemon;

import PokemonEditor.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;

public class Canvas extends JPanel {    //Klasse Start

    private BlockArrayList mapLayer1;
    private BlockArrayList mapLayer2;

    private BufferedImage tilesetBufferedImage;
    private final BufferedImage spritesBufferedImage;
    private final int ZOOM = 4;                                                                                         //TODO muss noch eingebaut werden 4 ist Standard
    private final int TILESIZE = 16;
    private final int OFFSET = 11 * ZOOM;                                                                               //umso höher umso höher zeichnet er
    //    private final Physics physics;                                                                                //ohne thread
    private Physics physics;                                                                                            //mit thread
    private final Hero mario;
    //    private final MediaPlayer mediaPlayer;
    private final LoadMap loadMap;
    private boolean isPressingRightButton = false;
    private boolean isPressingLeftButton = false;
    private boolean isPressingSpaceButton = false;
    private final Thread physicsThread;
    private final Canvas canvas;
    private long last_time;
    private long delta_time;
    private int counter;
    private BufferedImage mapBufferedImage = new BufferedImage(1024, 860, BufferedImage.TYPE_INT_ARGB);
    private Graphics mapGraphics = mapBufferedImage.createGraphics();
    private boolean accessingBufferedImageHeroB = false;

    BufferedImage bufferedImageHeroA;
    BufferedImage bufferedImageHeroB;
    BufferedImage bufferedImageHeroC;
    BufferedImage bufferedImageHeroD;
    Graphics graphicsHero;

    Thread refreshBufferedImagesThread = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                if (mario.heroDataChanged()) {
                    refreshHeroBufferedImage(mario.getDirection(), mario.getFeetPosition(), mario.getPositionX(), mario.getPositionY());        //Mario wird gezeichnet, je nach Position, Blickrichtung, Fußposition (Lauf, Sprung, Fall)
                    System.out.println("refreshed");
                } else {
                    try {
                        Thread.sleep(0);                                                                          //Workaround, sonst //TODO
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

    });

    public Canvas() {    //Konstruktor Start
        canvas = this;
        physicsThread = new Thread(() -> physics = new Physics(canvas));
        physicsThread.start();
//        physics = new Physics(this);  //Ohne Thread
        keyBinding();
        System.out.println(physics);
        mario = new Hero(this, physics);
        physics.startAnimation();
        loadMap = new LoadMap();

        if (loadMap.getMapString() != null) {
            this.mapLayer1 = loadMap.getLoadedMap()[0];
            this.mapLayer2 = loadMap.getLoadedMap()[1];
        }

        try {
            tilesetBufferedImage = TilePanel.getExistingInstance().getBufferedImage();
        } catch (Exception e) {
            System.err.println("Konnte TilePanel-Image nicht holen");
            e.printStackTrace();
        }

        spritesBufferedImage = loadMap.getBufferedImage(GuiData.filenameSpriteSet);

        refreshBufferedImagesThread.start();
        refreshMapBufferedImage();

        setFocusable(true);
        requestFocusInWindow();
    }

    public void keyBinding() {
        InputMap im = getInputMap(WHEN_IN_FOCUSED_WINDOW);
        ActionMap am = getActionMap();

        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0, false), "pressedSpace");
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0, true), "releasedSpace");
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "pressedLeft");
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), "releasedLeft");
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "pressedRight");
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), "releasedRight");
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_R, 0, true), "pressedR");

        am.put("pressedSpace", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (!isPressingSpaceButton && !physics.getJumping() && !physics.getFalling()) {                   //Doppelsprung wird vermieden durch diese If Bedingung
                    isPressingSpaceButton = true;
                    if (mario.getDirection() == Direction.LEFT) {
                        mario.setFeetPosition(1);                                                                       //FeetPosition1 beim Jump heißt, er schaut links
                    }
                    if (mario.getDirection() == Direction.RIGHT) {
                        mario.setFeetPosition(2);
                    }
                    mario.setDirectionBackup(mario.getDirection());                                                     //FeetPosition2 beim Jump heißt, er schaut rechts
                    mario.doJump();
                }
            }
        });

        am.put("releasedSpace", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                isPressingSpaceButton = false;                                                                            // boolean der speichert, ob die Leertaste gerade gedrückt wird
            }
        });

        am.put("pressedLeft", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (!isPressingLeftButton) {
                    isPressingLeftButton = true;
                    mario.setSpeed(-mario.getWalkSpeed());                                                              //Geschwindigkeit auf negativen Wert setzen, bedeutet, dass Mario nach links läuft
                    mario.setDirection(Direction.LEFT);                                                                         //Wichtig für paintComponent
                    mario.setDirectionBackup(Direction.LEFT);
                    mario.refreshFinalPositionX();
                    if (!physics.getJumping() && !physics.getFalling()) {
                        mario.setFeetPosition(1);                                                                       //Er fängt immer mit einem Fuß vorne an zu laufen
                    }
                }
            }
        });

        am.put("releasedLeft", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                isPressingLeftButton = false;
                if (!physics.getJumping() && !physics.getFalling()) {
                    mario.setFeetPosition(2);
                }
                if (physics.getJumping() && physics.getFalling()) {
                    mario.setFeetPosition(1);                                                                           //Feet Position 1 bei Jump heißt er guckt links
                }
                repaint();
                if (!isPressingRightButton) {                                                                              //Wenn kein anderer Button gedrückt wird, dann Mario anhalten durch Geschwindigkeit 0
                    mario.setSpeed(0);
                }
            }
        });

        am.put("pressedRight", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (!isPressingRightButton) {
                    isPressingRightButton = true;
                    mario.setSpeed(mario.getWalkSpeed());                                                               //Geschwindigkeit auf positiven Wert setzen, bedeutet, dass Mario nach rechts läuft
                    mario.setDirection(Direction.RIGHT);                                                                        //Wichtig für paintComponent
                    mario.setDirectionBackup(Direction.RIGHT);
                    mario.refreshFinalPositionX();
                    if (!physics.getJumping() && !physics.getFalling()) {
                        mario.setFeetPosition(1);                                                                       //Er fängt immer mit einem Fuß vorne an zu laufen
                    }
                }
            }
        });

        am.put("releasedRight", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                isPressingRightButton = false;
                if (!physics.getJumping() && !physics.getFalling()) {
                    mario.setFeetPosition(2);
                }
                if (physics.getJumping() && physics.getFalling()) {
                    mario.setFeetPosition(2);                                                                           //Feet Position 1 bei Jump heißt er guckt links
                }
//                physics.doRepaint();
                repaint();
                if (!isPressingLeftButton) {                                                                               //Wenn kein anderer Button gedrückt wird, dann Mario anhalten durch Geschwindigkeit 0
                    mario.setSpeed(0);
                }
            }
        });

        am.put("pressedR", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
//                mario.setDirection(mario.getDirectionBackup());                                                         //Wenn während des Sprungs/Falls die Richtung durch Tastendruck gewechselt wird, wird das DirectionBackup damit überschrieben
//                mario.setFeetPosition(2);

                System.out.println("Pressed R!");
                refreshHeroBufferedImage(mario.getDirection(), 2, mario.getPositionX(), mario.getPositionY());
                canvas.repaint();

            }
        });
    }

    private void refreshMapBufferedImage() {
        for (int i = 0; i < mapLayer1.size(); i++) {
            mapGraphics.drawImage(canvas.getTilesetBufferedImage(), mapLayer1.get(i).getDestinationX() * TILESIZE * ZOOM, mapLayer1.get(i).getDestinationY() * TILESIZE * ZOOM, (mapLayer1.get(i).getDestinationX() + 1) * TILESIZE * ZOOM, (mapLayer1.get(i).getDestinationY() + 1) * TILESIZE * ZOOM, mapLayer1.get(i).getSourceX() * TILESIZE, mapLayer1.get(i).getSourceY() * TILESIZE, (mapLayer1.get(i).getSourceX() + 1) * TILESIZE, (mapLayer1.get(i).getSourceY() + 1) * TILESIZE, null);
        }
        for (int i = 0; i < mapLayer2.size(); i++) {
            mapGraphics.drawImage(canvas.getTilesetBufferedImage(), mapLayer2.get(i).getDestinationX() * TILESIZE * ZOOM, mapLayer2.get(i).getDestinationY() * TILESIZE * ZOOM, (mapLayer2.get(i).getDestinationX() + 1) * TILESIZE * ZOOM, (mapLayer2.get(i).getDestinationY() + 1) * TILESIZE * ZOOM, mapLayer2.get(i).getSourceX() * TILESIZE, mapLayer2.get(i).getSourceY() * TILESIZE, (mapLayer2.get(i).getSourceX() + 1) * TILESIZE, (mapLayer2.get(i).getSourceY() + 1) * TILESIZE, null);
        }
    }

    public void refreshHeroBufferedImage(Direction direction, int feetPosition, int positionX, int positionY) {
        if (System.currentTimeMillis() - last_time > 15 || !isPressingRightButton && !isPressingLeftButton) {           //Nur neu zeichnen wenn mehr als 15ms vergangen sind (66,66 FPS) oder er nicht bewegt wird (Workaround, da er nach dem Fallen nicht mehr die buffImages refresht hat
            bufferedImageHeroC = new BufferedImage(1024, 860, BufferedImage.TYPE_INT_ARGB);
            graphicsHero = bufferedImageHeroC.createGraphics();

            if (mario.getDirection().equals(Direction.LEFT)) {
                switch (mario.getFeetPosition()) {                                                                                                                        //+ZOOM=Korrektur
                    case 1 -> graphicsHero.drawImage(canvas.getSpritesBufferedImage(), positionX, positionY - OFFSET, positionX + TILESIZE * ZOOM, (positionY + TILESIZE * ZOOM) + ZOOM, 1, 91, 17, 118 + 1, null); //links Fuß vorn
                    case 2 -> graphicsHero.drawImage(canvas.getSpritesBufferedImage(), positionX, positionY - OFFSET, positionX + TILESIZE * ZOOM, (positionY + TILESIZE * ZOOM) + ZOOM, 19, 91, 35, 118 + 1, null); //links stehend
                }
            }
            if (direction.equals(Direction.RIGHT)) {
                switch (feetPosition) {                                                 // offset ist der Wert wieviel über 16 Pixel Block gezeichnet werden soll        //+ZOOM=Korrektur
                    case 1 -> graphicsHero.drawImage(canvas.getSpritesBufferedImage(), positionX, positionY - OFFSET, positionX + TILESIZE * ZOOM, (positionY + TILESIZE * ZOOM) + ZOOM, 1, 31, 17, 58 + 1, null); //rechts Fuß vorn
                    case 2 -> graphicsHero.drawImage(canvas.getSpritesBufferedImage(), positionX, positionY - OFFSET, positionX + TILESIZE * ZOOM, (positionY + TILESIZE * ZOOM) + ZOOM, 19, 31, 35, 58 + 1, null); //rechts stehend
                }
            }
            if (direction.equals(Direction.UP)) {               //Springen
                switch (feetPosition) {
                    case 1 -> graphicsHero.drawImage(canvas.getSpritesBufferedImage(), positionX, positionY - OFFSET - ZOOM, positionX + TILESIZE * ZOOM, (positionY + TILESIZE * ZOOM), 1, 1, 17, 28 + 1, null); //Springen links
                    case 2 -> graphicsHero.drawImage(canvas.getSpritesBufferedImage(), positionX, positionY - OFFSET - ZOOM, positionX + TILESIZE * ZOOM, (positionY + TILESIZE * ZOOM), 19, 1, 35, 28 + 1, null); //Springen rechts
                }
            }
            if (direction.equals(Direction.DOWN)) {             //Fallen
                switch (feetPosition) {
                    case 1 -> graphicsHero.drawImage(canvas.getSpritesBufferedImage(), positionX, positionY - canvas.getOFFSET() - ZOOM, positionX + TILESIZE * ZOOM, positionY + TILESIZE * ZOOM, 1, 61, 17, 89, null); //Fallen links
                    case 2 -> graphicsHero.drawImage(canvas.getSpritesBufferedImage(), positionX, positionY - canvas.getOFFSET() - ZOOM, positionX + TILESIZE * ZOOM, positionY + TILESIZE * ZOOM, 19, 61, 35, 89, null); //Fallen rechts
                }
            }
            mario.refreshHeroData();
            if (!accessingBufferedImageHeroB) {
                if (bufferedImageHeroB != null) {
                    accessingBufferedImageHeroB = true;
                    bufferedImageHeroD = bufferedImageHeroB;
                }
                accessingBufferedImageHeroB = true;
                bufferedImageHeroB = bufferedImageHeroC;
                accessingBufferedImageHeroB = false;
                bufferedImageHeroC = null;
            }
            last_time = System.currentTimeMillis();
        }
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        if (mapBufferedImage != null) {
            g.drawImage(mapBufferedImage, 0, 0, this);
        }
        g.drawString("GameLoops " + physics.getGameLoops(), 50, 50);

        if (bufferedImageHeroB != null) {
            bufferedImageHeroD = bufferedImageHeroA;
            if (!accessingBufferedImageHeroB) {
                accessingBufferedImageHeroB = true;
                bufferedImageHeroA = bufferedImageHeroB;
                bufferedImageHeroB = null;
                accessingBufferedImageHeroB = false;
            }
        }
        g.drawImage(bufferedImageHeroA, 0, 0, this);

    }


    /*
    Getter und Setter - Der spannendste Teil meines Codes
     */
    public BlockArrayList getMapLayer1() {
        return mapLayer1;
    }

    public BlockArrayList getMapLayer2() {
        return mapLayer2;
    }

    public BufferedImage getTilesetBufferedImage() {
        return tilesetBufferedImage;
    }

    public BufferedImage getSpritesBufferedImage() {
        return spritesBufferedImage;
    }

    public boolean isPressingRightButton() {
        return isPressingRightButton;
    }

    public boolean isPressingLeftButton() {
        return isPressingLeftButton;
    }

    public boolean isPressingSpaceButton() {
        return isPressingSpaceButton;
    }

    public int getOFFSET() {
        return OFFSET;
    }

    public int getZOOM() {
        return ZOOM;
    }

    public int getTILESIZE() {
        return TILESIZE;
    }

}


Physics:
Java:
package Pokemon;

import java.util.List;

public class Physics {


    private final double gravitation = 9.81;                //    m/s           //Veränderte Gravitation lässt Mario langsamer hoch gleiten
    private int pixelPerTimerPass = 1;                      //muss verändert werden für Geschwindigkeit,
    private final double timerPassInMs = 17;                           //so viele ms pro TimerDurchlauf
    private long timeElapsedInMs;  //TODO ersetzen durch gameLoopDurchgänge????
    private int jumpedPixelCounter = 0;
    private long fallStartTime;
    private long fallDuration = 0;
    private double startSpeedInMeterPerSecond = 11;                     //   11 braucht er für 300 hoch Dieser Wert bestimmt die zu erreichende Höhe
    private final int minimumSpeed = 4;              //3Mindestens 2 -> je höher dieser Wert, desto höher die Geschwindigkeit des Heros bei Kehrtwende von oben nach unten
    private final int jumpDownTimerSpeed = 30;
    private long start;
    private List<Integer> pixelPerTimerPassList;
    private Hero mario;
    private Canvas canvas;
    private long startTime = System.currentTimeMillis();
    private long duration = 0;
    private int counterFrames = 0;
    private boolean isJumping = false;
    private boolean isFalling = false;
    private boolean isRunning = true;                                                                                   //Gameloop
    Runnable animationRunnable;
    private Thread animationThread;
    private final int sleepDuration = 10;
    private int gameLoopCounter = 0;
    private int gameLoops = 0;
    private long startTime2 = 0;
    private long end;//nötig??

    // in 1 s = 58 Durchgänge = 58 Pixel
    // Mario 20 Pixel hoch = 2m?
    //Sprung = 300 Pixel = 30m?
    // Bei 58 Pixel = 5,8m pro Sekunde muss man für 20m/s oder 200 Pixel/s  gleich 20m/s durch

    public Physics(Canvas canvas) {
        this.canvas = canvas;
        animationRunnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("Aufruf GameLoop");
                startTime2 = System.currentTimeMillis();
                while (isRunning) {


                    if (canvas.isPressingLeftButton() && canvas.isPressingRightButton() && mario.getSpeed() == 0) {
                        mario.setFeetPosition(2);
                    }
                    if (mario.getSpeed() > 0) {
                        if (isJumping || isFalling) {
                            mario.setFeetPosition(2);                                                                   //FeetPosition 2 beim Jump heißt, dass Mario rechts guckt
                        }
                        if (mario.getPositionX() >= mario.getFinalPositionX() && !isJumping && !isFalling) {            //Wenn 16 Pixel/halber Block erreicht dann feetPosition changen
                            mario.changeFeetPosition();
                            mario.refreshFinalPositionX();
                        }
                    }
                    if (mario.getSpeed() < 0) {
                        if (isJumping || isFalling) {
                            mario.setFeetPosition(1);                                                                   //Wenn Mario links guckt, muss das entsprechende Bild gezeichnet werden
                        }
                        if (mario.getPositionX() <= mario.getFinalPositionX() && !isJumping && !isFalling) {            //Wenn 16 Pixel/halber Block erreicht dann feetPosition changen
                            mario.changeFeetPosition();
                            mario.refreshFinalPositionX();
                        }
                    }
                    mario.move(mario.getSpeed());
                    //////////////////////////////////// Ende des MoveEvents
                    if (isJumping) {
                        mario.setDirection(Direction.UP);                                                               //Wenn Mario springt, dann setze Direction auf "up" //TODO doppelt?
                        gameloopDrivenJumpMethod();
                    }
                    if (isFalling) {                                                                                    //TODO Dieser Code verhindert die Möglichkeit die Fußstellung noch vor dem Erreichen des Bodens zurückzustellen
                        mario.setDirection(Direction.DOWN);                                                             //Wenn Mario fällt, dann setze Direction auf "down"
                        gameloopDrivenFallMethod(jumpDownTimerSpeed);
                    }
                    try {
                        Thread.sleep(sleepDuration);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    /*
                    Durchgänge pro Sekunde bei etwa 64
                     */
                    gameLoopCounter++;
                    if (System.currentTimeMillis() - startTime2 >= 1000) {
                        gameLoops = gameLoopCounter;
                        canvas.repaint();
                        gameLoopCounter = 0;
                        startTime2 = System.currentTimeMillis();
                    }
                }
            }
        };
        animationThread = new Thread(animationRunnable);
    }
    
    public void startAnimation() {
        animationThread.start();
    }

    private void gameloopDrivenFallMethod(int jumpDownTimerSpeed) {
        setFallDuration(System.currentTimeMillis() - (getFallStartTime()));
        jumpDownTimerSpeed = (int) (jumpDownTimerSpeed / getGravitation()) * (int) (getFallDuration() / mario.getFallDelay());
        for (int i = 0; i < jumpDownTimerSpeed; i++) {
            mario.moveDown(1);                                                                                     //1 Pixel nach unten

            if (mario.getPositionY() == mario.getFinalPositionY()) {
                isFalling = false;
                setFallDuration(0);
                mario.setDirection(mario.getDirectionBackup());                                                         //Wenn während des Sprungs/Falls die Richtung durch Tastendruck gewechselt wird, wird das DirectionBackup damit überschrieben
                mario.setFeetPosition(2);                                                                               //FußPosition 2 -> Mario steht
                canvas.refreshHeroBufferedImage(mario.getDirection(), 2, mario.getPositionX(), mario.getPositionY());  //
                canvas.repaint();
                break;
            }
        }
        canvas.repaint();
    }

    private void gameloopDrivenJumpMethod() {
        setTimeElapsedInMs();     //Aktuell vergangene Zeit setzen

        for (int i = 0; i < getPixelPerTimerPass(); i++) {
            setTimeElapsedInMs();     //Aktuell vergangene Zeit setzen //TODO kann vielleicht weg
            refreshPixelPerTimerPass();

            //So viel PixelSprung pro Schleifendurchgang
            mario.moveUp(1);                                                                                       //eins nach oben pro Schleifendurchgang
            increaseJumpedPixelCounter(1);

            if (getPixelPerTimerPass() < getMinimumSpeed()) {                                                           //Methode 1: Wenn er weniger als 3 Pixel pro Durchlauf Geschwindigkeit hat, hört er auf -> unterschiedliche Sprunghöhen
//                if (getJumpedPixelCounter() == 288) {                                                                 //Methode 2: Wenn er 288 Pixel erreicht hat, stoppt er den JumpUpTimer -> wenn nie erreicht bleibt er stehen -> unterschiedliche Sprungzeiten
//            if (getDurationJump() >= getJumpTimeInMs()) {                                                             //Methode 3: Wenn Zeit abgelaufen, dann stoppt er den JumpUpTimer -> unterschiedliche Sprunghöhen?

                isJumping = false;                                                                                      //Jumping abgeschlossen
                mario.setFinalPositionY(mario.getPositionY() + getJumpedPixelCounter());                                //FinalPositionY berechnen durch aktuelle PositionY - Sprunghöhe
                System.out.println("gesprungene Pixel:");                                                               //debug
                System.out.println(getJumpedPixelCounter());                                                            //debug
                System.out.println("Vergangene Zeit");                                                                  //debug
                System.out.println(getTimeElapsedInMs());                                                               //debug
                System.out.println();                                                                                   //debug

                setJumpedPixelCounter(0);                                                                               //Gesprungene Pixel wieder auf 0 setzen, da Sprung zurückgesetzt wird
                setFallStartTime(System.currentTimeMillis());
                isFalling = true;                                                                                       //Fallen eingeleitet
                break;                                                                                                  //Wenn If Bedingung eintritt, for Schleife abbrechen
            }
        }
    }


    /**
     * Formel zum Berechnen der Sprungdauer bei gegebener Start-Geschwindigkeit
     *
     * @return
     */
    public double getJumpTimeInMs() {
        double time = (-this.startSpeedInMeterPerSecond / -this.gravitation);
        return time * 1000;
    }

    public long getDurationJump() {
        return System.currentTimeMillis() - start;
    }

    /**
     * Formel zum Berechnen der Geschwindigkeit nach einer gewissen Zeit
     *
     * @param timeElapsedInMs
     * @return
     */
    public double getSpeedAfterTimeInMeterPerSecond(long timeElapsedInMs) {
//        return startSpeedInMeterPerSecond - gravitation * (timeElapsedInMs / 1000.0);
        return ((this.startSpeedInMeterPerSecond * 1000) - (this.gravitation * timeElapsedInMs)) / 1000;
    }

//    public void calculateJump() {
//        pixelPerTimerPassList = new ArrayList<>();
//        for (int i = 0; i < getJumpTimeInMs(); i++) {       //JumpTime 500ms
//            pixelPerTimerPassList.add(getNeededPixelPerTimerPassWithGivenSpeedInMeterPerSecond(getSpeedAfterTimeInMeterPerSecond(i)));
//        }
//    }

    public int getNeededPixelPerTimerPassWithGivenSpeedInMeterPerSecond(double speedInMeterPerSecond) {
        return (int) (speedInMeterPerSecond);
    }

    public Physics refreshPixelPerTimerPass() {
        this.pixelPerTimerPass = getPixelPerTimerPass();
        return this;
    }

    public int getPixelPerTimerPass() {                         //Pixel die er pro TimerDurchgang hoch springt
        int speed = (int) (getSpeedAfterTimeInMeterPerSecond(timeElapsedInMs));
        if (speed > 0) return (int) (getSpeedAfterTimeInMeterPerSecond(timeElapsedInMs));
        else return 1;
    }

    public Physics setTimeElapsedInMs() {
        long timeElapsedInMs = System.currentTimeMillis() - start;
        this.timeElapsedInMs = timeElapsedInMs;
        return this;
    }


    public double getStartSpeedInMeterPerSecond() {
        return startSpeedInMeterPerSecond;
    }

    public void setStartSpeedInMeterPerSecond(double startSpeedInMeterPerSecond) {
        this.startSpeedInMeterPerSecond = startSpeedInMeterPerSecond;
    }

    public double getGravitation() {
        return gravitation;
    }

    public long getTimeElapsedInMs() {
        return timeElapsedInMs;
    }

    public int getJumpedPixelCounter() {
        return jumpedPixelCounter;
    }

    public Physics setJumpedPixelCounter(int jumpedPixelCounter) {
        this.jumpedPixelCounter = jumpedPixelCounter;
        return this;
    }

    public Physics increaseJumpedPixelCounter(int pixel) {
        jumpedPixelCounter = jumpedPixelCounter + pixel;
        return this;
    }

    public long getFallStartTime() {
        return fallStartTime;
    }

    public Physics setFallStartTime(long fallStartTime) {
        this.fallStartTime = fallStartTime;
        return this;
    }

    public long getFallDuration() {
        return fallDuration;
    }

    public Physics setFallDuration(long fallDuration) {
        this.fallDuration = fallDuration;
        return this;
    }

    public int getMinimumSpeed() {
        return minimumSpeed;
    }


    public long getStart() {
        return start;
    }

    public Physics setStart(long start) {
        this.start = start;
        return this;
    }


    public List<Integer> getPixelPerTimerPassList() {
        return pixelPerTimerPassList;
    }


    public Physics setHeroObject(Hero mario) {
        this.mario = mario;
        return this;
    }


    public boolean getJumping() {
        return isJumping;
    }

    public Physics setJumping(boolean jumping) {
        this.isJumping = jumping;
        return this;
    }

    public boolean getFalling() {
        return isFalling;
    }

    public Physics setFalling(boolean falling) {
        this.isFalling = falling;
        return this;
    }
    
    public int getGameLoops() {
        return gameLoops;
    }

}


Hero:
Java:
package Pokemon;


public class Hero {

    private final Physics physics;
    private Canvas canvas;
    private final int TILESIZE;
    private final int ZOOM;
    private int speed = 0;
    private int feetPosition = 2;  //1,2,3            (1 = rechter Fuß, 2 = normal, 3 = linker Fuß) Veraltetes Kommentar, da Wiederverwendung aus anderem Projekt
    private Direction direction = Direction.RIGHT;
    private int positionX = 0;  //Position des Charakters beim Start
    private int positionY;                                                                                              //Position des Charakters beim Start
    private int finalPositionX;                                                                                         //Temporäre Final-Position für den Fußwechsel
    private int finalPositionY;
    private int walkSpeed = 4;
    private int fallDelay = 41;                                                                                         //Umso höher umso langsamer fällt Mario
    private int feetPositionBackup;
    private Direction directionBackup;
    Direction directionOld = getDirection();
    int feetPositionOld = getFeetPosition();
    int positionXOld = positionX;
    int positionYOld = positionY;
    private boolean promptRepaintHero = false;

    public Hero(Canvas canvas, Physics physics) {
        this.physics = physics;
        this.canvas = canvas;
        ZOOM = canvas.getZOOM();
        TILESIZE = canvas.getTILESIZE();
        positionX = 2 * TILESIZE * ZOOM;
        positionY = 10 * TILESIZE * ZOOM;
        physics.setHeroObject(this);
    }

    public boolean heroDataChanged() {
        if (promptRepaintHero) {
            promptRepaintHero = false;
            return true;
        }
        if (directionOld != getDirection() || feetPositionOld != getFeetPosition() || positionXOld != positionX || positionYOld != positionY) {
            refreshHeroData();
            return true;
        }
        return false;
    }

    public void refreshHeroData() {
        directionOld = getDirection();
        feetPositionOld = getFeetPosition();
        positionXOld = positionX;
        positionYOld = positionY;
    }

    public void doJump() {
        if (!physics.getJumping() && !physics.getFalling()) {
            finalPositionY = getPositionY() - physics.getJumpedPixelCounter();
            physics.setStart(System.currentTimeMillis());
            physics.setJumping(true);
        }
    }

    public void move(int speed) {                           //Bewegt Charakter in angegebener Geschwindigkeit (- oder +)
        if (speed != 0) {                                   //Bei Geschwindigkeit 0 muss nicht repainted werden
            positionX += speed;
            promptRepaintHero = true;
            canvas.repaint();
        }
    }

    public void moveUp(int pixel) {
        for (int i = 0; i < pixel; i++) {
            positionY--;
            promptRepaintHero = true;
            canvas.repaint();
        }
    }

    public void moveDown(int pixel) {
        for (int i = 0; i < pixel; i++) {
            positionY++;
            promptRepaintHero = true;
            canvas.repaint();
        }
    }

    public void refreshFinalPositionX() {
        if (speed > 0) {
            finalPositionX = positionX + (canvas.getTILESIZE() / 2) * canvas.getZOOM();  //Wert muss das X fache sein
        }
        if (speed < 0) {
            finalPositionX = positionX - (canvas.getTILESIZE() / 2) * canvas.getZOOM();

        }
    }

    public void changeFeetPosition() {
        if (feetPosition == 1) {                        //rechts1, normal2, links3  //im Mario Projekt kein rechter Fuß
            feetPosition = 2;
        } else if (feetPosition == 2) {
            feetPosition = 1;
        } else {
            feetPosition = 2;
        }
        promptRepaintHero = true;
        canvas.repaint();

    }

    /*
    Getter und Setter
     */
    public int getFeetPosition() {
        return feetPosition;
    }

    public Hero setFeetPosition(int feetPosition) {
        this.feetPosition = feetPosition;
        promptRepaintHero = true;
        canvas.repaint();
        return this;
    }

    public Direction getDirection() {
        return direction;
    }

    public Hero setDirection(Direction direction) {
        this.direction = direction;
        promptRepaintHero = true;
        canvas.repaint();
        return this;
    }

    public int getPositionX() {
        return positionX;
    }

    public Hero setPositionX(int positionX) {
        this.positionX = positionX;
        promptRepaintHero = true;
        canvas.repaint();
        return this;
    }

    public int getFinalPositionX() {
        return finalPositionX;
    }

    public Hero setFinalPositionX(int finalPositionX) {
        this.finalPositionX = finalPositionX;
        return this;
    }

    public int getWalkSpeed() {
        return walkSpeed;
    }

    public Hero setWalkSpeed(int walkSpeed) {
        this.walkSpeed = walkSpeed;
        return this;
    }

    public int getFeetPositionBackup() {
        return feetPositionBackup;
    }

    public Hero setFeetPositionBackup(int feetPositionBackup) {
        this.feetPositionBackup = feetPositionBackup;
        return this;
    }

    public Direction getDirectionBackup() {
        return directionBackup;
    }

    public Hero setDirectionBackup(Direction directionBackup) {
        this.directionBackup = directionBackup;
        return this;
    }

    public int getSpeed() {
        return speed;
    }

    public Hero setSpeed(int speed) {
        this.speed = speed;
        return this;
    }


    public int getFallDelay() {
        return fallDelay;
    }

    public Hero setFallDelay(int fallDelay) {
        this.fallDelay = fallDelay;
        return this;
    }


    public int getPositionY() {
        return positionY;
    }

    public int getFinalPositionY() {
        return finalPositionY;
    }

    public Hero setFinalPositionY(int finalPositionY) {
        this.finalPositionY = finalPositionY;
        return this;
    }

    public Hero setPromptRepaintHero(boolean promptRepaintHero) {
        this.promptRepaintHero = promptRepaintHero;
        return this;
    }


}



So, jetzt habe ich meinen Code gepostet duck
 

KonradN

Super-Moderator
Mitarbeiter
Also ich sehe in dem Code noch nicht wirklich das, was wichtig ist bei dem entsprechenden Ansatz, den ich bei #31 beschrieben habe, aber evtl. übersehe ich da auch etwas.

a) Du hast ja mehrere BufferedImages in der gewünschten Größe. Und diese müssen dann ja auch wechseln. Du scheinst da zwar irgendwas zu wechseln, aber du malst immer nur das gleiche Bild, oder sehe ich das falsch?
b) Dein Canvas hat da ein festes Graphics Objekt auf einem Bild? Und das wird in einem Thread ständig verwendet? Und malt das Bild, das Du ansonsten auch anzeigst?
...

Nur um es noch einmal ganz deutlich zu sagen:
Du hast nur zwei Threads. Und einer ist der UI Thread, also kein Thread den Du bewusst erzeugst. Und der zweite ist die Gameloop.

Die Gameloop macht dann immer das Gleiche:
a) Berechnung von Veränderungen der Objekte
b) Darstellung der Objekte

Bei der Darstellung der Objekte wird dann ein Bild gemalt, d.h. ein BufferedImage, das derzeit nicht verwendet wird, wird benutzt:
1. ein Graphics Objekt wird für dieses Image erstellt.
2. Das Bild wird gemalt
3. das Graphics Objekt wird vernichtet (dispose() Aufruf)
4. das Bild wird dem UI Thread angeboten
5. ein repaint() wird aufgerufen, um den UI Thread zu triggern bezüglich neumalen des Bildes.

Und der Canvas ist dann wirklich trivial. Alles was da gemacht wird, ist
  • das aktuelle Bild holen
  • das Bild malen

Sonst nichts.

Das einzig komplexe ist dabei die Verwaltung der BufferedImages. Wenn Du diesen Weg wirklich so versuchen willst, dann mache ich Dir da wirklich mal etwas fertig. Hatte ich Dir ja schon einmal angeboten und dann nur nicht weiter verfolgt.
 

KonradN

Super-Moderator
Mitarbeiter
Also nur um die Idee einmal zu skizzieren: So in der Art könnte die Verwaltung aussehen:
Java:
package Pokemon;

import java.awt.image.BufferedImage;

/**
 * Management of Images for
 */
public class ImageManagement {

    private Object viewingImageLock = new Object();

    /**
     * Width of window / images.
     */
    private volatile int width;

    /**
     * Height of window / images.
     */
    private volatile int height;

    private volatile BufferedImage currentDrawingImage;
    private volatile BufferedImage nextDrawingImage1;
    private volatile BufferedImage nextDrawingImage2;

    private volatile BufferedImage currentViewingImage;
    private volatile BufferedImage nextViewingImage;

    /**
     * Creates a new instance of ImageManagement;
     * @param width width of images.
     * @param height height of images.
     */
    public ImageManagement(int width, int height) {
        this.width = width;
        this.height = height;

        currentDrawingImage = createImage();
        nextDrawingImage1 = createImage();
        nextDrawingImage2 = createImage();
    }

    /**
     * Creates a new BufferedImage in the correct size.
     * @return BufferedImage to use.
     */
    private BufferedImage createImage() {
        return new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
    }

    /**
     * Sets the new size of the images.
     * @param width Width of images.
     * @param height Height of images.
     */
    public void setSize(int width, int height) {
        this.width = width;
        this.height = height;
    }

    /**
     * Gets the current image to view. Takes care of a new image and moves old image back to queue.
     * @return The BufferedImage that should be displayed.
     */
    public BufferedImage getCurrentViewingImage() {
        if (nextViewingImage != null) {
            synchronized (viewingImageLock) {
                if (nextDrawingImage1 == null) {
                    nextDrawingImage1 = currentViewingImage;
                } else {
                    nextDrawingImage2 = currentViewingImage;
                }
                currentViewingImage = nextViewingImage;
                nextViewingImage = null;
            }
        }
        return currentViewingImage;
    }

    /**
     * Gets the next BufferedImage to draw.
     * <li>
     *     <ul>The last drawn image is moved to be shown next.</ul>
     *     <ul>Gets the next image in the queue</ul>
     *     <ul>Checks the size of the image and creates a new one if required.</ul>
     * </li>
     * @return
     */
    public BufferedImage getNextDrawingImage() {
        synchronized (viewingImageLock) {
            if (nextViewingImage != null) {
                nextDrawingImage2 = nextViewingImage;
            }
            nextViewingImage = currentDrawingImage;
            currentDrawingImage = nextDrawingImage1;
            nextDrawingImage1 = nextDrawingImage2;
            nextDrawingImage2 = null;
        }

        // Check if resize happened
        if (currentDrawingImage.getWidth() != width || currentDrawingImage.getHeight() != height)
            currentDrawingImage = createImage();

        return currentDrawingImage;
    }
}

Du hast dabei an Schnittstelle nur relativ wenig:
a) Konstruktor - der wird einmalig aufgerufen mit der notwendigen Größe der Bilder.
b) Die Größe kann angepasst werden - dazu dient setSize. Alle neuen Bilder, die dann gemalt werden, haben ab da die richtige Größe.
c) getCurrentViewingImage - damit holt sich der UI Thread das aktuelle Bild um es dann einfach zu malen und fertig. Diese Methode aktualisiert auch das Bild und so ...
d) getNextDrawingImage - damit sagt die Gameloop: Ich habe das Bild fertig gemalt - gib mit das nächste Bild zum malen. Das ist sozusagen das Ende der Gameloop:

Code:
drawingImage = imageManagement.getNextDrawingImage();
canvas.repaint();

Das alles aber ungetestet und einfach einmal so hingeschrieben. Evtl. muss es noch etwas angepasst werden weil ich irgendwas vergessen habe. Aber man hat eine klare Trennung. Evtl. habe ich die Verschiebung der Bilder nicht gut genug durchdacht - Wenn da ein Fehler ist, dann könnte nextDrawingImage1 evtl. irgendwann mal null sein. Aber ich sehe gerade nicht, wann dies der Fall sein könnte...
 

Hag2bard

Bekanntes Mitglied
Fantastisch. Das sieht - und das muss ich bescheiden zugeben - doch ein klein wenig besser aus, als das was ich da so getrieben habe.
Ich habe das jetzt eingebaut, musste erstmal kurz drüber nachdenken. Mein Problem ist nun, dass ich nicht weiß wo ich die Logik dafür einbauen soll, dass er das BufferedImage mit Leben füllt.

Beispiel:

Java:
case 1 -> graphicsHero.drawImage(canvas.getSpritesBufferedImage(), positionX, positionY, positionX + TILESIZE, positionY + TILESIZE, 0, 32, 0, 32, null);

Vielleicht sollte ich meinen Code noch ein wenig erläutern.
Mein Charakter wird gezeichnet je nach folgenden Variablen

Fußposition: Je nachdem welche Fußposition mein Charakter gerade hat (1=laufend) (2=stehend), greift er auf das SpriteBufferedImage auf unterschiedliche Zonen zu.
Außerdem unterscheidet er noch nach der Richtung des Charakters. Vorher habe ich ein Pokemon Klon machen wollen, da gab es genau so 4 Richtungen wie bei Mario auch.
Wenn er springt sieht er anders aus, als wenn er fällt oder läuft. Unterschieden wird hier auch zwischen "nach links schauend" und "nach rechts schauend".

Deshalb habe ich eine Logik wie diese hier aufgebaut:

Code:
            if (mario.getDirection().equals(Direction.LEFT)) {
                switch (mario.getFeetPosition()) {                                                                                                                        //+ZOOM=Korrektur
                    case 1 -> graphicsHero.drawImage(canvas.getSpritesBufferedImage(), positionX, positionY - OFFSET, positionX + TILESIZE * ZOOM, (positionY + TILESIZE * ZOOM) + ZOOM, 1, 91, 17, 118 + 1, null); //links Fuß vorn
                    case 2 -> graphicsHero.drawImage(canvas.getSpritesBufferedImage(), positionX, positionY - OFFSET, positionX + TILESIZE * ZOOM, (positionY + TILESIZE * ZOOM) + ZOOM, 19, 91, 35, 118 + 1, null); //links stehend
                }
            }
            if (direction.equals(Direction.RIGHT)) {
                switch (feetPosition) {                                                 // offset ist der Wert wieviel über 16 Pixel Block gezeichnet werden soll        //+ZOOM=Korrektur
                    case 1 -> graphicsHero.drawImage(canvas.getSpritesBufferedImage(), positionX, positionY - OFFSET, positionX + TILESIZE * ZOOM, (positionY + TILESIZE * ZOOM) + ZOOM, 1, 31, 17, 58 + 1, null); //rechts Fuß vorn
                    case 2 -> graphicsHero.drawImage(canvas.getSpritesBufferedImage(), positionX, positionY - OFFSET, positionX + TILESIZE * ZOOM, (positionY + TILESIZE * ZOOM) + ZOOM, 19, 31, 35, 58 + 1, null); //rechts stehend
                }
              
              
                ... usw

Und je nach dem ob der Charakter andere Werte aufweist, wie z.b. eine veränderte Position oder eine andere Fußstellung oder Blickrichtung, soll dann aus dem SpriteBufferedImage das passende Sprite rausgefischt werden.

Aber an welche Stelle soll ich diesen Code einsetzen?

P.S.: Wie kann man in knapp 4 Monaten über 1000 Beiträge posten? Das sind rein rechnerisch etwa 9 Beiträge pro Tag. Mir ist viel Arbeit auch nicht fremd, aber solltest du nicht YouTuber oder sowas werden? Mit der Energie, Liebe und Qualität die in deiner Arbeit steckt, könntest du dem Mittelstand den Mittelfinger zeigen.
 
Zuletzt bearbeitet:

KonradN

Super-Moderator
Mitarbeiter
Das ist doch Teil des Aufbaus des Bildes. Damit gehört es in die Gameloop. Und hat natürlich nichts mit dem canvas am Hut. Das, was Du da also im Canvas hast scheint da nicht wirklich rein zu gehören. Oder ist Canvas nicht das UI Element für die Anzeige?

P.S.: Wie kann man in knapp 4 Monaten über 1000 Beiträge posten? Das sind rein rechnerisch etwa 9 Beiträge pro Tag. Mir ist viel Arbeit auch nicht fremd, aber solltest du nicht YouTuber oder sowas werden? Mit der Energie, Liebe und Qualität die in deiner Arbeit steckt, könntest du dem Mittelstand den Mittelfinger zeigen.
Das liegt einfach vermutlich einfach daran, dass ich derzeit nur sehr wenig Zeit in andere Hobbies stecke / stecken kann. Die laufen hetzt zwar auch alle nach und nach wieder an (nach Corona) aber ich bin da halt doch etwas vorsichtig.

Aber das wird sich vermutlich auch alles wieder relativieren. YouTube Videos in hoher Qualität zu bauen ist viel Aufwand. Einfache YouTube Videos habe ich auch schon gemacht, aber bezüglich Qualität und so habe ich das erst einmal aufgeben. Der Nutzen / Kosten Vergleich ist einfach nicht gegeben in meinen Augen.

Da ist es evtl. interessanter, eine Projektidee umzusetzen. Das ist etwas, das ich jetzt am Feiertag etwas überlegt habe und dann auch gestartet habe. Das ist dann im Augenblick das, was die größte Chance hat, mich vom Forum fern zu halten (Und z.B. auch die Erklärung, wieso ich nach #87 nicht Abends noch die Klasse schnell zusammen geschustert habe.
 

Hag2bard

Bekanntes Mitglied
Halli Hallo,

ich habe meinen Code etwas besser strukturiert. (Zumindest soweit dass ich ihn lesen kann :D)
Ich habe meine GameLoop, welche update() und render() aufruft.
Meine update Methode ist nach wie vor die selbe, die soll auch erstmal so bleiben.
Was mir nun aber noch komplett fehlt ist die render Methode.
Ich habe im Grundprinzip ja schon meine fertige render Methode, die halt je nach Zustand des Spiels (der in update geupdatet) wird das rendern übernimmt.
Dies geschieht eigentlich mittels der drawImage Methode.
Schön und gut, nur braucht diese ein Graphics Objekt.
Und genau da fängt mein Problem an.
In meiner ImageManagement Klasse habe ich 5 BufferedImages. Welches davon soll ich mir hernehmen um daraus ein Graphics Objekt zu erstellen und dies der render Methoden zu geben?
 

KonradN

Super-Moderator
Mitarbeiter
Du hast ja nach außen nur zwei Getter … und einmal deutet das Viewing darauf hin, dass es die Methode ist, die das Bild zurück gibt, das angezeigt wird.

Und die Methode mit dem Drawing deutet darauf hin, dass da das Bild zurück gegeben wird, in das gemalt werden soll.
 

Ähnliche Java Themen

Neue Themen


Oben