2009-04-21 4 views
13

Sto scrivendo un gioco semplice, e voglio coprire il mio framerate a 60 fps senza che il loop mangi la mia cpu. Come lo farei?Come posso limitare il framerate a 60 fps in Java?

+4

Non hardcode 60 FPS, determinare la frequenza di aggiornamento effettiva del display. 60 FPS saranno a scatti ad es. un monitor a 85 Hz. –

risposta

32

È possibile leggere il numero Game Loop Article. È molto importante prima capire le diverse metodologie per il ciclo di gioco prima di provare a implementare qualsiasi cosa.

+0

Articolo di facile comprensione. – mfx

5

La semplice risposta è impostare un java.util.Timer per sparare ogni 17 ms e svolgere il proprio lavoro nell'evento timer.

+1

Cosa succede se sono necessari più di 17 ms per completare il lavoro nell'evento timer? – cherouvim

+0

@cherouvim: Allora sei sovraccarico e il framerate cade. Stai ancora lavorando il più velocemente possibile, ma non più veloce di 60 fps. –

4

Ecco come l'ho fatto in C++ ... sono sicuro che è possibile adattarlo.

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

Basta chiamarlo con capFrameRate(60) una volta per ciclo. Dormirà, quindi non spreca cicli preziosi. glfwGetTime() restituisce il tempo da quando il programma è iniziato in secondi ... Sono sicuro che puoi trovare un equivalente in Java da qualche parte.

+1

In Java, l'equivalente di glfwGetTime() sarebbe System.currentTimeMillis(). Restituisce il tempo in millisecondi dal 1 ° gennaio 1970. Se usato in questo modo, fa la stessa cosa tranne che restituisce il tempo in millisecondi. – Manitobahhh

0

In Java è possibile eseguire System.currentTimeMillis() per ottenere il tempo in millisecondi anziché glfwGetTime().

Thread.sleep(time in milliseconds) rende il thread in attesa nel caso non lo si sappia e deve trovarsi all'interno di un blocco try.

0

Quello che ho fatto è solo continuare a scorrere e tenere traccia di quando ho fatto un'animazione l'ultima volta. Se è stato almeno 17 ms, passare alla sequenza di animazione.

In questo modo è possibile verificare gli input dell'utente e attivare/disattivare le note musicali secondo necessità.

Ma, questo era in un programma per insegnare musica ai bambini e la mia applicazione stava accarezzando il computer in quanto era a schermo intero, quindi non ha funzionato bene con gli altri.

0

Se si sta utilizzando Java Swing il modo più semplice per ottenere un 60 fps è attraverso la creazione di un javax.swing.Timer come questo ed è un modo comune per fare giochi:

public static int DELAY = 1000/60; 

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

E da qualche parte nella tua il codice è necessario impostare il timer per ripetere e avviarlo:

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

Ogni secondo ha 1000 ms e dividendo questo con il vostro fps (60) e l'impostazione del timer con questo ritardo (1000/60 = 16 ms arrotondato in basso) si otterrà un framerate alquanto fisso. Dico "un po '" perché questo dipende molto da ciò che fai nelle chiamate updateModel() e repaintScreen() sopra.

Per ottenere risultati più prevedibili, provare a cronometrare le due chiamate nel timer e assicurarsi che terminino entro 16 ms per mantenere 60 fps. Con la preallocazione intelligente della memoria, il riutilizzo degli oggetti e altre cose è possibile ridurre anche l'impatto del Garbage Collector. Ma questo è per un'altra domanda.

+0

Il problema con i timer è paragonabile a quello dell'utilizzo di Thread.Sleep ... non vi è alcuna garanzia che il ritardo sia esatto. Generalmente il DELAY effettivo è imprevedibile e potrebbe essere leggermente più lungo di quello che hai passato come parametro DELAY. –

1

Il timer non è accurato per il controllo di FPS in java. L'ho scoperto di seconda mano. È necessario implementare il proprio timer o eseguire FPS in tempo reale con limitazioni. Ma non usare Timer perché non è accurato al 100% e quindi non può eseguire correttamente le attività.

7

Ho preso il Game Loop Article che @cherouvim pubblicato, ed io abbiamo preso la strategia "Best" e tentò di riscrivere per un java Runnable, sembra funzionare per me

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

È lo scheletro del modo migliore di fare un ciclo di gioco, ma non spiega molto lo fa? Forse aggiungere alcuni brevi punti elenco che spiegano le cose? –

+0

Questo è un po 'come lo faccio io. Ok, quindi eseguiamo un ciclo, e se il tempo passato tra le iterazioni del ciclo è inferiore a quello di salto, eseguiamo nuovamente il ciclo. Se salta o è trascorso più tempo, chiamiamo il motore di gioco principale e chiediamo il frame successivo. Mi piace tirare tutto il frame come immagine e inviarlo al metodo paint. Nella mia classe di gioco, mi piace calcolare il massimo frame al secondo nel caso in cui il giocatore voglia mostrare FPS effettivi, che quando imposto il max fps, il reale è sempre qualche fotogramma inferiore all'importo impostato, cioè impostato su 60 effettivi è 57. –

2

Ecco un suggerimento per l'utilizzo di swing e ottenere FPS ragionevolmente accurati senza usare un timer.

In primo luogo, non eseguire il ciclo di gioco nel thread del dispatcher dell'evento, dovrebbe essere eseguito nel proprio thread facendo il duro lavoro e non intralciando l'interfaccia utente.

Il componente Swing che visualizza il gioco dovrebbe trarre in sé l'override di questo metodo:

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

Il problema è che il ciclo di gioco non sa quando il thread di evento dispatcher ha effettivamente avuto intorno a svolgere l'azione di vernice perché nel ciclo di gioco chiamiamo semplicemente component.repaint() che richiede solo una vernice che potrebbe accadere in seguito.

Utilizzando wait/notify è possibile ottenere il metodo di aggiornamento per attendere fino a quando non è avvenuta la vernice richiesta e quindi continuare.

Ecco codice di lavoro completo dalla https://github.com/davidmoten/game-exploration che fa un semplice movimento di una stringa in un JFrame utilizzando il metodo di cui sopra:

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

Penso che ogni loop di gioco che utilizza [Thread.Sleep] (https://docs.oracle.com/javase/7/docs/api/java/lang/Thread.html#sleep%28long,%20int%29) sia difettoso . Dal docu: 'Fa in modo che il thread attualmente in esecuzione [...] dorme per il numero specificato di millisecondi più il numero specificato di nanosecondi, ** soggetto alla precisione e all'accuratezza dei timer di sistema e degli schedulatori **.'. Non vi è alcuna garanzia che il thread in realtà dorma per il numero di millisecondi + nanos che si richiedono. –

+0

Concordo sul fatto che l'uso di 'Thread.sleep' non è l'ideale. Le tecniche di non blocco sarebbero più belle.Non sono sicuro che tu possa fare di meglio che l'accuratezza 'Thread.sleep' offra una tecnica non bloccante usando però un comando 'ScheduledExecutorService'. Cosa ne pensi? –

+0

IMO il migliore è un loop simile a quello pubblicato da Parth Mehrotra, cioè uno che non si basa sul sonno o qualcosa di simile. –

0

Ci sono un sacco di buone informazioni qui già, ma ho voluto condividere un problema che ho avuto con queste soluzioni e la soluzione a questo. Se, nonostante tutte queste soluzioni, il tuo gioco/finestra sembra saltare (specialmente sotto X11), prova a chiamare Toolkit.getDefaultToolkit().sync() una volta per frame.