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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/771206/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 19:17:36  来源:igfitidea点击:

How do I cap my framerate at 60 fps in Java?

javacpuframe-rate

提问by William

I am writting a simple game, and I want to cap my framerate at 60 fps without making the loop eat my cpu. How would I do this?

我正在编写一个简单的游戏,我想将我的帧速率限制在 60 fps 而不让循环消耗我的 CPU。我该怎么做?

回答by cherouvim

You can read the Game Loop Article. It's very important that you first understand the different methodologies for the game loop before trying to implement anything.

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

回答by Lawrence Dol

The simple answer is to set a java.util.Timer to fire every 17 ms and do your work in the timer event.

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

回答by mpen

Here's how I did it in C++... I'm sure you can adapt it.

这是我在 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();
}

Just call it with capFrameRate(60)once per loop. It will sleep, so it doesn't waste precious cycles. glfwGetTime()returns the time since the program started in seconds... I'm sure you can find an equivalent in Java somewhere.

capFrameRate(60)每个循环只需调用一次。它会休眠,因此不会浪费宝贵的周期。glfwGetTime()以秒为单位返回自程序启动以来的时间......我相信你可以在某个地方找到 Java 中的等价物。

回答by Anonymous

In java you could do System.currentTimeMillis()to get the time in milliseconds instead of glfwGetTime().

在 java 中,您可以System.currentTimeMillis()以毫秒为单位而不是glfwGetTime().

Thread.sleep(time in milliseconds)makes the thread wait in case you don't know, and it must be within a tryblock.

Thread.sleep(time in milliseconds)在您不知道的情况下使线程等待,并且它必须在一个try块内。

回答by James Black

What I have done is to just continue to loop through, and keep track of when I last did an animation. If it has been at least 17 ms then go through the animation sequence.

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

This way I could check for any user inputs, and turn musical notes on/off as needed.

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

But, this was in a program to help teach music to children and my application was hogging up the computer in that it was fullscreen, so it didn't play well with others.

但是,这是在一个帮助教儿童音乐的程序中,我的应用程序占用了计算机,因为它是全屏的,所以它不能很好地与其他人一起玩。

回答by Johncl

If you are using Java Swing the simplest way to achieve a 60 fps is by setting up a javax.swing.Timer like this and is a common way to make games:

如果您正在使用 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();
    }
});

And somewhere in your code you must set this timer to repeat and start it:

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

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

Each second has 1000 ms and by dividing this with your fps (60) and setting up the timer with this delay (1000/60 = 16 ms rounded down) you will get a somewhat fixed framerate. I say "somewhat" because this depends heavily on what you do in the updateModel() and repaintScreen() calls above.

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

To get more predictable results, try to time the two calls in the timer and make sure they finish within 16 ms to uphold 60 fps. With smart preallocation of memory, object reuse and other things you can reduce the impact of the Garbage Collector also. But that is for another question.

要获得更可预测的结果,请尝试在计时器中对两次调用进行计时,并确保它们在 16 毫秒内完成以保持 60 fps。通过智能预分配内存、对象重用和其他功能,您还可以减少垃圾收集器的影响。但那是另一个问题。

回答by abc123

Timer is inaccurate for controlling FPS in java. I have found that out second hand. You will need to implement your own timer or do real time FPS with limitations. But do not use Timer as it's not 100% accurate, and therefore cannot execute tasks properly.

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

回答by Parth Mehrotra

I took the Game Loop Articlethat @cherouvim posted, and I took the "Best" strategy and attempted to rewrite it for a java Runnable, seems to be working for me

我采用了@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);
    }
}

回答by Dave Moten

Here's a tip for using Swing and getting reasonably accurate FPS without using a Timer.

这是使用 Swing 并在不使用计时器的情况下获得相当准确的 FPS 的提示。

Firstly don't run the game loop in the event dispatcher thread, it should run in its own thread doing the hard work and not getting in the way of the UI.

首先不要在事件调度程序线程中运行游戏循环,它应该在自己的线程中运行,完成艰苦的工作,而不是妨碍 UI。

The Swing component displaying the game should draw itself overriding this method:

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

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

The catch is that the game loop doesn't know when the event dispatcher thread has actually got round to performing the paint action because in the game loop we just call component.repaint() which requests a paint only that may happen later.

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

Using wait/notify you can get the redraw method to wait until the requested paint has happened and then continue.

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

Here's complete working code from https://github.com/davidmoten/game-explorationthat does a simple movement of a string across a JFrame using the method above:

这是来自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);
            }
        });
    }

}

回答by zrneely

There is a lot of good information here already, but I wanted to share one problem that I had with these solutions, and the solution to that. If, despite all these solutions, your game/window seems to skip (especially under X11), try calling Toolkit.getDefaultToolkit().sync()once per frame.

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