Исследование оптимального расчета времени сна в игровом цикле - PullRequest
19 голосов
/ 11 марта 2011

При программировании анимации и небольших игр я узнал о невероятной важности Thread.sleep(n);. Я полагаюсь на этот метод, чтобы сообщить операционной системе, когда моему приложению не понадобится ЦП, и с его помощью моя программа прогрессирует впредсказуемая скорость.

Моя проблема заключается в том, что JRE использует разные методы реализации этой функциональности в разных операционных системах.В основанных на UNIX (или под влиянием) ОС: таких, как Ubuntu и OS X, базовая реализация JRE использует хорошо функционирующую и точную систему для распределения процессорного времени между различными приложениями, что делает мою 2D-игру гладкой и без задержек,Тем не менее, в Windows 7 и более старых системах Microsoft распределение времени процессора, кажется, работает по-другому, и вы обычно возвращаете время процессора после заданного количества сна, которое варьируется примерно через 1-2 мс от целевого сна.Тем не менее, вы получаете случайные всплески дополнительного 10-20 мс времени сна.Это заставляет мою игру задерживаться каждые несколько секунд, когда это происходит.Я заметил, что эта проблема существует в большинстве Java-игр, которые я пробовал в Windows, при этом Minecraft является заметным примером.

Теперь я искал в Интернете, чтобы найти решение этой проблемы.Я видел много людей, использующих только Thread.yield(); вместо Thread.sleep(n);, что безупречно работает за счет того, что используемое в настоящее время ядро ​​ЦП получает полную нагрузку, независимо от того, сколько ЦП фактически требует ваша игра.Это не идеально подходит для игры на ноутбуках или рабочих станциях с высоким энергопотреблением, и это ненужный компромисс на компьютерах Mac и Linux.

Оглядываясь дальше, я нашел широко используемый метод исправления несоответствий во время сна, называемый«spin-sleep», где вы заказываете режим сна только по 1 мс за раз и проверяете согласованность с помощью метода System.nanoTime();, который очень точен даже в системах Microsoft.Это помогает в течение 1-2 мс несогласованности сна, но не помогает против случайных всплесков + 10-20 мс несогласованности сна, поскольку это часто приводит к увеличению времени, затрачиваемого на то, чтобы один цикл моего цикла занял всевместе.

После долгих поисков я нашел загадочную статью Энди Малакова, которая очень помогла улучшить мой цикл: http://andy -malakov.blogspot.com / 2010/06 / alternative-to-threadsleep.html

Основываясь на его статье, я написал этот метод сна:

// Variables for calculating optimal sleep time. In nanoseconds (1s = 10^-9ms).
private long timeBefore = 0L;
private long timeSleepEnd, timeLeft;

// The estimated game update rate.
private double timeUpdateRate;

// The time one game loop cycle should take in order to reach the max FPS.
private long timeLoop;

private void sleep() throws InterruptedException {

    // Skip first game loop cycle.
    if (timeBefore != 0L) {

        // Calculate optimal game loop sleep time.
        timeLeft = timeLoop - (System.nanoTime() - timeBefore);

        // If all necessary calculations took LESS time than given by the sleepTimeBuffer. Max update rate was reached.
        if (timeLeft > 0 && isUpdateRateLimited) {

            // Determine when to stop sleeping.
            timeSleepEnd = System.nanoTime() + timeLeft;

            // Sleep, yield or keep the thread busy until there is not time left to sleep.
            do {
                if (timeLeft > SLEEP_PRECISION) {
                    Thread.sleep(1); // Sleep for approximately 1 millisecond.
                }
                else if (timeLeft > SPIN_YIELD_PRECISION) {
                    Thread.yield(); // Yield the thread.
                }
                if (Thread.interrupted()) {
                    throw new InterruptedException();
            }
                timeLeft = timeSleepEnd - System.nanoTime();
            }
            while (timeLeft > 0);
        }
        // Save the calculated update rate.
        timeUpdateRate =  1000000000D / (double) (System.nanoTime() - timeBefore);
    }
    // Starting point for time measurement.
    timeBefore = System.nanoTime();
}

SLEEP_PRECISION Я обычно ставлю примерно на 2 мс, а SPIN_YIELD_PRECISION - примерно на 10000 нс для лучшей производительности на моем компьютере с Windows 7.

После множества тяжелых работ это самое лучшее, что я могу придумать.Итак, так как я все еще беспокоюсь об улучшении точности этого метода сна, и я все еще не удовлетворен производительностью, я хотел бы обратиться ко всем вам, хакерам и аниматорам Java-игр, за предложениями по лучшему решению дляПлатформа Windows.Могу ли я использовать платформу для Windows, чтобы сделать ее лучше?Меня не волнует наличие небольшого кода для конкретной платформы в моих приложениях, поскольку большая часть кода не зависит от ОС.

Я также хотел бы знать, есть ли кто-нибудь, кто знает о Microsoft и OracleРазрабатываете лучшую реализацию метода Thread.sleep(n);, или каковы будущие планы Oracle по улучшению своей среды в качестве основы для приложений, требующих высокой точности синхронизации, таких как музыкальные программы и игры?

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

Ответы [ 5 ]

4 голосов
/ 12 марта 2011

Ваши возможности ограничены, и они зависят от того, что именно вы хотите сделать. В вашем фрагменте кода упоминается максимальный FPS, но максимальный FPS потребует, чтобы вы вообще никогда не спали, поэтому я не совсем уверен, что вы собираетесь с этим делать. Однако ни одна из этих проверок сна или выхода не изменит ничего в большинстве проблемных ситуаций - если какое-то другое приложение должно быть запущено сейчас, а ОС не хочет в ближайшее время переключаться обратно, не имеет значения, какое из них когда вы позвоните, вы получите контроль, когда ОС решит это сделать, что в будущем, скорее всего, будет больше 1 мс. Тем не менее, ОС, безусловно, может быть направлена ​​на то, чтобы делать переключение чаще - Win32 имеет вызов timeBeginPeriod именно для этой цели, который вы можете использовать каким-либо образом. Но есть веская причина не переключаться слишком часто - это менее эффективно.

Лучшее, что может быть сделано, хотя и несколько сложнее, - это пойти на игровой цикл, который не требует обновлений в реальном времени, но вместо этого выполняет обновления логики с фиксированными интервалами (например, 20x в секунду) и рендерит всякий раз, когда возможно (возможно, с произвольным коротким сном, чтобы освободить процессор для других приложений, если он не работает в полноэкранном режиме). Буферизируя прошлое логическое состояние, а также текущее, вы можете интерполировать их, чтобы рендеринг выглядел так же гладко, как если бы вы каждый раз выполняли обновления логики. Дополнительную информацию об этом подходе вы можете найти в статье Fix Your Timestep .

Я также хотел бы знать, есть ли кто-нибудь, кто знает, что Microsoft и Oracle разрабатывают лучшую реализацию Thread.sleep (n); метод, или каковы дальнейшие планы Oracle по улучшению своей среды в качестве основы для приложений, требующих высокой точности синхронизации, таких как музыкальное программное обеспечение и игры?

Нет, этого не произойдет. Помните, сон - это просто метод, который говорит, как долго вы хотите, чтобы ваша программа спала. Это не спецификация, когда он будет или должен проснуться, и никогда не будет. По определению, любая система с функциями ожидания и выхода является многозадачной системой, в которой необходимо учитывать требования других задач, и операционная система всегда получает окончательный вызов для планирования этого. Альтернатива не будет работать надежно, потому что, если программа может потребовать повторной активации в определенный момент ее выбора, она может лишить других процессоров мощности процессора. (Например, программа, которая породила фоновый поток и имела оба потока, выполняющие 1 мс работы и вызывающие sleep (1) в конце, может по очереди загружать ядро ​​ЦП.) Таким образом, для программы пользовательского пространства, сна (и функциональности) как это) всегда будет нижней границей, а не верхней границей. Чтобы добиться этого, требуется, чтобы сама ОС позволяла определенным приложениям в значительной степени владеть расписанием, и это нежелательная функция в операционных системах для потребительского оборудования (хотя она является распространенной и полезной функцией для промышленных приложений).

4 голосов
/ 11 марта 2011

Вы можете использовать циклический таймер , связанный с мьютексом . Это IHMO самый эффективный способ делать то, что вы хотите. Но тогда вам следует подумать о пропуске кадров на случай, если компьютер зависает (вы можете сделать это с помощью другого неблокирующего мьютекса в коде таймера.)

Редактировать: псевдокод для уточнения

Код таймера:

While(true):
  if acquireIfPossible(mutexSkipRender):
    release(mutexSkipRender)
    release(mutexRender)

Код сна:

acquire(mutexSkipRender)
acquire(mutexRender)
release(mutexSkipRender)

Начальные значения:

mutexSkipRender = 1
mutexRender = 0

Редактировать : исправлены значения инициализации.

Следующий код довольно хорошо работает на окнах (зацикливает на скорости 50 кадров в секунду с точностью до миллисекунды)

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Semaphore;


public class Main {
    public static void main(String[] args) throws InterruptedException {
        final Semaphore mutexRefresh = new Semaphore(0);
        final Semaphore mutexRefreshing = new Semaphore(1);
        int refresh = 0;

        Timer timRefresh = new Timer();
        timRefresh.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                if(mutexRefreshing.tryAcquire()) {
                    mutexRefreshing.release();
                    mutexRefresh.release();
                }
            }
        }, 0, 1000/50);

        // The timer is started and configured for 50fps
        Date startDate = new Date();
        while(true) { // Refreshing loop
            mutexRefresh.acquire();
            mutexRefreshing.acquire();

            // Refresh 
            refresh += 1;

            if(refresh % 50 == 0) {
                Date endDate = new Date();
                System.out.println(String.valueOf(50.0*1000/(endDate.getTime() - startDate.getTime())) + " fps.");
                startDate = new Date();
            }

            mutexRefreshing.release();
        }
    }
}
2 голосов
/ 11 марта 2011

Сроки работают на окнах, как известно, плохо. Эта статья - хорошее место для начала. Не уверен, что вас это волнует, но также обратите внимание, что могут быть и более серьезные проблемы (особенно с System.nanoTime) на виртуальных системах (когда Windows является гостевой операционной системой).

1 голос
/ 11 марта 2011

Thread.Sleep говорит, что вашему приложению больше не нужно время.Это означает, что в худшем случае вам придется ждать целого среза потока (40 мс или около того).

Теперь в плохих случаях, когда драйвер или что-то занимает больше времени, это может бытьподождите 120 мс (3 * 40 мс), поэтому Thread.Sleep - это не тот путь.Идите другим путем, например, зарегистрируйте обратный вызов 1 мс и начните рисовать код с очень большим количеством обратных вызовов X.

(Это в Windows, я бы использовал инструменты MultiMedia для получения этих обратных вызовов с разрешением 1 мс)

0 голосов
/ 23 января 2012

Thread.sleep является неточной и делает анимацию дрожащей большую часть времени.

Если вы полностью замените его на Thread.yield, вы получите стабильный FPS без задержки или дрожания, однако загрузка ЦП значительно возрастает. Я перешел на Thread.yield давно.

Эта проблема обсуждалась на форумах по разработке игр Java в течение многих лет.

...