Попытка создать стабильный игровой движок - PullRequest
0 голосов
/ 23 марта 2011

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

В этой игре запущено несколько потоков, как в серверных, так и в клиентских приложениях, назначенных для различных задач.Давайте возьмем, например, поток движка в моем серверном приложении.В этой теме я пытаюсь создать игровой цикл с использованием Thread.sleep, пытаясь учесть время, затрачиваемое игровыми вычислениями.Вот мой цикл, помещенный в метод run ().Функция Tick () является полезной нагрузкой цикла.Он просто содержит упорядоченные вызовы других методов, выполняющих постоянное обновление игры.

long engFPS = 40;
long frameDur = 1000 / engFPS;
long lastFrameTime;
long nextFrame;

<...>

while(true)
{
    lastFrameTime = System.currentTimeMillis();
    nextFrame = lastFrameTime + frameDur;

    Tick();

    if(nextFrame - System.currentTimeMillis() > 0)
    {
        try
        {
            Thread.sleep(nextFrame - System.currentTimeMillis());
        }
        catch(Exception e)
        {
            System.err.println("TSEngine :: run :: " + e);
        }
    }
}

Основная проблема в том, что Thread.sleep просто любит предавать ваши ожиданияо том, сколько он будет спать.Он может легко перевести поток в режим ожидания гораздо дольше или намного короче, особенно на некоторых компьютерах с Windows XP (я сам это проверял, WinXP дает действительно неприятные результаты по сравнению с Win7 и другими ОС).Я довольно много разыскивал в интернете, и результат был разочаровывающим.Кажется, это ошибка планировщика потоков той ОС, в которой мы работаем, и его так называемая гранулярность.Насколько я понял, этот планировщик постоянно в течение определенного времени проверяет требования каждого потока в системе, в частности выводит / выводит их из спящего режима.Когда время повторной проверки мало, например, 1 мс, все может показаться плавным.Хотя говорят, что WinXP имеет степень детализации до 10 или 15 мс.Я также читал, что с этой проблемой сталкиваются не только программисты на Java, но и те, кто использует другие языки.Зная это, кажется почти невозможным сделать стабильный, крепкий, надежный игровой движок.Тем не менее, они везде.Мне очень интересно, с помощью чего можно решить эту проблему или обойти ее.Может ли кто-нибудь более опытный дать мне подсказку на это?

Ответы [ 3 ]

2 голосов
/ 23 марта 2011

Не полагайтесь на ОС или какой-либо механизм таймера, чтобы разбудить ваш поток или вызвать какой-либо обратный вызов в определенный момент времени или после точной задержки. Это просто не произойдет.

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

Так, например, вы можете сделать:

long maxWorkingTimePerFrame = 1000 / FRAMES_PER_SECOND;  //this is optional
lastStartTime = System.currentTimeMillis();
while(true)
{
    long elapsedTime = System.currentTimeMillis() - lastStartTime;
    lastStartTime = System.currentTimeMillis();

    Tick(elapsedTime);

    //enforcing a maximum framerate here is optional...you don't need to sleep the thread
    long processingTimeForCurrentFrame = System.currentTimeMillis() - lastStartTime;
    if(processingTimeForCurrentFrame  < maxWorkingTimePerFrame)
    {
        try
        {
            Thread.sleep(maxWorkingTimePerFrame - processingTimeForCurrentFrame);
        }
        catch(Exception e)
        {
            System.err.println("TSEngine :: run :: " + e);
        }
    }
}

Также обратите внимание, что вы можете получить большую гранулярность таймера, используя System.nanoTime() вместо System.currentTimeMillis().

0 голосов
/ 23 марта 2011

Вы можете получить лучшие результаты с

LockSupport.parkNanos(long nanos) 

, хотя это немного усложняет код по сравнению с sleep ()

0 голосов
/ 23 марта 2011

Может быть, это поможет вам. это от Бока Дэвида Брэкнина, развивающего игры в Java и вычисляет среднюю степень детализации, чтобы имитировать более плавную частоту кадров: ссылка

public class TimeSmoothie {
    /**
        How often to recalc the frame rate
    */
    protected static final long FRAME_RATE_RECALC_PERIOD = 500;
    /**
            Don't allow the elapsed time between frames to be more than 100 ms

    */
    protected static final long MAX_ELAPSED_TIME = 100;
    /**

        Take the average of the last few samples during the last 100ms

    */
    protected static final long AVERAGE_PERIOD = 100;
    protected static final int NUM_SAMPLES_BITS = 6; // 64 samples
    protected static final int NUM_SAMPLES = 1 << NUM_SAMPLES_BITS;
    protected static final int NUM_SAMPLES_MASK = NUM_SAMPLES - 1;
    protected long[] samples;
    protected int numSamples = 0;
    protected int firstIndex = 0;
    // for calculating frame rate
    protected int numFrames = 0;
    protected long startTime;
    protected float frameRate;

    public TimeSmoothie() {
        samples = new long[NUM_SAMPLES];
    }
    /**
        Adds the specified time sample and returns the average
        of all the recorded time samples.
    */

    public long getTime(long elapsedTime) {
        addSample(elapsedTime);
        return getAverage();
    }

    /**
        Adds a time sample.
    */

    public void addSample(long elapsedTime) {
        numFrames++;
        // cap the time
        elapsedTime = Math.min(elapsedTime, MAX_ELAPSED_TIME);
        // add the sample to the list
        samples[(firstIndex + numSamples) & NUM_SAMPLES_MASK] =
            elapsedTime;
        if (numSamples == samples.length) {
            firstIndex = (firstIndex + 1) & NUM_SAMPLES_MASK;
        }
        else {
            numSamples++;
        }
    }
    /**
        Gets the average of the recorded time samples.
    */

    public long getAverage() {
        long sum = 0;
        for (int i=numSamples-1; i>=0; i--) {
            sum+=samples[(firstIndex + i) & NUM_SAMPLES_MASK];
            // if the average period is already reached, go ahead and return
            // the average.
            if (sum >= AVERAGE_PERIOD) {
                Math.round((double)sum / (numSamples-i));
            }
        }

        return Math.round((double)sum / numSamples);

    }

    /**

        Gets the frame rate (number of calls to getTime() or

        addSample() in real time). The frame rate is recalculated

        every 500ms.

    */

    public float getFrameRate() {

        long currTime = System.currentTimeMillis();



        // calculate the frame rate every 500 milliseconds

        if (currTime > startTime + FRAME_RATE_RECALC_PERIOD) {

            frameRate = (float)numFrames * 1000 /

                (currTime - startTime);

            startTime = currTime;

            numFrames = 0;

        }



        return frameRate;

    }

}
...