【问题标题】:How do I cap my framerate at 60 fps in Java?如何在 Java 中将帧速率限制在 60 fps?
【发布时间】:2009-04-21 06:00:36
【问题描述】:

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

【问题讨论】:

  • 不要硬编码 60 FPS,确定显示器的实际刷新率。例如,60 FPS 将是生涩的。一个 85 赫兹的显示器。

标签: java cpu frame-rate


【解决方案1】:

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

【讨论】:

【解决方案2】:

我采用了@cherouvim 发布的Game Loop Article,并采用了“最佳”策略并尝试将其重写为 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);
    }
}

【讨论】:

  • 它是更好的游戏循环方式的骨架,但它并没有解释太多,不是吗?也许添加一些简短的要点来解释事情?
  • 这有点像我的做法。好的,所以我们循环,如果循环迭代之间过去的时间小于跳过,我们就再次循环。如果跳过或超过时间,我们调用主游戏引擎并请求下一帧。我喜欢将整个帧拉为图像并将其发送到绘画方法。在我的游戏课程中,我喜欢计算每秒最大帧数,以防玩家想要显示实际 FPS,当我设置最大 fps 时,实际总是比设置量少几帧,即设置为 60 实际为 57。
【解决方案3】:

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

【讨论】:

  • 如果在定时器事件中完成工作超过17ms怎么办?
  • @cherouvim:然后你就超载了,你的帧率下降了。您仍在以尽可能快的速度工作,但速度不会超过 60 fps。
【解决方案4】:

这就是我在 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 中的某个地方找到等价物。

【讨论】:

  • 在 Java 中,glfwGetTime() 的等价物是 System.currentTimeMillis()。它以毫秒为单位返回自 1970 年 1 月 1 日以来的时间。像这样使用时,它的作用大致相同,只是它以毫秒为单位返回时间。
【解决方案5】:

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

首先不要在事件调度器线程中运行游戏循环,它应该在自己的线程中运行,完成繁重的工作并且不妨碍 UI。

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

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

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

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

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

}

【讨论】:

  • 我认为任何使用Thread.Sleep 的游戏循环都是有缺陷的。来自文档:Causes the currently executing thread to sleep [...] for the specified number of milliseconds plus the specified number of nanoseconds, **subject to the precision and accuracy of system timers and schedulers**.。无法保证线程实际上会在您要求的毫秒数+纳秒内休眠。
  • 我实际上同意使用Thread.sleep 并不理想。非阻塞技术会更好。尽管使用ScheduledExecutorService 说,我不确定您是否可以通过非阻塞技术提供Thread.sleep 的准确性。你怎么看?
  • IMO 最好的循环是类似于 Parth Mehrotra 发布的循环,即不依赖睡眠或类似的循环。
  • 是的,可能会更准确,但您需要专用处理器。取决于我想的游戏。
  • 确实如此。我必须承认我从未开发过游戏,所以我不完全确定最好的方法是什么。我只是认为依赖Thread.Sleep 可能是不确定的。
【解决方案6】:

Timer 对于控制 Java 中的 FPS 不准确。我已经找到了二手货。您将需要实现自己的计时器或进行有限制的实时 FPS。但不要使用 Timer,因为它不是 100% 准确的,因此无法正确执行任务。

【讨论】:

    【解决方案7】:

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

    【讨论】:

    • 谢谢 我遇到了很多丢帧的问题(渲染但未显示),这解决了问题。
    【解决方案8】:

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

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

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

    【讨论】:

      【解决方案9】:

      如果您使用 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 毫秒内完成以保持 60 fps。通过内存的智能预分配、对象重用和其他事情,您还可以减少垃圾收集器的影响。但这是另一个问题。

      【讨论】:

      • 定时器的问题与使用 Thread.Sleep 的问题相当......不能保证延迟是准确的。一般来说,实际的 DELAY 是不可预测的,可能会比您作为 DELAY 参数传递的稍长。
      【解决方案10】:

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2020-04-07
        • 1970-01-01
        • 1970-01-01
        • 2019-05-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多