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

时间:2009-04-21 06:00:37

标签: java cpu frame-rate

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

10 个答案:

答案 0 :(得分:33)

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

答案 1 :(得分:10)

我接受了@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);
    }
}

答案 2 :(得分:5)

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

答案 3 :(得分: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中的等价物。

答案 4 :(得分:3)

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

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

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

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

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

使用wait / notify可以让redraw方法等到请求的paint已经发生,然后继续。

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

}

答案 5 :(得分:2)

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

答案 6 :(得分:0)

在java中,你可以System.currentTimeMillis()以毫秒而不是glfwGetTime()来获取时间。

Thread.sleep(time in milliseconds)使线程等待以防您不知道,并且它必须位于try块内。

答案 7 :(得分:0)

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

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

但是,这是一个帮助孩子们教音乐的程序,我的应用程序正在计算机上,因为它是全屏的,所以它与其他人不一致。

答案 8 :(得分:0)

如果你使用的是Java Swing,最简单的方法就是设置一个像这样的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。通过智能预分配内存,对象重用等功能,您还可以减少垃圾收集器的影响。但这是另一个问题。

答案 9 :(得分:0)

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