Подтвердить что ты не робот

Исследование оптимального расчета времени сна в игровом цикле

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

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

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

Оглядываясь дальше, я нашел широко используемый метод исправления несоответствий времени сна, называемый "спин-сон", где вы заказываете только сон за 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 примерно до 10 000 нс для лучшей производительности на моей машине с Windows 7.

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

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

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

4b9b3361

Ответ 1

Вы можете использовать циклический таймер связанный с mutex. Это 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

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

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

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

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

Ответ 3

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

Ответ 4

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

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

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

Ответ 5

Thread.sleep является неточным и делает анимацию более нервной.

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

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