Как я могу ограничить мою частоту кадров на 60 кадров в секунду в Java? - PullRequest
14 голосов
/ 21 апреля 2009

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

Ответы [ 10 ]

33 голосов
/ 21 апреля 2009

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

10 голосов
/ 20 января 2013

Я взял статью Game Loop , которую разместил @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);
    }
}
5 голосов
/ 21 апреля 2009

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

4 голосов
/ 21 апреля 2009

Вот как я это сделал в 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 можно найти эквивалент.

3 голосов
/ 07 сентября 2013

Вот совет по использованию Swing и получению достаточно точного FPS без использования таймера.

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

Компонент Swing, отображающий игру, должен отрисовывать себя, переопределяя этот метод:

@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);
            }
        });
    }

}
2 голосов
/ 19 апреля 2011

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

0 голосов
/ 25 марта 2015

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

0 голосов
/ 31 августа 2010

Если вы используете 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 мс, и, разделив это на свой fps (60) и установив таймер с этой задержкой (1000/60 = 16 мс с округлением вниз), вы получите несколько фиксированную частоту кадров. Я говорю «несколько», потому что это сильно зависит от того, что вы делаете в вызовах updateModel () и repaintScreen () выше.

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

0 голосов
/ 04 ноября 2009

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

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

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

0 голосов
/ 04 ноября 2009

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

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

...