2D Plattformer - Springen / Physik

Hag2bard

Bekanntes Mitglied
Hallo,

ich hoffe ich bekomme mein Problem hier verständlich geschildert.

Ich habe vor Super Mario World zu kopieren und hänge momentan am Sprungverhalten.

Mir geht es hier nicht um Syntax Fragen sondern darum wie ein Sprung in der Physik funktioniert. Am Anfang hat man die eigene Schwerkraft überwunden und beschleunigt nach oben. Dann nimmt die Geschwindigkeit ab und der Charakter schwebt kurz. Anschließend geht es wieder nach unten und dabei beschleunigt er, bis er durch den Boden abbrupt gebremst wird.
Wie kann ich mir sowas genau vorstellen?
Die Konstante 9,81m/s2 kenn ich.
 

Neumi5694

Top Contributor
Es handelt sich um eine Parabel.

Jump & Runs halten sich nur selten an die Physik, so kannst du z.B. oft in der Luft abbremsen oder den Fall horizontal korrigieren.
Die Parabel nutzt du, um die vertikale Position und gegebenenfalls Geschwindigkeit zu berechnen. Als Grundlage wählst du hierfür aber nicht die X-Position, sondern die vergangene Zeit.

Irgendwann haben wir das Ganze anhand der zugrundeliegenden Differentialgleichungen hergeleitet .... mann, ist das lange her.
 

httpdigest

Top Contributor
Wie kann ich mir sowas genau vorstellen?
Naja... genauso wie du es beschrieben hast. Du willst vermutlich aber eher wissen, wie man diese (Newton'sche / klassische) Physik in einem Spiel umsetzen kann. Hierfür ist es nützlich, erstmal die grundsätzlichen Formeln der klassischen Physik zu kennen:
1. dv/dt = a (also das Differential der Geschwindigkeit in Bezug auf die Zeit ist die Beschleunigung)
2. ds/dt = v (also das Differential der Position/Strecke in Bezug auf die Zeit ist die Geschwindigkeit)

Ein Spiel bzw. eine Game Loop ist im Prinzip ein diskreter numerischer Integrator, da die Zeit immer in diskreten Schritten abläuft (denke "FPS"). Diese diskreten Zeit-Schritte können wir für dt annehmen. Und jetzt wollen wir pro Zeit-Schritt (Frame) im Spiel wissen, wie sich unsere Geschwindigkeit und unsere Position (des Spieler-Charakters) über die Zeit ändert. Hierfür brauchen wir nur die obigen Gleichungen umzuformen:

Änderung der Geschwindigkeit: dv = a * dt
Änderung der Position: ds = v * dt

Wenn wir diese Gleichungen schrittweise numerisch integrieren, dann erhalten wir die jeweils neue Geschwindigkeiten und Position durch:
Neue Geschwindigkeit: v(t+dt) = v(t) + a * dt
Neue Position: s(t+dt) = s(t) + v(t+dt) * dt

Üblicherweise ist die Position und die Geschwindigkeit ein Vektor. Wenn du im Spiel springst, dann ist die aktuelle Geschwindigkeit einmal nach oben gerichtet (und addiert sich evtl. mit der horizontalen Geschwindigkeit der Spielfigur, wenn sie gerade rennt/geht).
Die Beschleunigung wirkt dann als Erdbeschleunigung nach unten und verändert die aktuelle Geschwindigkeit.

Ob Spiele hier aber _wirklich_ die klassische Physik simulieren oder nicht einfach immer nur eine vorberechnete Animation / Parabel abspielen, bleibt mal dahingestellt.

Siehe z.B. auch: https://gafferongames.com/post/integration_basics/
 

Hag2bard

Bekanntes Mitglied
Das Thema ist so kompliziert, ich verstehe es nicht. Ich bin in Englisch auch nicht so schlecht, aber darin verstehe ich halt noch weniger.
Beim Springen kommt es ja darauf an, dass die Geschwindigkeit nach und nach reduziert wird.
Dazu bräuchte ich eine Funktionsgleichung, die mir die Ellipse darstellt, welche bei einem Super Mario Spiel Sinn ergibt.
Eure Rechnungen finde ich ehrlich gesagt zu viel des guten, ich verstehe auch nichts von Vektoren.
 

fhoffmann

Top Contributor
- In waagerechter Richtuung hast du eine konstante Geschwindigkeit v (den Luftwiderstand wollen wir hier nicht beachten).
Die zurückgelegte Strecke (in wagerechter Richtung) ist damit s = v*t.

- In senkrechter Richtung hast du zuerst eine Startgeschwindigkeit v0. Davon musst du die Erdbeschleunigung g mal der vergangenen Zeit abziehen:
du hast also eine Geschwindigkeit (zum Zeit t nach dem Absprung) von v0 - g * t.
Die zurückgelegte Strecke (in senkrechter Richtung) ist damit s = v0*t - 0.5*g*t².

Das gilt natürlich nur, bis du wieder auf der Erde gelandet bist.
EDIT:
also wenn 0 = v0*t - 0.5*g*t²,
das gilt bei t = 0 (Absprung)
und bei t = 2 * v0 / g (Landung)
 
Zuletzt bearbeitet:

Hag2bard

Bekanntes Mitglied
Also müsste ich jetzt erstmal ein paar Werte ausprobieren und schauen ob das realistisch aussieht, bzw. ob das dem Spiel gleich ist?

Dann mach ich das mal:
Sagen wir er springt mit 4m/s los (etwa 15km/h) = v0
g = 9,81(m/s²)
t ist unbekannt

Ich müsste also wissen wie lange er maximal bis nach oben brauch und wie schnell er beim Start ist
Sagen wir er darf max 2 Sekunden brauchen.
t = 2s

Also setze ich ein:

Strecke = 4[m/s]*2 - 0.5*9,81[m/s²]*2²
Strecke = 8[m/s²] - 4.905[m/s²] * 2²
Strecke = 8[m/s²] - 4,905[m/s²] * 4
Strecke = -11,62m

Ergibt das einen Sinn?
 

Hag2bard

Bekanntes Mitglied
Richtig, hab mich korrigiert, bekomme aber einen - Wert

edit:

Laut meinem Verständnis benötige ich, wenn ich nach oben springen möchte, je nach zurückgelegter Strecke eine Geschwindigkeit.
Sagen wir am Anfang ist die Geschwindikgkeit bei 4m/s, nach einer gewissen Strecke wird diese Geschwindigkeit immer kleiner bis sie auf 0 geht.
Dafür benötige ich eine quadratische Funktion oder?
 

Hag2bard

Bekanntes Mitglied
Ok also die Zeit bis zur Landung, aber in der Formel fehlt die Strecke oder nicht?
Und am Anfang ist die Geschwindigkeit bei 0. Mit zurückgelegter Zeit wird die Geschwindigkeit größer.
Das habe ich soweit begriffen.
Gehen wir hier jetzt also erstmal vom Fallen aus?
 

mihe7

Top Contributor
Wenn Du beim Absprung eine vertikale Geschwindigkeit von 4 m/s erreichst, dann kannst Du Dich fragen, wie lange es bei einer Beschleunigung von a = -g = -9,81 m/s² dauert, bis die Geschwindigkeit 0 m/s erreicht wird - dann hast Du offensichtlich den höchsten Punkt erreicht.

v(t) = v0 + a*t <=> 0 = v0 + a*t <=> t = -v0 / a = (-4 m/s) / (-9,81 m/s²) = 4/9,81 s

In der Zeit legst Du die Strecke s = v0*t + 0,5 at² = v0*t - 0,5 gt² zurück. Da Du wieder zum Boden zurück musst, ist die Gesamtstrecke natürlich doppelt so groß, also S = 2s = 2v0*t - gt².
 

temi

Top Contributor
Wie wäre es mit einer fertige Bibliothek für Physik im 2D Raum? Bei LibGDX ist da, glaube ich, was dabei. Box2D?
 

Hag2bard

Bekanntes Mitglied
Ich verstehe immer noch nicht wie das funktionieren soll.
Ich möchte folgendes:

Ich versuche es nochmal so zu erklären wie ich das verstehe.
Wenn mein Charakter springt, hat er eine Anfangsgeschwindigkeit. Richtig?
Von dieser Anfangs-Geschwindigkeit ist es abhängig, wie hoch er springt. Auch richtig?
Die Geschwindigkeit ändert sich mit der Zeit. Korrekt?

Ich benötige jetzt also die Zeit die er für diesen Sprung bis oben hin braucht.
Anhand dessen wie viel Zeit vergangen ist, muss ich die Geschwindigkeit neu festlegen.
Welche Formel kann ich da nutzen um rauszufinden, bei wieviel vergangener Zeit er welche Geschwindigkeit hat?
 

mihe7

Top Contributor
@Hag2bard, alles richtig und die Antworten auf Deine Fragen stehen oben auch schon alle da:

Ich benötige jetzt also die Zeit die er für diesen Sprung bis oben hin braucht.
t = -v0 / a = -v0 / -g = v0 / g (mit der Absprunggeschwindigkeit v0 und der Beschleunigung a = -g)

Welche Formel kann ich da nutzen um rauszufinden, bei wieviel vergangener Zeit er welche Geschwindigkeit hat?
du hast also eine Geschwindigkeit (zum Zeit t nach dem Absprung) von v0 - g * t.
Nachtrag: und ich hatte das oben so geschrieben: v(t) = v0 + a*t
 

Hag2bard

Bekanntes Mitglied
Danke bis hier her. Wie kann ich die diese Formel:
Code:
Geschwindigkeit (zum Zeit t nach dem Absprung)  = v0 - g * t
so umformulieren, dass ich da ms einsetzen kann?
Wenn ein Sprung nur 3 Sekunden geht, dann ist das ziemlich ungenau da mit int Werten zu arbeiten.
 

mihe7

Top Contributor
An der Formel ändert sich nichts, bloß weil Du mit ms rechnest. Du musst auch nicht mit int rechnen. Du erhältst mit m/s und m/s² am Ende Meter und die kannst Du auf das Koordinatensystem entsprechend des von Dir gewählten Maßstabs umrechnen. Natürlich könntest Du eine x-beliebige andere Einheit wählen. Außerdem musst Du nicht die Erdbeschleunigung annehmen. Da wirst Du ein wenig ausprobieren müssen.
 

Hag2bard

Bekanntes Mitglied
Hallo,
danke nochmal für die Hilfe, es hat soweit geklappt.
Mein Code wird so langsam echt kompliziert, deswegen hat es so lange gedauert.
Nun habe ich ein anderes Problem, welches mich schon länger begleitet.

Es geht um den KeyListener und die Funktion keyPressed.
Wenn ich rechts gedrückt halte und dann ganz schnell links drücke, dann dauert es manchmal eine Weile(ca. 510ms) bis er auch wirklich das KeyEvent VK_Left feuert.

Warum reagiert Java so träge auf eine Tastenänderung? Kann man das verhindern?
 

Hag2bard

Bekanntes Mitglied
Er repainted natürlich sehr oft, aber wie sonst funktioniert eine Animation? Timer sind ein eigener Thread oder?
Sonst passiert nicht viel im ui Thread.
Ich habe irgendwo was gelesen von Keybindings statt KeyListener. Ist da irgendwas dran?
Kann ich den KeyListener in einem extra Thread auslagern?
 

Hag2bard

Bekanntes Mitglied
Hallo,

Ich habe mir das DuckyGame Beispiel mal runtergeladen aber auch da reagiert er träge auf wechselnde Tastendrücke (links, rechts).
Ich habe in meinem Code KeyBindings verwendet(testweise), welche mir von der Syntax her tatsächlich besser gefallen aber an meinem Problem hat das nichts geändert.

Was mir aufgefallen ist, dass dieses Verhalten auftritt, wenn die links-Taste noch nicht vollständig losgelassen wurde und man schon auf die rechts-Taste drückt. Gerade dann benötigt er sehr lange. (Bei DuckyGame auch.)
Für einen flüssigeren Spielablauf ist diese Verzögerung allerdings kontraproduktiv.
 
Y

yfons123

Gast
weil du ansich den input für alle tasten berechnest und den normailiserten vektor zur berechnung deiner rechnung zum bewegen
hernimmst
 
Y

yfons123

Gast
ok du hast folgendes problem : dein ding geht nicht :D

du hast zb input W
dh dein Vektor2 movement ist der vector V ( 1 , 0 )
dh 1 nach oben
für D ist der vector V ( 0 , 1 )
für S V ( -1 , 0 )
für A V( 0 , -1)

du hast eine klasse die einfach nur den input abfängt von wasd und sofort den input vektor berechnet ( hier ist dein problem mit deinem "input lagg" die klasse blockt die berechnung weil sie solange bewegt bis man los lässt was scheiße ist)
zb bei nur W bekommst du V ( 1 , 0 ) raus als movement
usw für die anderen vektoren

so und jetzt hast du das problem was ist wenn du W und D drückst , dann muss deine klasse die input vektoren zusammen rechnen
was hier V ( 1 , 1 ) wäre aber da wir ja alle in der schule aufgepasst hatten und das nicht dem einheitskreis entspricht musst du es normalisieren aus folgendem grund :

du bist mit V ( 1 , 1 ) shcneller als wie mit einem vektor der V ( 1 , 0 ) hat zb ... ( satz des pythagoras usw ) deshalb wäre die normalisierung glaub ich aus dem stehgreif bei V ( 0,7 , 0,7 ) wenn 2 input tasten gedrückt wurden( in dem fall W udn D ) um somit gleich schnell zu sein wie wenn man nur eine taste gedrückt hat

die klasse die den input berechnet sollte den input vektor als readonly public zur verfügung stellen somit deine movement klasse sich nach dem vektor ausrichten kann und nciht abhängig von input ist ... weil du dich im moment einfach nur selber blockst

PS: willkommen in der spiele entwicklung :D
 

Hag2bard

Bekanntes Mitglied
Ich gehöre leider zur Konsorte, zu spät festgestellt wie wichtig Schule ist, von daher hatte ich nur Mathematik bis zur 12., leider ohne Zulassung zur Prüfung für die Fachhochschulreife.
Das heißt Vektoren ist ein Thema, dass ich mit meiner Frau mal durchkauen müsste, da ich davon vorher nie was gehört habe.
Ich habe es anders gelöst.

Mein Code ist ungeordnet, unkommentiert und eigentlich wie meine Wohnung jetzt kurz nach dem Umzug, Man möchte niemanden rein lassen.
Gestern habe ich bis 3 Uhr in der Nacht programmiert und musste heute um halb 8 wieder raus. Von daher werde ich hier jetzt mal meinen Code in seiner Prototyp-Phase posten.
Ich bin bereit für Kritik, gerne würde ich aber erstmal auf das Problem mit der Verzögerung eingehen.
Wenn euch Klassen fehlen, gerne nerven.

Java:
package Pokemon;

import PokemonEditor.*;
import PokemonEditor.media.MediaPlayer;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;

public class Canvas extends JPanel {

    private BlockArrayList mapLayer1;
    private BlockArrayList mapLayer2;
    private BufferedImage tilesetBufferedImage;
    private final BufferedImage spritesBufferedImage;
    private final int ZOOM = 4;
    private final int TILESIZE = 16;
    private final String spritesFilename = GuiData.filenameSpriteSet;
    private int feetPosition = 2;  //1,2,3            (1 = rechter Fuß, 2 = normal, 3 = linker Fuß) Veraltetes Kommentar, da Wiederverwendung aus anderem Projekt
    private String direction = "right";
    private boolean pressing = false;
    private Timer moveTimer;
    private Timer jumpUpTimer;                                                                                    //Timer zum Hochspringen
    private Timer jumpDownTimer2;                                                                                   //Timer zum Runterfallen
    private int positionX = 2 * TILESIZE * ZOOM;  //Position des Charakters beim Start
    private int positionY = 10 * TILESIZE * ZOOM;  //Position des Charakters beim Start
    private int finalPositionX;
    private final int OFFSET = 12 * ZOOM;
    private int finalPositionY;
    private int walkSpeed = 4;
    private final int jumpDownTimerSpeed = 30;
    private long start;
    private final Physics physics = new Physics();
    private int feetPositionBackup;
    private String directionBackup;


    public Canvas() throws IOException {
        keyBinding();

        MediaPlayer mediaPlayer = new MediaPlayer();
        LoadMap 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(spritesFilename);
        mediaPlayer.playWav();

        moveTimer = new Timer(10, ae -> {
            if (direction.equals("right") || direction.equals("left")) {
                if (positionX == finalPositionX) {      //Wenn 16 Pixel/halber Block erreicht dann feetPosition changen
                    changeFeetPosition();
                    if (jumpUpTimer.isRunning() || jumpDownTimer2.isRunning()) {
                        refreshFinalPositionXslowly(direction);
                    }
                    if (!jumpUpTimer.isRunning() && !jumpDownTimer2.isRunning()) {
                        refreshFinalPositionX(direction);
                    }
                    repaint();
                }
                //TODO

                if (!pressing) {                                           //wenn nicht gedrückt(keyReleased
                    moveTimer.stop();                                   //Move Timer stoppen  //TODO!!!!!!!!!!!!!!!!!!!!!!!!!!!
                    feetPosition = 2;
                    System.out.println("moveTimer stopped");
                }
                repaint();
            }
            if (direction.equals("right")) {
                for (int i = 0; i < ZOOM * walkSpeed; i++) {
                    positionX++;
                    repaint();
                }
                //TODO
            }
            if (direction.equals("left")) {
                for (int i = 0; i < ZOOM * walkSpeed; i++) {
                    positionX--;
                    repaint();
                }
            }
        });

        jumpUpTimer = new Timer(1, jumpEvent -> {                   //muss bei 0 bleiben für max FPS
            if (direction.equals("left")) {
                feetPosition = 1;
            }
            if (direction.equals("right"))
                feetPosition = 2;

            physics.setTimeElapsedInMs(System.currentTimeMillis() - start);     //Aktuell vergangene Zeit setzen
            physics.setPixelPerTimerPass(
                    physics.getNeededPixelPerTimerPassWithGivenSpeedInMeterPerSecond(
                            physics.getSpeedAfterTimeInMeterPerSecond(
                                    physics.getStartSpeedInMeterPerSecond(), physics.getGravitation(), physics.getTimeElapsedInMs())));
            for (int i = 0; i < physics.getPixelPerTimerPass(); i++) {                 //So viel PixelSprung pro Schleifendurchgang
                positionY--;                                                //eins nach oben pro Schleifendurchgang //Ein Schleifendurchgang 17ms
                repaint();
                physics.setJumpedPixelCounter(physics.getJumpedPixelCounter() + 1);

                if (physics.getPixelPerTimerPass() < 2) {                       //Wenn er weniger als 1 Pixel pro Durchlauf Geschwindigkeit hat, hört er auf
                    jumpUpTimer.stop();
                    finalPositionY = positionY + physics.getJumpedPixelCounter();            //Anfangsposition wieder herstellen zum Fallen
                    physics.setJumpedPixelCounter(0);
                    physics.setFallStartTime(System.currentTimeMillis());
                    jumpDownTimer2.start();                             //Fallen gestartet
                    break;                                              //For Schleife wird hier gestoppt da Endposition erreicht
                }
            }
        });

        jumpDownTimer2 = new Timer(1, jE -> {
            direction = "down";
            fallDownMethodTimer(jumpDownTimerSpeed);
        });
    }


    private void fallDownMethodTimer(int jumpDownTimerSpeed) {
        jumpDownTimerSpeed = (int) (jumpDownTimerSpeed / physics.getGravitation());
        for (int i = 0; i < jumpDownTimerSpeed; i++) {
            positionY++;
            repaint();
            if (positionY == finalPositionY) {
                jumpDownTimer2.stop();
                direction = directionBackup;
                feetPosition = feetPositionBackup;
                break;
            }
        }
    }

    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_UP, 0, false), "pressedUp");
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "releasedUp");
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "pressedDown");
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "releasedDown");

        am.put("pressedSpace", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                feetPositionBackup = feetPosition;
                directionBackup = direction;
                doJump();
            }
        });

        am.put("releasedSpace", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                pressing = false;
                start = System.currentTimeMillis();  //TODO!!!!!!!!!!!!
            }
        });

        am.put("pressedLeft", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (!pressing && !moveTimer.isRunning()) {
                    pressing = true;
                    direction = "left";
                    refreshFinalPositionX(direction);
                    changeFeetPosition();
                    moveTimer.start();  //TODO !!!!!!!!!!!!!!!!!!!
                }
            }
        });

        am.put("releasedLeft", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                pressing = false;
                start = System.currentTimeMillis();  //TODO!!!!!!!!!!!!
            }
        });

        am.put("pressedRight", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (!pressing && !moveTimer.isRunning()) {
                    pressing = true;
                    System.out.println(positionY);
                    direction = "right";
                    refreshFinalPositionX(direction);
                    changeFeetPosition();
                    moveTimer.start(); //TODO !!!!!!!!!!!!!!!!!!!
                }
            }
        });

        am.put("releasedRight", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                pressing = false;
                start = System.currentTimeMillis();  //TODO!!!!!!!!!!!!
            }
        });

        am.put("pressedUp", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (!pressing && !moveTimer.isRunning()) {
                    pressing = true;

                    direction = "up";
                    moveTimer.start();
                }
            }
        });

        am.put("releasedUp", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                pressing = false;
                start = System.currentTimeMillis();  //TODO!!!!!!!!!!!!
            }
        });

        am.put("pressedDown", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (!pressing && !moveTimer.isRunning()) {
                    pressing = true;

                    direction = "down";
                    moveTimer.start();
                }
            }
        });

        am.put("releasedDown", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                pressing = false;
                start = System.currentTimeMillis();  //TODO!!!!!!!!!!!!
            }
        });

        setFocusable(true);
        requestFocusInWindow();
    }


    private void paintLayer(Graphics g, BlockArrayList mapLayer) {
        for (int i = 0; i < mapLayer.size(); i++) {
            g.drawImage(tilesetBufferedImage, mapLayer.get(i).getDestinationX() * TILESIZE * ZOOM, mapLayer.get(i).getDestinationY() * TILESIZE * ZOOM, (mapLayer.get(i).getDestinationX() + 1) * TILESIZE * ZOOM, (mapLayer.get(i).getDestinationY() + 1) * TILESIZE * ZOOM, mapLayer.get(i).getSourceX() * TILESIZE, mapLayer.get(i).getSourceY() * TILESIZE, (mapLayer.get(i).getSourceX() + 1) * TILESIZE, (mapLayer.get(i).getSourceY() + 1) * TILESIZE, null);
        }
    }

    private void refreshFinalPositionX(String direction) {
        if (direction.equals("right")) {
            finalPositionX = positionX + (TILESIZE / 2) * ZOOM;  //Wert muss das X fache sein
        }
        if (direction.equals("left")) {
            finalPositionX = positionX - (TILESIZE / 2) * ZOOM;
        }
    }

    private void refreshFinalPositionXslowly(String direction) {
        if (direction.equals("right")) {
            finalPositionX = positionX + (TILESIZE / 2) * ZOOM * 3;  //Wert muss das X fache sein
        }
        if (direction.equals("left")) {
            finalPositionX = positionX - (TILESIZE / 2) * ZOOM * 3;
        }
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        paintLayer(g, mapLayer1);
        paintLayer(g, mapLayer2);

        if (direction.equals("left")) {
            switch (feetPosition) {
                case 1 -> g.drawImage(spritesBufferedImage, positionX, positionY - OFFSET, positionX + TILESIZE * ZOOM, positionY + TILESIZE * ZOOM, 1, 91, 17, 118 + 1, null); //links rechter Fuß vorn
                case 2 -> g.drawImage(spritesBufferedImage, positionX, positionY - OFFSET, positionX + TILESIZE * ZOOM, positionY + TILESIZE * ZOOM, 19, 91, 35, 118 + 1, null); //links kein Fuß vorn
                case 3 -> g.drawImage(spritesBufferedImage, positionX, positionY - OFFSET, positionX + TILESIZE * ZOOM, positionY + TILESIZE * ZOOM, 37, 91, 53, 118 + 1, null); //links linker Fuß vorn
            }
        }
        if (direction.equals("right")) {
            switch (feetPosition) {                                                 // offset ist der Wert wieviel über 16 Pixel Block gezeichnet werden soll
                case 1 -> g.drawImage(spritesBufferedImage, positionX, positionY - OFFSET, positionX + TILESIZE * ZOOM, positionY + TILESIZE * ZOOM, 1, 31, 17, 58 + 1, null); //rechts rechter Fuß vorn
                case 2 -> g.drawImage(spritesBufferedImage, positionX, positionY - OFFSET, positionX + TILESIZE * ZOOM, positionY + TILESIZE * ZOOM, 19, 31, 35, 58 + 1, null); //rechts kein Fuß vorn
                case 3 -> g.drawImage(spritesBufferedImage, positionX, positionY - OFFSET, positionX + TILESIZE * ZOOM, positionY + TILESIZE * ZOOM, 37, 31, 53, 58 + 1, null); //rechs linker Fuß vorn
            }
        }
        if (direction.equals("up")) {               //rise up
            switch (feetPosition) {
                case 1 -> g.drawImage(spritesBufferedImage, positionX, positionY - 27, positionX + TILESIZE * ZOOM, positionY + TILESIZE * ZOOM + (TILESIZE * ZOOM / 2) - 27, 1, 1, 17, 29, null); //oben rechter Fuß vorn
                case 2 -> g.drawImage(spritesBufferedImage, positionX, positionY, positionX + TILESIZE * ZOOM, positionY + TILESIZE * ZOOM + (TILESIZE * ZOOM / 2), 19, 1, 35, 29, null); //oben kein Fuß vorn
                case 3 -> g.drawImage(spritesBufferedImage, positionX, positionY, positionX + TILESIZE * ZOOM, positionY + TILESIZE * ZOOM + (TILESIZE * ZOOM / 2), 37, 1, 53, 29, null); //oben linker Fuß vorn
            }
        }
        if (direction.equals("down")) {             //fall down
            switch (feetPosition) {
                case 1 -> g.drawImage(spritesBufferedImage, positionX, positionY - 27, positionX + TILESIZE * ZOOM, positionY + TILESIZE * ZOOM + (TILESIZE * ZOOM / 2) - 27, 1, 61, 17, 89, null); //Unten rechter Fuß vorn
                case 2 -> g.drawImage(spritesBufferedImage, positionX, positionY, positionX + TILESIZE * ZOOM, positionY + TILESIZE * ZOOM + (TILESIZE * ZOOM / 2), 19, 61, 35, 89, null); //Unten kein Fuß vorn
                case 3 -> g.drawImage(spritesBufferedImage, positionX, positionY, positionX + TILESIZE * ZOOM, positionY + TILESIZE * ZOOM + (TILESIZE * ZOOM / 2), 37, 61, 53, 89, null); //Unten linker Fuß vorn
            }
        }
    }

    private void doJump() {
        if (!jumpUpTimer.isRunning() && !jumpDownTimer2.isRunning()) {
            finalPositionY = positionY - physics.getJumpedPixelCounter();
            start = System.currentTimeMillis();
            jumpUpTimer.start();
        }
    }

//    @Override
//    public void keyTyped(KeyEvent e) {
//    }

//    @Override
//    public void keyPressed(KeyEvent e) {
//        if (e.getKeyCode() == KeyEvent.VK_SPACE) {
//            feetPositionBackup = feetPosition;
//            directionBackup = direction;
//            doJump();
//        }
//        if (e.getKeyCode() == KeyEvent.VK_S) {
//            walkSpeed = 2;
//
//        }
//        if (!pressing && !moveTimer.isRunning()) {  //TODO !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//        if (true) {
//            if (e.getKeyCode() == KeyEvent.VK_LEFT) {
//                pressing = true;
//
//                direction = "left";
//                refreshFinalPositionX(direction);
//                changeFeetPosition();
//                moveTimer.start();  //TODO !!!!!!!!!!!!!!!!!!!
//            }
//   
//            if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
//                pressing = true;
//                System.out.println(positionY);
//                direction = "right";
//                refreshFinalPositionX(direction);
//                changeFeetPosition();
//                moveTimer.start(); //TODO !!!!!!!!!!!!!!!!!!!
//            }
//            if (e.getKeyCode() == KeyEvent.VK_UP) {
//                pressing = true;
//
//                direction = "up";
//                moveTimer.start();
//            }
//            if (e.getKeyCode() == KeyEvent.VK_DOWN) {
//                pressing = true;
//
//                direction = "down";
//                moveTimer.start();
//            }
//        }
//    }
//

//    @Override
//    public void keyReleased(KeyEvent e) {
//        if (e.getKeyCode() == KeyEvent.VK_LEFT || e.getKeyCode() == KeyEvent.VK_RIGHT || e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_DOWN || e.getKeyCode() == KeyEvent.VK_SPACE) {
//            pressing = false;
//            start = System.currentTimeMillis();  //TODO!!!!!!!!!!!!
//
//        }
//        if (e.getKeyCode() == KeyEvent.VK_S) {
//            walkSpeed = 1;
//        }
//    }

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


Java:
package Pokemon;

public class Physics {


    private double startSpeedInMeterPerSecond = 11;                     //   11 braucht er für 300 hoch Dieser Wert bestimmt die zu erreichende Höhe
    private final double gravitation = 9.81;                //    m/s           //Veränderte Gravitation lässt Mario langsamer hoch gleiten
    private int heightJump = 300;
    private int pixelPerTimerPass = 1;                      //muss verändert werden für Geschwindigkeit,
    private final double timerPassInMs = 17;                           //so viele ms pro TimerDurchlauf
    private final double timerPassPerSecond = 1000 / timerPassInMs;
    private final double speedInMeterPerSecond = timerPassPerSecond * pixelPerTimerPass / 10;
    private long timeElapsedInMs;
    private int jumpedPixelCounter = 0;
    private long fallStartTime;

// Ein Counterdurchgang = 17ms
    // 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

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

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


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

    public Physics setPixelPerTimerPass(int pixelPerTimerPass) {
        this.pixelPerTimerPass = pixelPerTimerPass;
        return this;
    }

    public int getPixelPerTimerPass() {                         //Pixel die er pro TimerDurchgang hoch springt
        return pixelPerTimerPass;
    }

    public Physics setTimeElapsedInMs(long timeElapsedInMs) {
        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 getHeightJump() {
        return heightJump;
    }

    public Physics setHeightJump(int heightJump) {
        this.heightJump = heightJump;
        return this;
    }

    public int getJumpedPixelCounter() {
        return jumpedPixelCounter;
    }

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

    public long getFallStartTime() {
        return fallStartTime;
    }

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

//public

}
 
Y

yfons123

Gast
!pressing && !moveTimer.isRunning())
da hammas doch schon

du erlaubst es nicht mehr tasten zu drücken also wird es erst freigegeben wenn du los lässt und dann kommst du in das problem das ich dir erklärt habe und dir auch schon die lösung gesagt hab ( wenn nix gedrückt wird dann ist der input vektor V( 0 , 0 ) aber dein move klasse bewegt sich trotzdem in keine richtung ;) )

und du läufst in die typischen If und attribut monster rein die immer am anfang bei der spiele entwicklung kommen
und das passiert dadurch dass du deine objekte nicht austauscht ( kannst du ja auch nicht da du nur 2 klassen hast )
und da du die jobs nicht separierst sondern für den aktuellen zwischen fall denkst... eine tilemap funktionioert immer egal wie groß egal wie lang ( bis halt dein computer adios sagt ) dh das willst du in ( mindestens einer klasse ) haben um es NIE WIEDER anzuschauen.. und das ist halt wichtig dass du code weg haust den du dir nie wieder anschauen musst

zu methoden referenzen sag ich jetzt mal nix weil es allgemein am aufbau scheitert ( warum sollte ein tilemap bauer sich interessieren wo sich die figur befindet ? )


guter code braucht auch keine kommentare im regelfall... ansich nur javadocs die nochmal exakt sagen worums geht für die IDE dass du dich nicht an alles erinnern musst... aber die aktuellen kommentare von dir sind keine javadocs

PS darum gehts bei vektoren
1654021197428.png
 
Zuletzt bearbeitet von einem Moderator:
Y

yfons123

Gast
zusäztlich hast du das folgnede nicht bedacht

wenn du A und D drückst hast du ja nahc meinem ding einen vektor von V ( 0,1) und V(0,-1) dh dein charakter bewegt sich nirgnedswo hin da die vektor summe V 0 0 ist

sobald du einen button los lässt zb A dann hast du nur noch D und dann wird dein input vektor zu V ( 0 , 1 )somti hast du dein blockade system gelöst und hast mehr gebacken gekriegt als der bei dem projekt das du dir rechtmäßig ergaunert hast ( ;) )


Java:
    private BlockArrayList mapLayer1;
    private BlockArrayList mapLayer2;
    private BufferedImage tilesetBufferedImage;
    private final BufferedImage spritesBufferedImage;
    private final int ZOOM = 4;
    private final int TILESIZE = 16;
    private final String spritesFilename = GuiData.filenameSpriteSet;
    private int feetPosition = 2;  //1,2,3            (1 = rechter Fuß, 2 = normal, 3 = linker Fuß) Veraltetes Kommentar, da Wiederverwendung aus anderem Projekt
    private String direction = "right";
    private boolean pressing = false;
    private Timer moveTimer;
    private Timer jumpUpTimer;                                                                                    //Timer zum Hochspringen
    private Timer jumpDownTimer2;                                                                                   //Timer zum Runterfallen
    private int positionX = 2 * TILESIZE * ZOOM;  //Position des Charakters beim Start
    private int positionY = 10 * TILESIZE * ZOOM;  //Position des Charakters beim Start
    private int finalPositionX;
    private final int OFFSET = 12 * ZOOM;
    private int finalPositionY;
    private int walkSpeed = 4;
    private final int jumpDownTimerSpeed = 30;
    private long start;
    private final Physics physics = new Physics();
    private int feetPositionBackup;
    private String directionBackup;
.. sowas is doch einfach nur scheiße... warum denn ..

PS : deine schwerkraft wäre dann sowas

fallrate = VectorDown ( 0 , -1 ) * schwerkrafts formel dingens

du fällst ansich immer, nur wenn du am boden bist fällst du nicht durch


ein sprung wäre
sprung = VectorUp ( 0, 1 ) * sprung höhe

dein sprung scheitert übrigens daran dass dein programm es nicht schafft den Vektor ( 0,7 , 0,7 ) zu haben durch doppelten input, deswegen kannst du nicht nach vorne springen

aber dadurch dass du immer fällst hast du automatisch deine ellipse drinnen bzw den legendären mario sprung.. der immer fehlschlägt wenn man vor dem ziel ist :(
 
Zuletzt bearbeitet von einem Moderator:
Y

yfons123

Gast
um dir mal ganz kurz einen kleinen input über vektoren bei der bewegung von charakteren zu geben
 

Blender3D

Top Contributor
Ich habe mir das DuckyGame Beispiel mal runtergeladen aber auch da reagiert er träge auf wechselnde Tastendrücke (links, rechts).
Das kannst Du dadurch beheben in dem Du den KeyListener etwas verbessert.
Es wird in dem Code die Speed immer auf 0 gesetzt, sobald man eine Taste loslässt. Das sollte aber nur passieren, wenn die losgelassene Taste die war die vorher gedrückt wurde. Hier die Klasse mit dem verbesserten KeyListener für das Duckybeispiel.
Java:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

import javax.swing.JComponent;

@SuppressWarnings("serial")
public class DuckyGame extends JComponent implements Runnable {
    public final static int WIDTH = 600;
    public final static int HEIGHT = 400;
    public final static String[] names = { "diddyDuck", "obstacle1", "obstacle2" };
    public final static int DUCKY = 0;
    public final static int BUSH_A = 1;
    public final static int BUSH_B = 2;
    private int horizont = 175;
    private Duck duck;
    private float bushSpeed = 1.1f;
    private float duckSpeed = 4 * bushSpeed / 4;
    private ObstaclesLine bushes = new ObstaclesLine(horizont, WIDTH);
    private boolean running = false;
    private Thread aniamtor = null;

    public DuckyGame() {
        setPreferredSize(new Dimension(WIDTH, HEIGHT));
        initGame();
    }

    private void drawBackGround(Graphics g) {
        g.setColor(new Color(184, 233, 236));
        g.fillRect(0, 0, WIDTH, HEIGHT / 2);
        g.setColor(new Color(135, 121, 74));
        g.fillRect(0, HEIGHT / 2, WIDTH, HEIGHT / 2);
    }

    public KeyAdapter getKeyBoard() {
        return new KeyAdapter() {
            private int key = 0;

            @Override
            public void keyPressed(KeyEvent e) {              
                key = e.getKeyCode();
                if (key == KeyEvent.VK_LEFT)
                    duck.setSpeed(-duckSpeed);
                if (key == KeyEvent.VK_RIGHT)
                    duck.setSpeed(duckSpeed);
            }

            @Override
            public void keyReleased(KeyEvent e) {
                if (e.getKeyCode() == key)
                    duck.setSpeed(0);
                if (e.getKeyCode() == KeyEvent.VK_SPACE)
                    duck.jump();
            }
        };
    }

    private void initGame() {
        duck = new Duck(names[DUCKY], 50, 60);
        duck.setPos(100, horizont);
        bushes.create(new String[] { names[BUSH_A], names[BUSH_B] }, 50, 60);
        bushes.setSpeed(-bushSpeed);
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        drawBackGround(g);
        bushes.draw(g);
        duck.draw(g);
    }

    @Override
    public void run() {
        while (running) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            updateGame();
            repaint();
        }
    }

    public void start() {
        stop();
        running = true;
        aniamtor = new Thread(this);
        aniamtor.start();
    }

    private void stop() {
        running = false;
        while (aniamtor != null && aniamtor.isAlive())
            ;
        aniamtor = null;
    }

    private void updateGame() {
        duck.move();
        bushes.move();
        if (bushes.isCollision(duck)) {
            System.out.println("collision");
        }
    }
}
 

Anhänge

  • ducky.jar
    25,6 KB · Aufrufe: 1

KonradN

Super-Moderator
Mitarbeiter
Was man ggf. auch überlegen sollte: Mal nicht alles im UI Thread. Wenn ich das richtig gesehen habe, dann gehst Du im UI Thread die einzelnen Bilder durch um alle nacheinander zu malen. Das dauert auch eine gewisse Zeit und in der Zeit kann das System nicht auf Events reagieren.

Statt dessen kannst Du das in dem Thread erledigen, der alles berechnet: Dazu kannst Du mehrere Bilder in der Größe des Fensters haben:
a) Eines das gerade angezeigt wird.
b) Eines das parat steht für das nächste paint (kann null sein)
c) Eines das gerade gemalt wird.
d) Eines das gerade parat steht für das nächste malen (kann auch ggf zwei Bilder sein).

Das paint schaut also:
- Liegt ein neues Bild (b) vor? Dann wird das Bild aus a nach d gegeben und das neue Bild von b nach a verschoben.
- Dann wird das Bild in a gemalt.

Der Thread, der neue Bilder erzeugt (Die "Game Loop"), kann dann einfach:
- Die ganzen Berechnungen machen (Neue Positionen berechnen aus den Werten der Objekte).
- dann wird die Darstellung "gerendert" (Bei 2D spricht man wohl eher nicht von rendern denke ich mal.)
- dann wird das neue Bild in b) hinterlegt (und wenn da ein Bild vorhanden ist, kommt dieses vorher nach d)
- ein repaint wird ausgelöst

Der UI Thread hat damit nur noch minimale Aufgaben:
- WM_PAINT Event mal nur das fertige Bild was relativ flott gehen sollte
- Events setzen nur kurz etwas um a.la. "Objekt x hat eine neue Richtung".

So in der Form habe ich mal ein Spiel gebaut. Die Zugriffe auf die 4 Bildpositionen sind synchronisiert, d.h. wenn ein Thread die Referenz von b nach a kopiert, dann der Zugriff auf b gesperrt. Es kann also nicht der andere Thread die Referenz aus b nach d kopieren um dann in b das neue Bild abzulegen!) Aber wirklich nur dieses Referenz kopieren ist synchronisiert! Und aufpassen: Referenzen werden wirklich verschoben, d.h. wenn ich eine Referenz von b nach a kopiere, dann muss b im Anschluss gesetzt werden! Also z.B. auf null setzen falls da keine andere Referenz rein kopiert wird.

Das wäre so die Anregung, die ich noch mitgeben würde.
 
Y

yfons123

Gast
das multi threading mit den bildern wäre nicht nötig falls du dir eine art occlusion culling baust

dh du renderst nur das was du auch siehst, der rest bleibt versteckt damit belastest du den main thread nicht ( damit kannst du dir auch das multi threading sparen )
 

Hag2bard

Bekanntes Mitglied
Du meinst, dass nur das gezeichnet wird, was sich verändert?
Das wäre ja nur der Bereich den der Spielecharakter einnimmt. Was ist, wenn das Bild scrollt? Dann muss doch alles neu gerendert werden, oder?
 

Neumi5694

Top Contributor
Du meinst, dass nur das gezeichnet wird, was sich verändert?
Das wäre ja nur der Bereich den der Spielecharakter einnimmt. Was ist, wenn das Bild scrollt? Dann muss doch alles neu gerendert werden, oder?
... es ändert sich auch der Bereich, den der Charakter gerade verlassen hat. Aber es ging schon um den ganzen Bildschirm, aber eben nicht die Elemente, die außerhalb liegen. Die sind vergeudete Rechenleistung.

Früher nutzte man Sprites für Charaktere und bewegliche Objekte, mit einem Sprite wird zur Ausgabezeit direkt in den Grafikspeicher geschrieben, egal was darunter liegt. So musste man tatsächlich gar nichts neu berechnen (das Bild war ja schon vorgerendert) und nur die Sprites bewegen. Das ist heute dank der unglaublich viel höheren Rechenleistung nicht mehr notwendig. Beim Scrollen wurden immer Blöcke (nicht einzelne Pixel) bewegt, die der Blockgröße entsprachen, für die Farben definiert werden konnten (deshalb ist der Aufbau aller Nintendo-Spiele mehr oder minder gleich).

Totzdem muss man auch heute hin und wieder ein paar Tricks anwenden. Du kannst dir ja z.B. den gesamten Hintergrund in einer einzigen Grafik speichern (oder jeden Layer, falls du Parallax-Scrolling verwendest), die dann nur noch par paintImage eingefügt werden muss. Wenn Scrollen ein Thema ist, dann kannst du Bereiche außerhalb des Bildes vorberechnen.
Es gibt da reichlich Tutorials, etwas wurde ja schon oben genannt.
 
Y

yfons123

Gast
Du meinst, dass nur das gezeichnet wird, was sich verändert?
Das wäre ja nur der Bereich den der Spielecharakter einnimmt. Was ist, wenn das Bild scrollt? Dann muss doch alles neu gerendert werden, oder?
jap genau, und da kommst du in probleme rein die unity miserabel gelöst hat


zb du siehst 5 pixel von einem riesigen haus, dann wird das ganze haus gerendert weils minimal in der kamera liegt, eine lösung dafür wäre es deine Objjekte in scheibchen zu shcneiden aber da du sowieso keinerlei 3D hast denke ich mal dass die performance sowieso immer gut ist wenn du nicht grob fahrlässig programmierst

dein pc ist schneller als du denkst.. sogar die alten pcs sind schneller als du denkst...die sind nicht gleich am limit ( außer javafx wenn man hoch auflösende bilder also 4k in eine scroll pane tut da is dann das limit erreicht )

ist witzigerweise auch der hauptgrund warum UE bessere grafiken "verträgt" obwohl unitys kamera schneller ist...sie rendert eifnach nur sachen die sowieso keinen interessieren
in UE ist es fast ooptimal als default eingesetellt .. in unity brauchst du da mal ein paar monate
 

Hag2bard

Bekanntes Mitglied
Ja das sind auch noch ZukunftsProbleme. Meinen Input habe ich gebacken bekommen, jetzt habe ich das Problem dass er mal höher springt und mal nicht. Ich verstehe noch nicht wieso das so ist. Ich werde morgen mal meinen Quellcode posten, falls ich den nicht aktuell zerschossen habe. Wie ich eine Versionsverwaltung in IntelliJ einrichte, weiß ich noch nicht.
 

Hag2bard

Bekanntes Mitglied
Hallo nochmal,

ich habe Performance-Probleme mit meinem neuen MoveTimer. Also der Timer der dafür zuständig ist, dass der Spielcharakter läuft. Der MoveTimer wird am Anfang gestartet und läuft dann durchgängig. Wie bei dem DuckGame.
Das Problem ist, dass nach einer gewissen Zeit plötzlich die Performance einbricht, als würde irgendein Speicher oder sonstiges voll laufen.
Ich bekomme aber keine Fehlermeldung.
Woran könnte das liegen?
 
Y

yfons123

Gast
naja du bist in der spielentwicklung...

im gegensatz zu javafx oder swing im normalen umfeld... "wenn button drück dann"... hast du jetzt IRGEND einen input und es ist viel großflächiger

da du NATÜRLICH sehr objekt orientiert programmiert hast kannst du ja mal versuchen das problem einzugrenzen um ddas performance problem zu finden
 

Hag2bard

Bekanntes Mitglied
Eingegrenzt habe ich es in sofern schonmal, dass wenn der moveTimer ausbleibt, keine Performance-Probleme auftreten.
Java:
        moveTimer = new Timer(0, ae -> {
            if (mario.getSpeed() > 0) {
                if (jumpTimer.isRunning() || fallTimer.isRunning()) {
                    mario.setFeetPosition(2);                                                                           //FeetPosition 2 beim Jump heißt, dass Mario rechts guckt
                }
                if (mario.getPositionX() >= mario.getFinalPositionX() && !jumpTimer.isRunning() && !fallTimer.isRunning()) {      //Wenn 16 Pixel/halber Block erreicht dann feetPosition changen
                    mario.changeFeetPosition();
                    mario.refreshFinalPositionX();
                }
            }
            if (mario.getSpeed() < 0) {
                if (jumpTimer.isRunning() || fallTimer.isRunning()) {
                    mario.setFeetPosition(1);                                                                           //Wenn Mario links guckt, muss das entsprechende Bild gezeichnet werden
                }
                if (mario.getPositionX() <= mario.getFinalPositionX() && !jumpTimer.isRunning() && !fallTimer.isRunning()) {      //Wenn 16 Pixel/halber Block erreicht dann feetPosition changen
                    mario.changeFeetPosition();
                    mario.refreshFinalPositionX();
                }
            }
            mario.move(mario.getSpeed());
        });

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

KonradN

Super-Moderator
Mitarbeiter
Also das sagt uns so relativ wenig. Aber was auffällt ist, dass Du evtl. mehrere Timer hast. Erzeugst Du ggf. ständig neue Timer? Kann es sein, dass Du da neue immer mehr Timer erzeugst, die dann noch im Hintergrund aktiv sind? Das nur als eine mögliche Idee.
 

Neumi5694

Top Contributor
Schaut danach aus, das wäre schon etwas jenseits des Vernünftigen. Für alle Aktionen ein eigener Timer (movetimer, jumptimer, falltimer) ist schon etwas heftig.
Normalerweise hat man einen, der z.B. alle 10 Millisekunden die Auswertung startet. (früher wurde dazu oft die Soundkarte verwendet, später die Bildwiederholfrequenz des Monitors). Notier z.B., wann der Sprung angefangen hat und vergleiche mit der aktuellen Zeit. Dann reagier entsprechend.
 
Y

yfons123

Gast
Also unity hat es so gelöst dass die Physik alle 0.05 sek berechnet wird ( was ja fallen ist ) weil du es sonst übertreibst mit den berechnungen

"clean wäre es" wenn du einen Timer baust der während des spieles alle 0.05 sek alle registrierten methoden ausführt

zb sowas
Java:
// kannst du auch als singleton implementieren da es einfach nur falsch wäre
// wenn diese objekt nicht einzigartig ist
// und es würde dir die perfomrance zerschießen
public class PhysicsTimer
{
    void startPhysics(){
        foreach in Registered Methods => obj.PhysicsUpdate();
    }
    RegisterMethod(PhysicsReceiver receiver){
        ...
    }
   
}
public interface PhysicsReceiver{
    void PhysicsUpdate();
}

..

// der der das objekt erstellt muss es bei dem physics timer registrieren
public class Movement implements PhysicsReceiver{
   
    public void physicsUpdate(){
        hüpfen...
    }
}

EDIT: wenn du oop umsetzt sparst du dir 3/4 der ifs ;)
 
Zuletzt bearbeitet von einem Moderator:

KonradN

Super-Moderator
Mitarbeiter
Ich bin hier gerne so vorgegangen, dass ich einfach eine Gameloop hatte, die ständig lief.

Die Gameloop sah dann so aus, dass ich bei jedem Durchlauf erst die Zeit erfasst habe um dann alles zu machen Dabei wurde die Zeit mit beachtet (z.B. bei Bewegungen).
Nach jedem Durchgang habe ich dann die Zeit geprüft und geschaut, ob und wieviel ich schlafen soll um dann ggf. mich für kurze Zeit mit Thread.sleep schlafen zu legen.

Dieser Durchlauf war dann eigentlich dynamisch, d.h. das Spiel hat einfach eine Liste mit Aktionen und die ist er jedes Mal durchgegangen um diese so nacheinander auszuführen. (Das war bei mir etwas komplexer, da ich priorisierte Listen hatte. So war z.B. sicher gestellt, dass die Erzeugung der Darstellung erst am Ende erfolgte.

Halt etwas in der Art.
 
Y

yfons123

Gast
du bräuchtest auch noch eine Frame unabhängige zeit

die zeit vom letzten frame zum jetzigen ( eig müsste das swing haben.. sogar javafx hat das ) da du langsamer bist wenn du 1 fps hast oder bltizschnell mit 100 fps

mit der unabhängigkeit der frame rate in verbindung mit der zeit willst du das verhalten unterdrücken
 

Blender3D

Top Contributor
Der MoveTimer wird am Anfang gestartet und läuft dann durchgängig. Wie bei dem DuckGame.
Nicht wie beim DuckGame. Das hat außerdem einen sehr vereinfachten GameLoop.
Die gute Nachricht mit Swing lassen sich sehr gute ruckelfreie Spiele machen.
Hier ein kleines Beispiel eines Asteroidklones.
https://www.java-forum.org/thema/spielesammelthread.123839/#post-1288802
Der GameLoop sollte, wenn möglich, der einzige Thread sein der läuft.
Das funktioniert leider nicht da man auch Sound Tastatureingabe etc. benötigt.
Ich würde dir empfehlen dein Timer Konstrukt, zugunsten eines Runables wie beim Duckgame aufzugeben .
Ich habe mir eine abstrakte Klasse namens Display gemacht die von Canvas erbt und Runnable implementiert.
Sie besitzt unter anderem die 2 wichtigen Methoden
Java:
     /**
     * Do drawing stuff here.
     *
     * @param g
     */
    protected abstract void render(Graphics g);

    /**
     * Do game logic here.
     */
    protected abstract void update();
Jedes meiner Games verwendet diese Klasse. Ich muss mich in update dann um den Gameablauf kümmern
und in render um das Darstellen der Grafik.
Die Klasse besitzt noch eine privtate render Methode in der unter Anderem das Grafikobjekt erzeugt wird und das über die abstrakte Methode render nach außen gereicht wird. Das nennt man aktives Rendern. Im Gegensatz zum passiven Rendern wie es mittels paintComponent gemacht wird. Dort wird nur auf eine Änderung reagiert und dann wird neu gezeichnet. Es laufen aber im Hintergrund für unser Spiel unnötige Berechnungen (Clippiing , Zeichenmethoden von übergeordneten Klassen im Container enthaltene Componenten etc ) ab.
Das Ziel eines Gameloop ist es ja eine konstante Framerate zu erhalten. Beim passiven Rendern ist das Ziel möglichst wenig zu Zeichnen. Wir aber wollen ja das Bild bei z.B. 80 fps 80 mal in der Sekunde neu zeichnen.
--> aktives Rendern.
Java:
private void render() {
        BufferStrategy bs = getBufferStrategy();
        if (bs == null) {
            createBufferStrategy(3);
            return;
        }
        Graphics g = bs.getDrawGraphics();
        ..
        render(g);
        ..
Das DuckGame verwendet paintComponent, Das ist für ein schnelles Zeichnen nicht zu empfehlen und sollte vom Gameloop überhaupt nicht aufgerufen werden. Hier im Forum sollte duckgame einen primitiven Gameloop skizzieren.
Der Gameloop selbst wird dann in der Methode run realisiert.
Java:
@Override
    public void run() {
        ..
        while (running) {
            if (!paused) {
                update();
                render(); // render the game to a buffer
            }else
                overSleepTime = 0;
            ..

    }
.
Dort gilt es die Sleeptime des Threads konstant zu halten und gegebenfalls den Thread mittels
Code:
                    Thread.yield(); // give another thread a chance to run
,
an andere Threads periodisch abzugeben.

Ein weiter Tipp der eventuell schnell weiterhilft ist, falls Du Linux benutzt ist eventuell OpenGL deaktiviert.
Das kannst Du mittels
Code:
System.setProperty("sun.java2d.opengl", "true");
anschalten.
Ganz wichtig ist es außerdem, dass man auf die Erzeugung von vielen Objekten im GameLoop möglichst verzichtet. Warum? Nehmen wir einmal 80 fps. Du hast nun 20 Asteroiden die mittels Vektoren bewegt werden. Die Vektoren sind finale Objekte. --> für jede Positionsänderung werden pro Sekunde 1600 Vektoren mittels new erzeugt. Die Virtualmachine hat genug Speicher zur Verfügung um das zu kompensieren. Aber nach einer gewissen Zeit ist der dann knapp. Der Garbagecolector wird tätig und es kommt nach gewissen Perioden zu einem Ruckeln der Animation. Das Problem hatte ich bei einem Pacmanspiel 2016. Das war sogar der Grund dafür wie ich in dieses Forum gekommen bin.
 

Hag2bard

Bekanntes Mitglied
Hallo, danke für die zahlreichen Antworten, jetzt ist erstmal fraglich inwiefern ich das umgesetzt bekomme.
Erstmal zu den Timern:

Ich habe einen Timer der nur während des Sprungs aktiv ist. (jumpTimer)
Dieser Timer bewegt den Charakter x Pixel nach oben, wobei x hier Variabel ist.
In der Physics Klasse berechne ich in einer Methode, nach wievielen vergangenen Millisekunden er welche Geschwindigkeit haben muss, sprich wie hoch x ist.

Der Timer läuft so lange bis eine gewisse Endgeschwindigkeit erreicht ist, dann wird er gestoppt.

jumpTimer:
Java:
    private void timerDrivenJumpMethod() {
        setTimeElapsedInMs();     //Aktuell vergangene Zeit setzen

        for (int i = 0; i < getPixelPerTimerPass(); i++) {
            refreshPixelPerTimerPass(getPixelPerTimerPass());

            //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) {         //TODO das obere eingrauen!!!!                                    //Methode 2: Wenn er 288 Pixel erreicht hat, stoppt er den JumpUpTimer -> wenn nie erreicht bleibt er stehen -> unterschiedliche Sprungzeiten
//            if (physics.getDurationJump() >= physics.getJumpTimeInMs()) {                          //Methode 3: Wenn Zeit abgelaufen, dann stoppt er den JumpUpTimer -> unterschiedliche Sprunghöhen?

                jumpTimer.stop();
                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());                                  //Noch nicht benutzt
                fallTimer.start();                                                             //Fallen gestartet
                break;                                                                                 //Wenn If Bedingung eintritt, for Schleife abbrechen
            }
        }

    }

Danach wird der fallTimer gestartet, der stoppt wenn die Endposition (Boden) erreicht ist.

Java:
    private void timerDrivenFallMethod(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()) {
                fallTimer.stop();
                setFallDuration(0);
                mario.setDirection(mario.getDirectionBackup());
                mario.setFeetPosition(2);                                                                               //FußPosition 2 -> Mario steht
                break;
            }
        }
    }

Wie ihr seht, werden diese 2 Timer nicht ständig neu erstellt, sondern nur gestartet/gestoppt.
 

Blender3D

Top Contributor
Wie ihr sehr werden diese 2 Timer nicht ständig neu erstellt, sondern nur gestartet/gestoppt.
Ich verweise Dich auf meine Empfehlung von #46.
Du solltest das MarioGame hinten anstellen und dich auf den Gameloop konzentrieren. Lass dort Rechtecke oder Bälle an den Wänden abprallen. Mache dir eine Anzeige für die erreichten fps. Sobald du eine große Zahl deiner Objekte Ruckelfrei bewegen kannst hast Du einen brauchbaren Gameloop. --> Dann verwende diesen um dein Jump and Run zu realiseren.
Hier ein Beispiel für das Scrollen einer Landschaft mit der Klasse Display.
Bewege die Landschaft mittels Pfeil links und rechts.
1654796405667.png
 

Anhänge

  • scroll.jar
    580,8 KB · Aufrufe: 0
Zuletzt bearbeitet:

Neumi5694

Top Contributor
Probier's mal mit einem einzigen Timer. Ob seit dem Sprung x Millisekunden vergangen sind, kriegst du auch damit raus und hast keine Interrupts und ähnliches, die sich gegenseitig blockieren können.
Ich weiß ehrlich gesagt nicht, warum du überhaupt einen fall- und einen jump-timer brauchst. Nur, um die Geschwindigkeit zu erhöhen?

Viel macht das wohl nicht aus, aber zu viele einzelne System.outs können auch die Geschwindigkeit beeinflussen. Besser ist, den String vorher zusammenzustellen und dann als Ganzen auszugeben. In deinem Fall bietet sich die printf Methode an,
 

Ähnliche Java Themen

Neue Themen


Oben