我正在编写一个简单的游戏,希望将帧率限制在60fps,同时不让循环占用过多CPU资源。有什么方法可以实现这一点?
我正在编写一个简单的游戏,希望将帧率限制在60fps,同时不让循环占用过多CPU资源。有什么方法可以实现这一点?
您可以阅读游戏循环文章。在尝试实现任何内容之前,首先要了解游戏循环的不同方法非常重要。
我参考了@cherouvim发布的游戏循环文章,采用了“最佳”策略并尝试将其重写为Java可运行代码,目前看来对我有效。
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);
}
}
以下是我在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中找到相应的函数。
@Override
public void paintComponent(Graphics g){
// draw stuff
}
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);
}
});
}
}
使当前正在执行的线程休眠[...]指定的毫秒数加上指定的纳秒数,**取决于系统计时器和调度程序的精度和准确性**。
。不能保证线程实际上会休眠你要求的毫秒数+纳秒数。 - TT.Thread.sleep
不是最理想的做法。非阻塞技术会更好。但我不确定你是否能够通过使用 ScheduledExecutorService
等非阻塞技术来达到 Thread.sleep
的准确度。你怎么看? - Dave Moten这里已经有很多好的信息了,但我想分享一个问题以及解决方案。如果尽管尝试了所有这些解决方案,你的游戏/窗口似乎会跳过(特别是在X11下),请尝试每帧调用一次Toolkit.getDefaultToolkit().sync()
。
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。通过智能预分配内存、对象重用和其他方法,您还可以减少垃圾收集器的影响。但那是另一个问题。
System.currentTimeMillis()
而不是glfwGetTime()
来获取以毫秒为单位的时间。
Thread.sleep(毫秒数)
会使线程等待,如果您不知道,它必须在try
块内。