Пишем игру для Android. Часть 3 - Как добиться одинаковой скорости выполнения игры на разных телефонах

Программирование игр в AndroidНапрошлом уроке мы говорили о работе с графикой. Мы нарисовали робота, научили его двигаться по экрану. Код работает, но имеет существенный недостаток: мы никак не контролируем скорость выполнения нашей программы. То есть на быстрых телефонах робот будет двигаться быстро, а на медленных - медленно. Поскольку все телефоны разные, необходим механизм, который будет обеспечивать одинаковую скорость перемещения робота по экрану.

Сейчас игровой цикл у нас выполняется в отдельном потоке, определенным в MainThread. Опуская все подробности упрощая игровой цикл может быть представлен в виде

boolean running=true;
while(!running)
{
updateGameState();
displayGameState();
}

Здесь updateGameState() обновляет состояние игровых объектов, а displayGameState() выводят игровую ситуацию на экран. Давайте введем два понятия, FPS и UPS:

  • FPS– Число кадров в секунду (Frames per Second), то есть сколько раз в секунду вызвался метод displayGameState().
  • UPS– Число обновлений в секунду (Update per Second), то есть сколько раз в секунду вызвался метод updateGameState().


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

Например, если наша цель 25 FPS, это означает что у нас метод displayGameState() вызывается каждые 40 мс (1000.25=40 мс, 1000 мс = 1с). Мы должны иметь ввиду, что updateGameState() должен запускаться до displayGameState(). Так что, если
мы хотим достичь показателя 25 FPC, нам нужно убедиться, что последовательность вызовов методов updateGameState() и displayGameState()  укладывается в 40 мс. В случае, если эта цифра будет превышена, будет создаваться ощущение, что игра притормаживает.

Чтобы лучше понять FPS, посмотрите на диаграмму. На показанном рисунке FPS=1,  поскольку цикл обновления-рисования (вызов методов updateGameState() и displayGameState()) укладывается в 1 секунду только 1 раз. На второй диаграмме FPS=10, то есть выполнение последовательности обновления-рисования занимает 100 мс и укладывается в секунду 10 раз.

Программирование игры для Android FPS

Применение такого подхода к организации игрового цикла имеет серьезный недостаток: скорость игры оказывается в прямой зависимости от производительности телефона. То есть на медленном телефоне все будет заметено подтормаживать, а на быстром - игровые объекты будут носиться по экрану с огромной скоростью.

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

Программирование игры для Android FPS

В случае запуске на слабом аппарате, последовательность обновления-рисования будет занимать больше времени, чем 0.1с, например, 0.12с. Это значит что у нас возникнет дефицит времени, и следующая итерация начнется на 0.02 с позже, чем требуется.

Программирование игры для Android FPS

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

Программирование игры для Android FPS

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

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

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

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

Давайте реализуем описанный механизм в нашем игровом цикле. Откройте файл MainThread.java и измените метод run()


// желательный fps
private final static int MAX_FPS=50;
// максимальное число кадров, которые можно пропустить
private final static int MAX_FRAME_SKIPS=5;
// период, который занимает кадр(последовательность обновление-рисование)
private final static int FRAME_PERIOD=1000/ MAX_FPS;
 
@Override
public void run(){
Canvas canvas;
Log.d(TAG,"Starting game loop");
 
long beginTime;// время начала цикла
long timeDiff;// время выполнения шага цикла
int sleepTime;// сколько мс можно спать (<0 если выполнение опаздывает)
int framesSkipped;// число кадров у которых не выполнялась операция вывода графики на экран
 
sleepTime=0;
 
while(running){
canvas=null;
// пытаемся заблокировать canvas
// для изменение картинки на поверхности
try{
canvas= this.surfaceHolder.lockCanvas();
synchronized(surfaceHolder){
beginTime=System.currentTimeMillis();
framesSkipped=0;// обнуляем счетчик пропущенных кадро
// обновляем состояние игры
this.gamePanel.update();
// формируем новый кадр
this.gamePanel.onDraw(canvas);//Вызываем метод для рисования
// вычисляем время, которое прошло с момента запуска цикла
timeDiff=System.currentTimeMillis()- beginTime;
// вычисляем время, которое можно спать
sleepTime=(int)(FRAME_PERIOD- timeDiff);
 
if(sleepTime>0){
// если sleepTime > 0 все хорошо, мы идем с опережением
try{
// отправляем поток в сон на период sleepTime
// такой ход экономит к тому же заряд батареи
Thread.sleep(sleepTime);
} catch(InterruptedException e){}
}
 
while(sleepTime<0&amp;&amp; framesSkipped< MAX_FRAME_SKIPS){
// если sleepTime < 0 нам нужно обновлять игровую
// ситуацию и не тратить время на вывод кадра
this.gamePanel.update();
// добавляем смещение FRAME_PERIOD, чтобы иметь
// время границы следующего кадра
sleepTime+= FRAME_PERIOD;
framesSkipped++;
}
}
} finally{
// в случае ошибки, плоскость не перешла в
//требуемое состояние
if(canvas!=null){
surfaceHolder.unlockCanvasAndPost(canvas);
}
}
}
}

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

Исходный код этого урока можно скачатьздесь.

Перевод:Александр Ледков

Источники:Android Game Development - The Game Loop




Наши соцсети

Подписаться Facebook Подписаться Вконтакте Подписаться Twitter Подписаться Google Подписаться Telegram

Популярное

Ссылки

Новости [1] [2] [3]... Android/ iOS/ J2ME[1] [2] [3]) Android / Архив

Рейтинг@Mail.ru Яндекс.Метрика
MobiLab.ru © 2005-2018
При использовании материалов сайта ссылка на www.mobilab.ru обязательна