Статьи

Разработка игр для Android — The Game Loop

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

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

1
2
3
4
5
6
boolean running = true;
while (!running)
{
   updateGameState();
   displayGameState();
}

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

UpdateGameState () обновляет состояние каждого объекта в игре, а displayGameState () отображает объекты в изображение, отображаемое на экране.

Здесь следует рассмотреть две вещи: FPS и UPS.

FPS — число кадров в секунду — количество вызовов displayGameState () в секунду.
UPSUpdate per Second — количество вызовов updateGameState () в секунду.

В идеале методы update и render будут вызываться одинаковое количество раз в секунду (предпочтительно не менее 20-25 раз в секунду). 25 FPS обычно достаточно для телефона, чтобы мы, люди, не заметили, что анимация была вялой.

Например, если мы нацелены на 25 FPS, это означает, что мы должны вызывать метод displayGameState () каждые 40 мс (1000/25 = 40 мс, 1000 мс = 1 с). Мы должны помнить, что updateGameState () также вызывается перед методом отображения, и чтобы мы достигли 25 FPS, мы должны убедиться, что последовательность обновления — отображения выполняется ровно за 40 мс. Если это занимает менее 40 мс, то у нас более высокий FPS. Если это займет больше, то у нас будет медленная игра.

Давайте посмотрим несколько примеров, чтобы лучше понять FPS.

Следующая диаграмма показывает ровно 1 FPS. Для выполнения цикла обновлениярендеринга требуется ровно одна секунда. Это означает, что изображение на экране будет меняться раз в секунду.

1 кадр в секунду

Следующая диаграмма показывает 10FPS. Цикл обновлениярендеринга занимает 100 мс. Это означает, что каждую десятую секунды изображение меняется.

10 кадров в секунду

Но приведенный выше сценарий означает, что цикл обновлениярендеринга выполняется в 1/10 секунды КАЖДЫЙ раз. Это предположение, и мы не можем контролировать фактическое время выполнения цикла, или мы можем? Что произойдет, если у нас будет 200 врагов и каждый враг стреляет в нас? Нам нужно обновить состояние каждого врага и состояние его пуль и проверить наличие столкновений в одном обновлении. Другое дело, когда у нас всего 2 врага. Время будет четко отличаться. То же самое относится и к методу рендеринга. Рендеринг 200 дроидов явно займет больше времени, чем рендеринг только 2.

Так каковы сценарии? У нас может быть цикл обновления-рендеринга, который заканчивается менее чем за 100 мс (1/10 секунды), заканчивается ровно за 100 мс или заканчивается более чем за это. На мощном оборудовании это будет быстрее, чем на более слабом. Давайте посмотрим на диаграммы.

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

Кадр со временем, чтобы сэкономить

Следующая диаграмма показывает цикл, который отстает. Это означает, что время, необходимое для завершения цикла рендеринга обновлений, больше требуемого. Если это занимает 12 мс, это означает, что мы отстаем на 2 мс (все еще учитывая 10FPS). Это может нарастить, и каждый цикл мы теряем время, и игра будет работать медленно.

Просроченная рамка

Первая ситуация желаемая. Это дает нам немного свободного времени, чтобы что-то сделать, прежде чем мы начнем следующий цикл. Нам не нужно ничего делать, поэтому мы просто просим игровой цикл перейти в спящий режим на оставшийся период времени и проснуться, когда наступит следующий цикл. Если мы этого не сделаем, игра будет работать быстрее, чем предполагалось. Благодаря введению времени ожидания мы достигли постоянной частоты кадров .

Вторая ситуация (я пропустил идеальную, так как она почти никогда не происходит), когда цикл позади, требует другого подхода.

Для достижения постоянной скорости в игре нам необходимо обновлять состояние наших объектов, когда это необходимо. Представьте себе дроида, который приближается к вам с постоянной скоростью. Вы знаете, прошел ли он половину экрана за одну секунду, поэтому потребуется еще одна секунда, чтобы добраться до другой стороны экрана. Чтобы точно рассчитать положение, нам нужно знать либо дельту времени с момента последнего положения, и текущую скорость дроида, либо мы обновляем положение (состояние) дроида с постоянными интервалами. Я выберу второй, так как играть с дельтами в обновлении игры может быть сложно. Для достижения постоянной скорости игры нам придется пропускать показ кадров. Скорость игры НЕ FPS!

Изучите следующую диаграмму. Это сценарий, в котором цикл обновлениярендеринга занимает больше времени, чем нужно, поэтому нам нужно наверстать упущенное. Для этого мы пропустим рендеринг этого кадра и сделаем еще одно обновление, чтобы скорость игры не пострадала. Мы сделаем нормальный цикл в следующем кадре с еще некоторым временем, чтобы отдать ЦП отдых.

Постоянная скорость игры с переменным FPS

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

RunThin MainThread.java выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// desired fps
private final static int    MAX_FPS = 50;
// maximum number of frames to be skipped
private final static int    MAX_FRAME_SKIPS = 5;
// the frame period
private final static int    FRAME_PERIOD = 1000 / MAX_FPS; 
 
@Override
public void run() {
    Canvas canvas;
    Log.d(TAG, "Starting game loop");
 
    long beginTime;     // the time when the cycle begun
    long timeDiff;      // the time it took for the cycle to execute
    int sleepTime;      // ms to sleep (<0 if we're behind)
    int framesSkipped;  // number of frames being skipped
 
    sleepTime = 0;
 
    while (running) {
        canvas = null;
        // try locking the canvas for exclusive pixel editing
        // in the surface
        try {
            canvas = this.surfaceHolder.lockCanvas();
            synchronized (surfaceHolder) {
                beginTime = System.currentTimeMillis();
                framesSkipped = 0// resetting the frames skipped
                // update game state
                this.gamePanel.update();
                // render state to the screen
                // draws the canvas on the panel
                this.gamePanel.render(canvas);
                // calculate how long did the cycle take
                timeDiff = System.currentTimeMillis() - beginTime;
                // calculate sleep time
                sleepTime = (int)(FRAME_PERIOD - timeDiff);
 
                if (sleepTime > 0) {
                    // if sleepTime > 0 we're OK
                    try {
                        // send the thread to sleep for a short period
                        // very useful for battery saving
                        Thread.sleep(sleepTime);
                    } catch (InterruptedException e) {}
                }
 
                while (sleepTime < 0 && framesSkipped < MAX_FRAME_SKIPS) {
                    // we need to catch up
                    // update without rendering
                    this.gamePanel.update();
                    // add frame period to check if in next frame
                    sleepTime += FRAME_PERIOD;
                    framesSkipped++;
                }
            }
        } finally {
            // in case of an exception the surface is not left in
            // an inconsistent state
            if (canvas != null) {
                surfaceHolder.unlockCanvasAndPost(canvas);
            }
        }   // end finally
    }
}

Изучите приведенный выше код очень внимательно, поскольку он реализует логику, лежащую в основе диаграммы. Вы можете найти полный код в загружаемом проекте.

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

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

Обратите внимание, что я также изменил значения по умолчанию в классе Speed.java . Скорость измеряется в единицах / секунду. Поскольку мы устанавливаем желаемый FPS на 50, это означает, что скорость будет увеличиваться на 50 * speed.value каждое обновление. Чтобы иметь скорость, скажем, 40 пикселей в секунду, вам нужно установить дельту скорости для каждого тика равной 2 (40 / (1000/50) = 2). Другими словами, вам нужно, чтобы дроид продвигался на 2 пикселя при каждом обновлении игры (если у вас 50 обновлений в секунду), чтобы покрыть 40 пикселей в секунду.

Загрузите код здесь и играйте с ним.

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

Ссылка: Game Loop от нашего партнера JCG Тамаса Яно из блога « Против зерна ».

Не забудьте проверить нашу новую Android игру ArkDroid (скриншоты ниже) . Ваш отзыв будет более чем полезным!
Статьи по Теме: