2009-04-21 10 views
13

Я пишу простую игру, и я хочу ограничить скорость кадров на 60 кадров в секунду, не заставляя цикл съедать мой процессор. Как мне это сделать?Как ограничить частоту кадров в 60 кадров в секунду на Java?

+4

Не жестко кодируйте 60 FPS, определите фактическую частоту обновления дисплея. 60 FPS будут рывками напр. монитор 85 Гц. –

ответ

32

Вы можете прочитать Game Loop Article. Очень важно, чтобы вы сначала поняли разные методологии для игрового цикла, прежде чем пытаться реализовать что-либо.

+0

Простая для понимания статья. – mfx

5

Простым ответом является установка java.util.Timer для запуска каждые 17 мс и выполнения вашей работы в событии таймера.

+1

Что делать, если для завершения работы в событии таймера требуется более 17 мс? – cherouvim

+0

@cherouvim: Затем вы перегружены и ваш кадр падает. Вы по-прежнему выполняете работу как можно быстрее, но не быстрее 60 кадров в секунду. –

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 где-нибудь.

+1

В Java эквивалент glfwGetTime() будет System.currentTimeMillis(). Он возвращает время в миллисекундах с 1 января 1970 года. При использовании подобным образом он делает примерно одно и то же, за исключением того, что возвращает время в миллисекундах. – Manitobahhh

0

В java вы можете сделать System.currentTimeMillis(), чтобы получить время в миллисекундах вместо glfwGetTime().

Thread.sleep(time in milliseconds) заставляет поток ждать на случай, если вы не знаете, и он должен находиться в блоке try.

0

То, что я сделал, это просто продолжить цикл и отслеживать, когда я последний раз делал анимацию. Если он прошел не менее 17 мс, то выполните последовательность анимации.

Таким образом, я мог проверять любые пользовательские входы и включать или отключать музыкальные ноты по мере необходимости.

Но это было в программе, помогающей преподавать музыку детям, и мое приложение заставляло компьютер зависеть от того, что он был полноэкранным, поэтому он не хорошо играл с другими.

0

Если вы используете 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 кадров в секунду. Благодаря интеллектуальному предопределению памяти, повторному использованию объекта и другим вещам вы также можете уменьшить влияние сборщика мусора. Но это по другому вопросу.

+0

Проблема с таймерами сопоставима с проблемой использования Thread.Sleep ... нет гарантии, что задержка является точной. Обычно фактическая DELAY непредсказуема и, возможно, будет немного длиннее, чем вы передали как параметр DELAY. –

1

Таймер является неточным для управления FPS в java. Я нашел это из вторых рук. Вам нужно будет реализовать свой собственный таймер или сделать FPS в реальном времени с ограничениями. Но не используйте Таймер, поскольку он не на 100% точнее и поэтому не может выполнять задачи должным образом.

7

Я взял 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); 
    } 
} 
+0

Это скелет лучшего способа сделать игровой цикл, но он не объясняет, что это так? Может быть, добавить несколько коротких пунктов, объясняющих вещи? –

+0

Это как я это делаю. Итак, мы зацикливаемся, и если время между итерациями цикла меньше, чем пропустить, мы просто снова зацикливаемся. Если пропуск или больше времени прошло, мы вызываем основной движок игры и запрашиваем следующий кадр. Мне нравится тянуть весь кадр как изображение и отправлять его методу рисования. В моем классе игры мне нравится вычислять максимальный кадр в секунду, если игрок хочет показать фактический FPS, который, когда я устанавливаю max fps, фактический всегда на несколько кадров меньше заданного количества, т.е. установлен на 60 действительных 57. –

2

Вот подсказка для использования свинг и получать достаточно точную 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); 
      } 
     }); 
    } 

} 
+0

Я думаю, что любой игровой цикл, который использует [Thread.Sleep] (https://docs.oracle.com/javase/7/docs/api/java/lang/Thread.html#sleep%28long,%20int%29), ошибочен , Из документа: 'Заставляет текущий исполняемый поток спать [...] за указанное количество миллисекунд плюс указанное количество наносекунд **, с учетом точности и точности системных таймеров и планировщиков **.'. Нет никакой гарантии, что поток действительно будет спать за миллисекунды + нано, которые вы просите. –

+0

Я согласен, что использование 'Thread.sleep' не является идеальным. Неблокирующие методы были бы лучше.Я не уверен, что вы можете сделать что-то лучше, чем точность 'Thread.sleep' предлагает с неблокирующей техникой, хотя используется« ScheduledExecutorService ». Как вы думаете? –

+0

ИМО - лучшая петля, подобная той, что была опубликована Партом Мехротрой, то есть тем, что не полагается на сон или что-то подобное. –

0

Там есть много хорошей информации уже здесь, но я хотел бы поделиться одной проблемой что у меня были с этими решениями, и решение этого. Если, несмотря на все эти решения, ваша игра/окно, кажется, пропускает (особенно в X11), попробуйте позвонить Toolkit.getDefaultToolkit().sync() один раз за кадр.