如何在Java中以60 fps限制帧速率?

我正在写一个简单的游戏,我想以60 fps的速度限制我的帧速率,而不会让循环吃掉我的cpu。 我该怎么办?

你可以阅读游戏循环文章 。 在尝试实现任何内容之前,首先要了解游戏循环的不同方法非常重要。

我拿了@cherouvim发布的游戏循环文章 ,我采取了“最佳”策略并试图为java Runnable重写它,似乎对我有用

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); } } 

简单的答案是将java.util.Timer设置为每17毫秒触发一次,并在计时器事件中完成工作。

这是我在C ++中的表现…我相信你可以适应它。

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

每个循环只需用capFrameRate(60)调用一次。 它会睡觉,所以它不会浪费宝贵的周期。 glfwGetTime()返回自程序启动以来的时间......我确信你可以在某处找到Java中的等价物。

这里有一个使用Swing并在不使用Timer的情况下获得相当准确的FPS的提示。

首先不要在事件调度程序线程中运行游戏循环,它应该在自己的线程中运行,并且不会妨碍UI。

显示游戏的Swing组件应该自行覆盖此方法:

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

问题是游戏循环不知道事件调度程序线程何时实际执行了绘制操作,因为在游戏循环中我们只调用component.repaint(),它仅请求可能在以后发生的绘制。

使用wait / notify,您可以使重绘方法等到请求的绘制发生,然后继续。

这是来自https://github.com/davidmoten/game-exploration的完整工作代码,它使用上述方法在JFrame上简单地移动字符串:

 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); } }); } } 

计时器在java中控制FPS是不准确的。 我发现了第二只手。 您需要实现自己的计时器或执行有限制的实时FPS。 但是不要使用Timer,因为它不是100%准确,因此无法正确执行任务。

我所做的就是继续循环,并跟踪我上次制作动画的时间。 如果它已经至少17毫秒,那么通过动画序列。

这样我可以检查任何用户输入,并根据需要打开/关闭音符。

但是,这是一个帮助孩子们教音乐的程序,而我的应用程序正在占用计算机,因为它是全屏的,所以它与其他人不能很好地兼容。

如果您使用的是Java Swing,那么实现60 fps的最简单方法就是设置一个像这样的javax.swing.Timer,这是制作游戏的常用方法:

 public static int DELAY = 1000/60; Timer timer = new Timer(DELAY,new ActionListener() { public void actionPerformed(ActionEvent event) { updateModel(); repaintScreen(); } }); 

在代码中的某处,您必须将此计时器设置为重复并启动它:

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

每秒都有1000毫秒,通过将其除以fps(60)并设置具有此延迟的定时器(1000/60 = 16毫秒向下舍入),您将获得一定程度的固定帧速率。 我说“有点”因为这在很大程度上取决于你在上面的updateModel()和repaintScreen()调用中所做的事情。

要获得更可预测的结果,请尝试计时器中的两个调用时间,并确保它们在16 ms内完成以维持60 fps。 通过智能预分配内存,对象重用等function,您还可以减少垃圾收集器的影响。 但这是另一个问题。

在java中,您可以使用System.currentTimeMillis()来获取以毫秒为单位的时间而不是glfwGetTime()

Thread.sleep(time in milliseconds)使线程等待你不知道,并且它必须在try块内。

这里有很多好的信息,但我想分享一下我对这些解决方案的一个问题,以及解决方案。 如果,尽管所有这些解决方案,您的游戏/窗口似乎跳过(特别是在X11下),尝试每帧调用Toolkit.getDefaultToolkit().sync()一次。