Я пишу простую игру, и я хочу ограничить скорость кадров на 60 кадров в секунду, не заставляя цикл съедать мой процессор. Как мне это сделать?Как ограничить частоту кадров в 60 кадров в секунду на Java?
ответ
Вы можете прочитать Game Loop Article. Очень важно, чтобы вы сначала поняли разные методологии для игрового цикла, прежде чем пытаться реализовать что-либо.
Простая для понимания статья. – mfx
Простым ответом является установка java.util.Timer для запуска каждые 17 мс и выполнения вашей работы в событии таймера.
Что делать, если для завершения работы в событии таймера требуется более 17 мс? – cherouvim
@cherouvim: Затем вы перегружены и ваш кадр падает. Вы по-прежнему выполняете работу как можно быстрее, но не быстрее 60 кадров в секунду. –
Вот как я это сделал на 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(). Он возвращает время в миллисекундах с 1 января 1970 года. При использовании подобным образом он делает примерно одно и то же, за исключением того, что возвращает время в миллисекундах. – Manitobahhh
В java вы можете сделать System.currentTimeMillis()
, чтобы получить время в миллисекундах вместо glfwGetTime()
.
Thread.sleep(time in milliseconds)
заставляет поток ждать на случай, если вы не знаете, и он должен находиться в блоке try
.
То, что я сделал, это просто продолжить цикл и отслеживать, когда я последний раз делал анимацию. Если он прошел не менее 17 мс, то выполните последовательность анимации.
Таким образом, я мог проверять любые пользовательские входы и включать или отключать музыкальные ноты по мере необходимости.
Но это было в программе, помогающей преподавать музыку детям, и мое приложение заставляло компьютер зависеть от того, что он был полноэкранным, поэтому он не хорошо играл с другими.
Если вы используете Java Swing, самый простой способ для достижения 60 кадров в секунду является создание 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 мс и разделив это с кадров в секунду (60) и настройка таймера с этой задержкой (округленный 1000/60 = 16 мс вниз) вы получите несколько фиксированную частоту кадров. Я говорю «несколько», потому что это сильно зависит от того, что вы делаете в вызовах updateModel() и repaintScreen() выше.
Чтобы получить более предсказуемые результаты, попробуйте время двух вызовов таймера и убедитесь, что они закончили в течение 16 мс, чтобы поддерживать 60 кадров в секунду. Благодаря интеллектуальному предопределению памяти, повторному использованию объекта и другим вещам вы также можете уменьшить влияние сборщика мусора. Но это по другому вопросу.
Проблема с таймерами сопоставима с проблемой использования Thread.Sleep ... нет гарантии, что задержка является точной. Обычно фактическая DELAY непредсказуема и, возможно, будет немного длиннее, чем вы передали как параметр DELAY. –
Таймер является неточным для управления FPS в java. Я нашел это из вторых рук. Вам нужно будет реализовать свой собственный таймер или сделать FPS в реальном времени с ограничениями. Но не используйте Таймер, поскольку он не на 100% точнее и поэтому не может выполнять задачи должным образом.
Я взял Game Loop Article что @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);
}
}
Это скелет лучшего способа сделать игровой цикл, но он не объясняет, что это так? Может быть, добавить несколько коротких пунктов, объясняющих вещи? –
Это как я это делаю. Итак, мы зацикливаемся, и если время между итерациями цикла меньше, чем пропустить, мы просто снова зацикливаемся. Если пропуск или больше времени прошло, мы вызываем основной движок игры и запрашиваем следующий кадр. Мне нравится тянуть весь кадр как изображение и отправлять его методу рисования. В моем классе игры мне нравится вычислять максимальный кадр в секунду, если игрок хочет показать фактический FPS, который, когда я устанавливаю max fps, фактический всегда на несколько кадров меньше заданного количества, т.е. установлен на 60 действительных 57. –
Вот подсказка для использования свинг и получать достаточно точную FPS без использования таймера.
Во-первых, не запускайте игровой цикл в потоке диспетчера событий, он должен работать в своем потоке, выполняя тяжелую работу и не мешая интерфейсу.
Компонент Качели отображение игры должен сделать сам перекрывая этот метод:
@Override
public void paintComponent(Graphics g){
// draw stuff
}
Загвоздка в том, что игровой цикл не знает, когда диспетчер события нить фактически обходила к выполнению действия краски, потому что в игровом цикле мы просто вызываем component.repaint(), который запрашивает только краску, которая может произойти позже.
Используя wait/notify, вы можете получить метод перерисовывания, чтобы подождать, пока запрошенная краска не произойдет, а затем продолжите.
Вот полный рабочий код из 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] (https://docs.oracle.com/javase/7/docs/api/java/lang/Thread.html#sleep%28long,%20int%29), ошибочен , Из документа: 'Заставляет текущий исполняемый поток спать [...] за указанное количество миллисекунд плюс указанное количество наносекунд **, с учетом точности и точности системных таймеров и планировщиков **.'. Нет никакой гарантии, что поток действительно будет спать за миллисекунды + нано, которые вы просите. –
Я согласен, что использование 'Thread.sleep' не является идеальным. Неблокирующие методы были бы лучше.Я не уверен, что вы можете сделать что-то лучше, чем точность 'Thread.sleep' предлагает с неблокирующей техникой, хотя используется« ScheduledExecutorService ». Как вы думаете? –
ИМО - лучшая петля, подобная той, что была опубликована Партом Мехротрой, то есть тем, что не полагается на сон или что-то подобное. –
Там есть много хорошей информации уже здесь, но я хотел бы поделиться одной проблемой что у меня были с этими решениями, и решение этого. Если, несмотря на все эти решения, ваша игра/окно, кажется, пропускает (особенно в X11), попробуйте позвонить Toolkit.getDefaultToolkit().sync()
один раз за кадр.
Не жестко кодируйте 60 FPS, определите фактическую частоту обновления дисплея. 60 FPS будут рывками напр. монитор 85 Гц. –