Ruckeln von Ping-Pong Game trotz hoher FPS

hunter19441

Grünschnabel
Hallo zusammen,

ich habe ein Ping-Pong in Java geschrieben welches auch soweit super läuft, außer das es auf meinem Laptop ruckelt und auf meinem großem Pc nicht. Ich hab bereits einen Frame-Counter eingebaut, ich begrenze die Zahl der Game-Loop Durchläufe über ein sleep(16) woraus ca. 60fps resultieren.
Auf beiden Rechnern werden mir 60fps angezeigt und es ruckelt auf meinem Laptop, die CPU Auslastung steigt bei dem Programm um ein paar Prozent an, aber keine komplette Auslastung. Ich benutze kein BufferedStrategy, oder ein selbst gebautes DoubleBuffering, nur das von JPanel integrierte DoubleBuffering ist eingeschaltet.
Code Verbesserungsvorschläge nehme ich gerne an ! ;)

Java:
protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        Graphics2D g2 = (Graphics2D) g;

        // Prüfen ob Objekte bereits aufgebaut
        if (GerdPong.ball == null && GerdPong.player1 == null) {
            return;
        }

        g.drawString(this.fps + " fps", 10, 20);
        g.drawString(GerdPong.player1.points + " : " + GerdPong.player2.points, 475, 20);

       
        while (!this.GameObjects.empty()) {
            g2.fill((Shape) GameObjects.pop());
        }

    }

Hier die Game-Loop Methode
Java:
    public void run() {

        long last;

        int width, height, x, y;
        x = GerdPong.gw.getX();
        y = GerdPong.gw.getY();
        width = GerdPong.gw.PanelSize().width;
        height = GerdPong.gw.PanelSize().height;

        System.out.println(x + " " + " " + y + " " + width + " " + height);

        this.doGameInit(width, height);

        while (true) {

            // START DER FRAME MESSUNG //
            last = System.nanoTime();

            // Player 1 Kollisionserkennung
            // #################################################################################
            if (GerdPong.player1.isCollideArc(GerdPong.ball.mPosX, GerdPong.ball.mPosY, GerdPong.ball.radius) == 5) {
                if (GerdPong.ball.xDir == -1 && GerdPong.ball.yDir == -1) {
                    GerdPong.ball.xDir = 1;
                    // y = -1
                } else if (GerdPong.ball.xDir == -1 && GerdPong.ball.yDir == 1) {
                    GerdPong.ball.xDir = 1;
                    // y = 1
                }
            }
            // #################################################################################

            // Player 2 Kollisionserkennung
            // #################################################################################
            if (GerdPong.player2.isCollideArc(GerdPong.ball.mPosX, GerdPong.ball.mPosY, GerdPong.ball.radius) == 6) {
                if (GerdPong.ball.xDir == 1 && GerdPong.ball.yDir == 1) {
                    GerdPong.ball.xDir = -1;
                    // y = 1
                } else if (GerdPong.ball.xDir == 1 && GerdPong.ball.yDir == -1) {
                    GerdPong.ball.xDir = -1;
                    GerdPong.ball.yDir = -1;
                }
            }
            // #################################################################################

            // Abrallen des Balls am Bildschirmrand //
            GerdPong.ball.isCollideHrzScr(x, y, width, height);

            /// Spieler Punktkollision + Neurstarten des Spiels (softreset) + countdown starten ///
            // Linker Screen -> Player2 point++
            if (GerdPong.ball.isCollideVrtScr(x, width) == 1) {
                GerdPong.player2.points++;

                // Spieler zurücksetzen
                GerdPong.player1.resetMove();
                GerdPong.player1.move();
                GerdPong.player2.resetMove();
                GerdPong.player2.move();
                GerdPong.ball.reset();

            } // Rechter Screen -> Player1 point++
            else if (GerdPong.ball.isCollideVrtScr(x, width) == 2) {
                GerdPong.player1.points++;

                // Spieler zurücksetzen
                GerdPong.player1.resetMove();
                GerdPong.player1.move();
                GerdPong.player2.resetMove();
                GerdPong.player2.move();
                GerdPong.ball.reset();

            }

            // Spielfeldbeschränkung der Spieler //
            GerdPong.player1.isCollideScreen(y, height);
            GerdPong.player2.isCollideScreen(y, height);

            // Spieler bewegen und zeichnen //
            GerdPong.player1.move();
            GerdPong.player1.stackRect();
            GerdPong.player2.move();
            GerdPong.player2.stackRect();

            // Ball bewegen
            GerdPong.ball.move();
            GerdPong.ball.stackBall();

            // Alle Änderungen zeichnen
            GerdPong.gw.repaint();

            try {
                sleep(16);
            } catch (InterruptedException ex) {
                Logger.getLogger(Game.class.getName()).log(Level.SEVERE, null, ex);
            }
            // ENDE DER FRAME MESSUNG //
            fps = frames(System.nanoTime() - last);
        }
    }


Tech. Daten der Rechner
Laptop:
Acer
Ubuntu 64bit + Java 1.8...
4gb Ram & CPU N3540 @ 2.16GHz × 4 + integrierte Intel Graphics HD

PC:
Win 7 64bit + Java 1.8..
8gb & Amd Phenom II x4 & ATI HD 5700 series

Auf beiden Rechnern nutze ich Netbeans JDK 1.8..


So das müsste eig alles sein.
Danke für eure Hilfe.
 
Hi

Wie ruckelt es: Ist die Bewegung insgesamt langsamer als sie sein sollte,
oder ist sie gleich schnell nur eben in unregelmäßigen Sprüngen?
(oder ist es nicht genau erkennbar weil der Unterschied zu klein ist?)

Hast du versucht, die Sleep-Dauer etwas zu verringern?
16 passt zwar ungefähr für 60fps, aber mehr schadet nicht
und kann bei Swing schon alles sein, was nötig ist.
 
Also ich hab es gerade mal mit sleep(10), 5 und 2 ausprobiert und es läuft durchaus flüssiger :-/ mhhhh
Zur Art des Ruckelns, die Geschwindigkeit ist bleibt konstant nur der Ball bzw. die Spielerrechtecke springen in ungleichmäßigen Abständen.
Was ich noch beobachtet habe, dass der Ball seine Richtung bereits geändert hatte, ohne das es zu einer optischen Kollision kam. Der Ball befand sich zum Zeitpunkt der Richtungsänderung ein gutes Stück vor dem Spieler mit dem er kollidierte ( bezieht sich auf ein sleep(16)).
 
es läuft durchaus flüssiger :-/ mhhhh
Ist es flüssig genug (= Problem gelöst) oder noch nicht?

Oder willst du es unbedingt vermeiden, mehr Leistung zu brauchen, und deshalb die FPS lieber unverändert lassen?
In dem Fall seh ich allerdings keine einfachen Alternativen. Hab mit Swing schon selber genug Animiertes gemacht
und das auch nie geschafft :p Man könnte natürlich gleich aufs Ganze gehen, Richtung OpenGL
(ohne oder mit Hilfslib/Engine, egal), nur Swing ist dafür einfach nicht geeignet.
 
Oder willst du es unbedingt vermeiden, mehr Leistung zu brauchen, und deshalb die FPS lieber unverändert lassen?
Ja genau :), aber ich denke mal das wie du bereits sagst, dass Swing nicht für so etwas gemacht wurde und ich mich nach Alternativen umsehen muss.
Kannst du mir vielleicht eine grobe Richtung raten in die ich mich orientieren könnte wenn ich nicht mehr Swing zur Animation nutzen möchte ?

Danke für deine Hilfe !
 
Also, gleich eine Warnung zuerst: Swing ist vermutlich etwas angenehmer. Ist zwar bei dir auch nicht wirklich das ideale Einsatzgebiet (Dialoge mit Buttons, Textfeldern usw.), aber Richtung "echte" Grafik ist ein anderes Level als paintComponent usw. (ich vermute, dass dich das generell interessiert. Nur wegen der FPS-Sache für dieses eine Programm lohnt sich das Einarbeiten sicher nicht)

Wie überall in der Programmierung gibts mehrere Abstraktionslevel, in diesem angefangen mit dem genau angeben der Byte, die zur Grafikkarte gesendet werden :p . Ok, ernsthaft: Für das "Unterste", was zum Programmieren Sinn macht, gibt es zwei Konkurrenten: OpenGL und DirectX,. DirectX kann man im Java-Zusammenhang eher vergessen, es ist von Microsoft und damit ziemlich RIchtung Windows-only. OpenGL ist eben mehr plattformunabhängig.
Aus Programmierersicht sind die beiden Sachen im Wesentlichen eine Sammlung von Funktionen, mit denen man so ziemlich alles Grafische machen kann. Erstellen von irgendwelchen Formen in 2D/3D-Koordinaten, Malen von Bildern darauf, Verändern des gedachten Sichtwinkels (womit man eben eine passende verzerrte Ansicht bekommt usw.), Neuberechnen lassen der Farben wenn auf Punkt XYZ ein gedachtes Licht in Richtung R scheint usw.usw.
Für ein Pingpongspiel werden die meisten Sachen nicht nötig sein, trotzdem wird die Programmierung umständlicher (schon weil die verwendeten Funktionen/Methoden eben so vielseitig sind).
Um eine ungefähre Idee vom Lowlevel-Grad zu bekommen, ein paar zusammenkopierte Codezeilen (passen nciht wirklich sinnvoll zusammen):
Code:
glPushMatrix();
glTranslatef(Display.getDisplayMode().getWidth() / 2, Display.getDisplayMode().getHeight() / 2, 0.0f);
glRotatef(angle, 0, 0, 1.0f);
glBegin(GL_QUADS);
glVertex2i(-50, -50);
glVertex2i(50, -50);
glVertex2i(50, 50);
glVertex2i(-50, 50);
glEnd();
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0.0, Display.getDisplayMode().getWidth(), 0.0, Display.getDisplayMode().getHeight(), -1.0, 1.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glViewport(0, 0, Display.getDisplayMode().getWidth(), Display.getDisplayMode().getHeight());
Tutorials zu OpenGL gibts zuhauf im Internet, würde aber Englische empfehlen. Die von NeHe haben einen guten Ruf. (ist in dem Fall zwar für die Programmiersprache C, aber noch kurz weiterlesen, stört nicht so).
OpenGL (und DirectX) selber sind von sich selbst aus vorwiegend auf C/C++ ausgerichtet, es gibt aber für viele andere bekannte Sprachen Verwendungsmöglichkeiten. Für Java wäre zB. LWJGL (http://www.lwjgl.org/)/ zu nennen, mit dem man die selben Funktionen wie in C ziemlich unverändert in Javacode verwendet kann. Ein ganz simples Programmgrundgerüst-Beispiel findet sich bei http://wiki.lwjgl.org/index.php?title=Examples:Game, ein anscheinend vollständiges Space-Invaders-Spiel (nur ohne Bilddateien und so) bei http://wiki.lwjgl.org/index.php?title=Examples:SpaceInvaders_Game . So als Anregung. Ausführlichere Doku zu den Funktionen gibts natürlich.

Das andere Extrem wären Spiele-Engines (die wiederum auf OpenGL aufbauen).
Einerseits Spezialisierte für einen bestimmten Spieletyp wie zB. Firstperson-Shooter, die oft schon so vollständig sind, dass man Hinzufügen von Grafikdateien und sehr wenigen Codezeilen schon eine einfaches lauffähiges Spiel hat und eigentlich nur noch den Teil programmieren muss, der einen von anderen SHootern unterscheidet.
Andererseits eben allgemein gehaltene Programmierlibraries, die ca. die selben Möglichkeiten wie OpenGL selber in Funktionen anbeiten, nur angenehmer zu verwenden. zB. mit einer Codezeile aus einer Grafikdatei einen Mensch oder sowas laden, der mit der näcjhsten Codezeile schon am Bildschirm herumgeht (passende Grafikmodels vorausgesetzt), statt sich mit Koordinaten von Dreiecken herumärgern zu müssen.
Beides ist wohl unpassend bzw. viel zu viel für ein Pingpong-Spiel. Jedenfalls gibts für Java zB. die jMonkeyEngine, ein Screenshot aus einem Spiel damit wäre zB. http://jmonkeyengine.org/project/chaos-in-the-darkness/ wie gesagt, viel zu viel).

Und zwischen Rohfunktionen und vollen Spielengines...? Gute Frage :D Nicht dass ich ein Experte in Mittel-Level-Java-Grafik-Engines wäre, aber so wirklich sinnvolle Sachen (für Java) verstecken sich wohl grad alle vor mir. LibGDX und Slick2D sind zwei Namen, die mir Google ausspuckt, aber für eine Beurteilung ob die brauchbar sind find ich irgendwie zu wenig (und das ist kein gutes Zeichen. Wenn etwas von wenig Leuten verwendet wird, obwohl es deutlich mehr brauchen...)

Naja, soviel einmal für einen sehr kleinen Überblick ( :D )
Wenns in irgendeine Richtung tiefer reingehen soll kann man ja mehr dazu schreiben
 
Ok, danke für die ausführliche Erklärung.
Kla das sich das Neuschreiben für so eine "billige" Anwendung + Einarbeitung in OpenGL nur für dieses Problemchen nicht gerechtfertigt ist, aber ich denke ich führe mir mal OpenGL im Zusammenspiel mit Java zu Gemüte. :confused:
Ist ja nicht so, als könnte ich das Wissen was ich aus dieser Einarbeitung in OpenGL danach wieder wegwerfen.
Es stört mich halt, dass ein solch einfaches Programm nicht "rund" läuft. :p
 
Hallo hunter19441,
sheel hat ja schon einiges zum Thema Spiele, Java und Grafik geschrieben. Ich möchte eigentlich auch nur noch anmerken, dass die Frame-Einbrüche vermutlich ein Designfehler sind.
Deine Hauptroutine lautet (stark vereinfacht und als Pseudocode) etwa so:
Code:
public void run()
{
    Bereite die Hauptroutine vor;
  
    WIEDERHOLE {
      
        letzteMessung = vergangeneNanosekundenSeitEinemBestimmenZeitpunkt();
      
        Aktualisiere den Spielstand;
      
        Zwinge das System 16 Millisekunden zu warten; // nicht gut
      
        fps = BerechneFPSausZeitmessung(vergangeneNanosekundenSeitEinemBestimmtenZeitpunkt(),
                                        letzteMessung);
      
    }
}
Verstehst du das Problem? Du möchtest erreichen, dass deine Routine ein Frame jede 60stel Sekunde zeichnet, aber stattdessen zwingst du die Hauptroutine einfach eine 60stel Sekunde zu Warten. An dieser Stelle wäre die Funktion frames(int, int) wichtig, denn eigentlich deutet deine Messung von 60 FPS darauf hin, dass das Aktualisieren des Spielstandes überhaupt keine Zeit braucht (es fällt mir ehrlichgesagt etwas schwer das zu glauben).
Jedenfalls vernachlässigst du bei diesem Aufbau der Routine zwei Schritte die ebenfalls Zeit brauchen: Das eigentliche Berechnen des nächsten Frames, und das eigentliche Zeichnen des nächsten Frames. Stell dir vor, deine Kollisionserkennung braucht eine 60stel Sekunde (ich weiß, das ist übertrieben :D). Dann hättest du schon bloß noch 30 FPS, weil deine Hauptroutine zwei 60stel Sekunden zur Ausführung braucht...
Wenn du dann noch bedenkst, dass das Zeichnen auch etwas Zeit beansprucht, dann kommt halt alles zusammen...
Eigentlich sollten dir deshalb weniger als die theoretischen 62,5 FPS angezeigt werden, deshalb wundere ich mich über die Zahl (aber wie gesagt, dafür müssten wir uns die Funktion frames(int, int) anschauen).
Jedenfalls kommen die Ruckler vermutlich daher, dass deine Spielstandaktualisierung je nach Fall unterschiedlich lange braucht. Du siehst ja selbst, da sind einige IF-Blöcke eingebaut. Diese nicht konstante Laufzeit, die linear an die Gesamtlaufzeit angehängt wird, sorgt für Ruckler.
Im Gaming-Bereich benutzt man deshalb ein etwas anderes Design, das in etwa so aufgebaut sein könnte (auch wieder vereinfachter Pseudocode):
Code:
public void run()
{
    Bereite die Hauptroutine vor;
  
    WIEDERHOLE {
      
        letzteMessung = vergangeneNanosekundenSeitEinemBestimmenZeitpunkt();
      
        aktualisiereSpielstand(dt); // alle zeitabhängigen Funktionen orientieren sich an dt
      
        vergangeneMillisekunden = (vergangeneNanosekundenSeitEinemBestimmtenZeitpunkt()
                                  - letzteMessung) / 1000000; // 1.000.000 ns = 1 ms
                                
        übrigeZeit = 16 ms - vergangeneMillisekunden;
      
        WENN übrigeZeit < 0 ms {
          
            // wir überspringen ein Frame da wir hinterher hängen
          
            Warte für (16 ms + übrigeZeit) ms;
          
            fps = 1000 ms / (16 ms + 16 ms) // 31,25 Hz
          
            dt = 16 ms + 16 ms;
          
            Fahre mit nächstem Schleifendurchgang fort;
          
        }
      
        Warte für übrigeZeit ms;
      
        fps = 1000 ms / 16 ms; // so wollen wir es (62,5 Hz)
      
        dt = 16 ms;
      
    }
}
Das ist natürlich nur ein mögliches Design. In der Engine-Entwicklung gibt es viele verschiedene Designs, jedes mit Vor- und Nachteilen. In diesem Fall hättest du den Vorteil, dass dein Spiel im Normalfall auf 60 FPS läuft. Falls der Prozessor das aber nicht schafft, können einzelne Frames übersprungen werden, das Programm läuft also zur Not auf 30 FPS noch flüssig. Noch mehr kannst du den Computer aber nicht überladen, das ist wohl einer der Nachteile.
Postest du mal deine Funktion frames(int, int)?

Grüße Technipion
 
Zuletzt bearbeitet:
Thats all:
Java:
    long frames(long delta) {

        return ((long) 1e9) / delta;

    }

Mh ich verstehe, ich hab diese delta Geschichte komplett außer Acht gelassen, weil ich mir dachte "Jo gut, son olles Spiel da muss ich das bestimmt nicht berücksichtigen, läuft ja auf meinem Pc..." :oops::p
Denkste Pustekuchen :D

Aber was du sagt bzw. das grobe Code-Design ist durchaus logisch, ich habe mich auch über die 60fps gewundert, als bräuchte das Spiel (die Ifs und repaints() etc...) überhaupt keine Zeit....:confused: was ja nicht stimmt.

Danke für die Erleuchtung ! ;)

PS: Was mir jedoch noch nicht ganz klar ist, wieso müssen alle Funktionen den delta (in deinem Fall den "dt") Wert kennen ? Wie sieht so etwas implementiert aus, wenn ich von einer Bewegungs-Methode ausgeht die den X Wert inkrementiert um ein Rechteck zu bewegen, muss ich addieren, multiplizieren... ?
 
Hi hunter19441,
Mh ich verstehe, ich hab diese delta Geschichte komplett außer Acht gelassen, weil ich mir dachte "Jo gut, son olles Spiel da muss ich das bestimmt nicht berücksichtigen, läuft ja auf meinem Pc..."
Das kenne ich nur zu gut :D. Genau so habe ich es früher auch gemacht, allerdings wirst du spätestens bei einem etwas größeren Projekt merken, dass diese "Deltageschichte" sehr wichtig ist. Würdest du ein Pong das 1995 entwickelt wurde auf einem heutigen Rechner spielen, dann könntest du den Ball gar nicht sehen weil er sich unglaublich schnell bewegt ;)...
Der Grundgedanke dabei ist: Statt von einer konstanten FPS-Rate auszugehen - was einen konstanten Zeitsprung zwischen dem Aufrufen deiner Funktionen zur Folge hätte - machst du deine Funktionen direkt abhängig von der vergangenen Zeit. Wenn du mal ein Frame überspringst, oder ein sehr schneller Computer sogar 120 FPS rendert, bleibt der Spielablauf trotzdem gleich, anstatt schneller oder langsamer zu werden.
Betrachten wir also den Ball im Spiel, dann folgt er dem einfachen physikalischen Gesetz v = s / t (Geschwindigkeit = Strecke pro Zeit). Das lässt sich auch als Differential schreiben (also mit so 'nem d davor, ich erkläre aber nicht warum): dv = ds / dt. Das kannst du also nach der Strecke umstellen, die der Ball seit dem letzten Frame zurückgelegt hat, wenn du die Geschwindigkeit des Balles kennst: ds = dv * dt (zurückgelegte Strecke (pro Frame) = Geschwindigkeit (in diesem Frame) mal vergangene Zeit (seit letztem Frame)).
Wenn du also sagen möchtest, der Ball soll eine halbe Sekunde brauchen vom einen Bildschirmrand zum anderen, dann ist seine Geschwindigkeit (in x-Richtung): dv = Bildschirmbreite[in Pixel] / 0,5 s. Also sagen wir einfach bei 800 x 600 wäre dv = 1600 Pixel / s.
Normalerweise würdest du den Ball dann ds = dv * dt = 1600 Pixel / s * 0,016 s = 26 Pixel pro Frame nach rechts/links verschieben. Falls du aber ein Frame aussetzt (oder vielleicht auch einfach die FPS-Rate änderst), passt sich diese Funktion automatisch an: ds = dv * dt = 1600 Pixel / s * 0,032 s = 51 Pixel. Das Spiel läuft also für den Spieler immer gleich schnell ab, egal wie viele Bilder er pro Sekunde sieht.
Ich weiß es bedeutet einen Mehraufwand die Funktionen so zu schreiben, aber es hat sich in der Praxis schon vor Jahrzehnten etabliert.

Gruß Technipion

PS: Witziger Funfact am Rande: Wenn du es so machst wie oben gezeigt, kannst du einfach alle 5-10 Schläge dv leicht erhöhen, und der Ball wird dadurch schneller :D.
 
Zurück