2009-04-21 3 views
13

Ich schreibe ein einfaches Spiel, und ich möchte meine Bildrate bei 60 fps begrenzen, ohne dass die Schleife meine CPU essen. Wie würde ich das tun?Wie schalte ich meine Framerate bei 60 fps in Java ein?

+4

Kodieren Sie nicht 60 FPS, ermitteln Sie die tatsächliche Bildwiederholfrequenz des Displays. 60 FPS werden auf z.B. ein 85-Hz-Monitor. –

Antwort

32

Sie können die Game Loop Article lesen. Es ist sehr wichtig, dass Sie zuerst die verschiedenen Methoden für die Spielschleife verstehen, bevor Sie versuchen, etwas zu implementieren.

+0

Leicht verständlicher Artikel. – mfx

5

Die einfache Antwort ist, einen java.util.Timer alle 17 ms zu feuern und Ihre Arbeit im Timer-Event zu erledigen.

+1

Was ist, wenn es mehr als 17ms dauert, um die Arbeit im Timer-Event zu beenden? – cherouvim

+0

@cherouvim: Dann sind Sie überlastet und Ihre Bildrate sinkt. Sie arbeiten immer noch so schnell wie möglich, aber nicht schneller als 60 fps. –

4

Hier ist, wie ich es in C++ getan habe ... Ich bin sicher, dass Sie es anpassen können.

void capFrameRate(double fps) { 
    static double start = 0, diff, wait; 
    wait = 1/fps; 
    diff = glfwGetTime() - start; 
    if (diff < wait) { 
     glfwSleep(wait - diff); 
    } 
    start = glfwGetTime(); 
} 

Rufen Sie es einfach mit capFrameRate(60) einmal pro Schleife. Es wird schlafen, also verschwendet es keine kostbaren Zyklen. glfwGetTime() gibt die Zeit seit dem Programmstart in Sekunden zurück ... Ich bin mir sicher, dass Sie irgendwo in Java ein Äquivalent finden.

+1

In Java wäre das Äquivalent von glfwGetTime() System.currentTimeMillis(). Sie gibt die Zeit in Millisekunden seit dem 1. Januar 1970 zurück. Wenn sie so verwendet wird, geschieht dies in etwa genauso, außer dass sie die Zeit in Millisekunden zurückgibt. – Manitobahhh

0

In Java könnten Sie System.currentTimeMillis() tun, um die Zeit in Millisekunden statt glfwGetTime() zu erhalten.

Thread.sleep(time in milliseconds) macht den Thread warten, falls Sie nicht wissen, und es muss in einem try Block sein.

0

Was ich getan habe, ist einfach weiter zu durchlaufen und zu verfolgen, wenn ich zuletzt eine Animation gemacht habe. Wenn es mindestens 17 ms war, dann gehe durch die Animationssequenz.

So kann ich nach Benutzereingaben suchen und Musiknoten nach Bedarf ein- und ausschalten.

Aber das war in einem Programm zu helfen, Musik an Kinder zu unterrichten und meine Anwendung war den Computer in der Lage, dass es Fullscreen war, so dass es nicht gut mit anderen spielen.

0

Wenn Sie mit Java Swing die einfachste Art und Weise eine 60 fps zu erreichen, ist durch einen javax.swing.Timer wie diese Einrichtung und ist ein gemeinsamer Weg Spiele zu machen:

public static int DELAY = 1000/60; 

Timer timer = new Timer(DELAY,new ActionListener() { 
    public void actionPerformed(ActionEvent event) 
    { 
     updateModel(); 
     repaintScreen(); 
    } 
}); 

Und irgendwo in Ihrem Code müssen Sie diesen Timer einstellen, um es zu wiederholen und zu starten:

timer.setRepeats(true); 
timer.start(); 

Jeder zweite hat 1000 ms und durch diese mit Ihren Bildern pro Sekunde (60) zu teilen und den Timer mit dieser Verzögerung des Einrichtens (1000/60 = 16 ms gerundet down) erhalten Sie eine etwas feste Framerate. Ich sage "etwas", weil dies stark davon abhängt, was Sie in den obigen updateModel() - und repaintScreen() - Aufrufen tun.

Um vorhersagbare Ergebnisse zu erzielen, versuchen Sie, die beiden Anrufe im Timer zu synchronisieren und sicherzustellen, dass sie innerhalb von 16 ms abgeschlossen sind, um 60 fps zu erhalten. Mit intelligenter Vorbelegung von Speicher, Wiederverwendung von Objekten und anderen Dingen können Sie die Auswirkungen des Garbage Collectors reduzieren. Aber das ist für eine andere Frage.

+0

Das Problem mit Timern ist vergleichbar mit dem von Thread.Sleep ... es gibt keine Garantie, dass die Verzögerung genau ist. Im Allgemeinen ist die tatsächliche VERZÖGERUNG unvorhersehbar und wird möglicherweise etwas länger sein als die, die Sie als DELAY-Parameter übergeben haben. –

1

Timer ist ungenau für die Steuerung von FPS in Java. Ich habe das aus zweiter Hand herausgefunden. Sie müssen Ihren eigenen Timer implementieren oder Echtzeit-FPS mit Einschränkungen durchführen. Verwenden Sie den Timer jedoch nicht, da er nicht zu 100% genau ist und daher Aufgaben nicht ordnungsgemäß ausführen kann.

7

ich die Game Loop Article nahm die gebucht @cherouvim, und ich nahm die „Best“ Strategie und versucht, es für eine Java-Lauffähig neu zu schreiben, scheint für mich zu arbeiten

double interpolation = 0; 
final int TICKS_PER_SECOND = 25; 
final int SKIP_TICKS = 1000/TICKS_PER_SECOND; 
final int MAX_FRAMESKIP = 5; 

@Override 
public void run() { 
    double next_game_tick = System.currentTimeMillis(); 
    int loops; 

    while (true) { 
     loops = 0; 
     while (System.currentTimeMillis() > next_game_tick 
       && loops < MAX_FRAMESKIP) { 

      update_game(); 

      next_game_tick += SKIP_TICKS; 
      loops++; 
     } 

     interpolation = (System.currentTimeMillis() + SKIP_TICKS - next_game_tick 
       /(double) SKIP_TICKS); 
     display_game(interpolation); 
    } 
} 
+0

Es ist das Skelett des besseren Wegs, eine Spielschleife zu machen, aber es erklärt nicht viel tut es? Vielleicht ein paar kurze Aufzählungspunkte hinzufügen, die Dinge erklären? –

+0

Das ist etwas wie ich es mache. Ok, also wir Schleife, und wenn die Zeit zwischen Schleifen Iterationen ist weniger als überspringen, wir nur noch eine Schleife. Wenn ein Sprung oder mehr Zeit verstrichen ist, rufen wir die Hauptspielmaschine auf und fragen nach dem nächsten Rahmen. Ich mag es, den ganzen Rahmen als Bild zu ziehen und an die Malmethode zu senden. In meiner Spielklasse berechne ich gerne Max Frame pro Sekunde, wenn der Spieler den tatsächlichen FPS zeigen möchte, wenn ich Max fps einstelle, ist der tatsächliche Wert immer ein paar Frames weniger als der eingestellte Betrag, dh bei 60 ist tatsächlich 57 eingestellt. –

2

Hier ein Tipp Schaukel für die Verwendung von und das Erzielen einigermaßen genauer FPS ohne Verwendung eines Timers.

Erstens, führen Sie nicht die Spielschleife im Ereignis Dispatcher-Thread, sollte es in einem eigenen Thread die harte Arbeit zu tun und nicht in die Richtung der Benutzeroberfläche.

Die Swing-Komponente das Spiel die Anzeige selbst diese Methode überschrieben ziehen sollte:

ist
@Override 
public void paintComponent(Graphics g){ 
    // draw stuff 
} 

Der Haken, dass das Spiel Schleife nicht weiß, wann der Event-Dispatcher-Thread Runde tatsächlich hat bekommen, weil die Farbe Aktion durchführen In der Spielschleife rufen wir nur component.repaint() auf, das nur eine Farbe anfordert, die später passieren kann.

Mit wait/notify können Sie die Methode zum erneuten Zeichnen warten lassen, bis die angeforderte Farbe passiert ist, und dann fortfahren.

Hier komplette Arbeitscode von https://github.com/davidmoten/game-exploration, die eine einfache Bewegung eines Strings macht über ein JFrame das Verfahren unter Verwendung von oben:

import java.awt.BorderLayout; 
import java.awt.Component; 
import java.awt.Graphics; 
import java.awt.Image; 

import javax.swing.JFrame; 
import javax.swing.JPanel; 

/** 
* Self contained demo swing game for stackoverflow frame rate question. 
* 
* @author dave 
* 
*/ 
public class MyGame { 

    private static final long REFRESH_INTERVAL_MS = 17; 
    private final Object redrawLock = new Object(); 
    private Component component; 
    private volatile boolean keepGoing = true; 
    private Image imageBuffer; 

    public void start(Component component) { 
     this.component = component; 
     imageBuffer = component.createImage(component.getWidth(), 
       component.getHeight()); 
     Thread thread = new Thread(new Runnable() { 
      @Override 
      public void run() { 
       runGameLoop(); 
      } 
     }); 
     thread.start(); 
    } 

    public void stop() { 
     keepGoing = false; 
    } 

    private void runGameLoop() { 
     // update the game repeatedly 
     while (keepGoing) { 
      long durationMs = redraw(); 
      try { 
       Thread.sleep(Math.max(0, REFRESH_INTERVAL_MS - durationMs)); 
      } catch (InterruptedException e) { 
      } 
     } 
    } 

    private long redraw() { 

     long t = System.currentTimeMillis(); 

     // At this point perform changes to the model that the component will 
     // redraw 

     updateModel(); 

     // draw the model state to a buffered image which will get 
     // painted by component.paint(). 
     drawModelToImageBuffer(); 

     // asynchronously signals the paint to happen in the awt event 
     // dispatcher thread 
     component.repaint(); 

     // use a lock here that is only released once the paintComponent 
     // has happened so that we know exactly when the paint was completed and 
     // thus know how long to pause till the next redraw. 
     waitForPaint(); 

     // return time taken to do redraw in ms 
     return System.currentTimeMillis() - t; 
    } 

    private void updateModel() { 
     // do stuff here to the model based on time 
    } 

    private void drawModelToImageBuffer() { 
     drawModel(imageBuffer.getGraphics()); 
    } 

    private int x = 50; 
    private int y = 50; 

    private void drawModel(Graphics g) { 
     g.setColor(component.getBackground()); 
     g.fillRect(0, 0, component.getWidth(), component.getHeight()); 
     g.setColor(component.getForeground()); 
     g.drawString("Hi", x++, y++); 
    } 

    private void waitForPaint() { 
     try { 
      synchronized (redrawLock) { 
       redrawLock.wait(); 
      } 
     } catch (InterruptedException e) { 
     } 
    } 

    private void resume() { 
     synchronized (redrawLock) { 
      redrawLock.notify(); 
     } 
    } 

    public void paint(Graphics g) { 
     // paint the buffered image to the graphics 
     g.drawImage(imageBuffer, 0, 0, component); 

     // resume the game loop 
     resume(); 
    } 

    public static class MyComponent extends JPanel { 

     private final MyGame game; 

     public MyComponent(MyGame game) { 
      this.game = game; 
     } 

     @Override 
     protected void paintComponent(Graphics g) { 
      game.paint(g); 
     } 
    } 

    public static void main(String[] args) { 
     java.awt.EventQueue.invokeLater(new Runnable() { 
      @Override 
      public void run() { 
       MyGame game = new MyGame(); 
       MyComponent component = new MyComponent(game); 

       JFrame frame = new JFrame(); 
       // put the component in a frame 

       frame.setTitle("Demo"); 
       frame.setSize(800, 600); 

       frame.setLayout(new BorderLayout()); 
       frame.getContentPane().add(component, BorderLayout.CENTER); 
       frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
       frame.setVisible(true); 

       game.start(component); 
      } 
     }); 
    } 

} 
+0

Ich denke, jede Spielschleife, die [Thread.Sleep] (https://docs.oracle.com/javase/7/docs/api/java/lang/Thread.html#sleep%28long,%20int%29) verwendet, ist fehlerhaft . Aus der Doku: 'Bewirkt, dass der aktuell ausführende Thread für die angegebene [...] Anzahl von Millisekunden plus die angegebene Anzahl von Nanosekunden inaktiviert wird, ** vorbehaltlich der Genauigkeit und Genauigkeit von Systemzeitgebern und -zeitplanern **.'. Es gibt keine Garantie, dass der Thread tatsächlich für die gewünschte Anzahl von Millisekunden + Nanos schläft. –

+0

Ich stimme tatsächlich zu, dass die Verwendung von 'Thread.sleep' nicht ideal ist. Non-Blocking-Techniken wären schöner.Ich bin mir nicht sicher, ob Sie es besser machen können, dass die Genauigkeit 'Thread.sleep' mit einer nicht-blockierenden Technik anbietet, obwohl Sie' 'ScheduletedExecutorService'' verwenden. Was denken Sie? –

+0

IMO die beste ist eine Schleife, die der von Parth Mehrotra gepostet wird, dh eine, die nicht auf schlafen oder etwas Ähnliches angewiesen ist. –

0

Es gibt eine Menge guter Informationen hier schon, aber ich wollte ein Problem teilen das hatte ich mit diesen Lösungen und der Lösung dazu. Wenn Ihr Spiel/Fenster trotz all dieser Lösungen zu überspringen scheint (besonders unter X11), versuchen Sie einmal pro Bild Toolkit.getDefaultToolkit().sync() anzurufen.

Verwandte Themen