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

在Java中如何将帧速率限制为60 fps?,java,cpu,frame-rate,Java,Cpu,Frame Rate,我正在写一个简单的游戏,我想在不让循环占用cpu的情况下将帧速率限制在60 fps。我该怎么做呢?您可以阅读。重要的是,你首先了解游戏循环的不同方法,然后尝试实现任何东西。 < P>简单的答案是设置JavaUTIL定时器每17毫秒开机,并在定时器事件中执行你的工作。< /P> < P>这里我是如何在C++中完成的…我相信你能适应它 void capFrameRate(double fps) { static double start = 0, diff, wait; wait =

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

您可以阅读。重要的是,你首先了解游戏循环的不同方法,然后尝试实现任何东西。

< P>简单的答案是设置JavaUTIL定时器每17毫秒开机,并在定时器事件中执行你的工作。< /P> < P>这里我是如何在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();
}
void capFrameRate(双帧){
静态双启动=0,差异,等待;
等待=1/fps;
diff=glfwGetTime()-开始;
如果(差异<等待){
glfwSleep(等待-差异);
}
start=glfwGetTime();
}

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

在Java中,你可以使用
System.currentTimeMillis()
以毫秒为单位获取时间,而不是
glfGetTime()


Thread.sleep(以毫秒为单位)
使线程等待,以防您不知道,并且它必须在
try
块内。

我所做的只是继续循环,并跟踪我上次制作动画的时间。如果至少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()和repaitscreen()调用中所做的操作


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

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

我采用了@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);
    }
}
双插值=0;
最终整数滴答每秒=25;
最终整数跳过刻度=每秒1000个/刻度;
最终int MAX_FRAMESKIP=5;
@凌驾
公开募捐{
double next_game_tick=System.currentTimeMillis();
int循环;
while(true){
循环=0;
while(System.currentTimeMillis()>下一个游戏
&&循环<最大帧跳过){
更新_game();
下一个_游戏_滴答+=跳过_滴答;
循环++;
}
插值=(System.currentTimeMillis()+跳过刻度-下一个刻度
/(双)跳过标记);
显示游戏(插值);
}
}

这里有一个使用Swing的技巧,可以在不使用计时器的情况下获得相当准确的FPS

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

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

@Override
public void paintComponent(Graphics g){
   // draw stuff
}
问题是,游戏循环不知道事件调度程序线程何时真正开始执行绘制操作,因为在游戏循环中,我们只调用component.repaint(),它只请求稍后可能发生的绘制

使用wait/notify可以让重画方法等待请求的绘制完成,然后继续

下面是完整的工作代码,它使用上述方法在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);
            }
        });
    }

}

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

如果计时器事件中的工作完成时间超过17ms,该怎么办?@cherouvim:那么您会过载,帧速率会下降。您仍在以尽可能快的速度工作,但速度不得超过60 fps。不要硬编码60 fps,请确定显示器的实际刷新率。例如,在85赫兹的显示器上,每秒60帧的速度将是不稳定的。我认为任何使用的游戏循环都是有缺陷的。从docu:
使当前执行的线程睡眠[…]指定的毫秒数加上指定的纳秒数,**取决于系统计时器和调度程序的精度和准确性**。
。无法保证线程会在您要求的毫秒数+毫微秒数内睡眠。我同意实际上使用
thread.sleep
并不理想。非阻塞技术会更好。我不确定你能做得比使用
ScheduledExecutorService
say的准确性
Thread.sleep
提供的非阻塞技术更好。你认为呢?我认为最好的是一个类似于Parth Mehrotra发布的循环,即不依赖睡眠或类似的循环。是的,可能更准确,但你需要一个处理器。取决于