четверг, 12 августа 2010 г.

Создание оптимизированного игрового цикла

Представляю вашему вниманию перевод руководства «Creating an Optimized AS3 Game Timer Loop».

Сегодня мы создадим оптимизированный игровой цикл. Этот цикл я начал делать в моем предыдущем туториале, теперь самое время его оптимизировать. Мы добавим к нему три улучшения. Во-первых, мы будем просто блокировать холст прежде, чем будем использовать метод copyPixels() и разблокировать когда все операции блитирования будут завершены. Второе, мы реализуем таймер, учитывающий бездействие игры (например, вследствие загрузки процессора). Третье, мы будем обновлять игровое содержимое не в каждом кадре, а только когда произойдет его изменение.


Самым обсуждаемым, наименее понятным и, в конечном счете, самым расстраивающим вопросом программирования игр на Flash является поддержания одинаковой частоты кадров игры, запущенной на различных компьютерах. Мы получаем разное количество кадров в секунду (FPS) в IDE, в автономном плеере и в браузере.

По теории, для плавного воспроизведения движения в игре достаточно частоты 30 кадров в секунду. Так что большинство из нас устанавливают в своих роликах частоту кадров в 30 FPS и надеются на лучшее. Если нам повезет или мы создадим игровой движок, который не будет нагружать плеер или наш навык оптимизации достаточно высок, то мы получим достаточно постоянное количество FPS. Когда мы компилируем игру и запускаем во внешнем плеере, то тоже получаем количество FPS, близкое к 30. Мы радуемся и запускаем игру в каком-нибудь браузере. Обычно здесь и начинаются проблемы. В Internet Explorer с режимом внедрения wmode установленным по умолчанию в window, наша игра теряет около 2-5 FPS. В других браузерах, например Firefox, FPS может упасть еще ниже. Возможно в последних обновлениях эта ситуация исправлена, но все равно FPS за пределами Flash IDE будет разная.

Другая проблема – игровой процесс может нагрузить процессор так, что вместо 40 запланированных обновлений в секунду он сможет рассчитать, например, всего 30. Нам нужно учитывать время бездействия игры, чтобы скомпенсировать невыполнившиеся обновления мира.

1. BitmapData.lock и BitmapData.unlock.



Суть блокировки битмапдаты в том, чтобы любые объекты, ссылающиеся на объект BitmapData, например объекты Bitmap, не обновлялись (это очень «дорогостоящая» операция) при изменении данного объекта BitmapData путем многократных вызовов copyPixels(). Когда мы вызываем метод unlock(), то все изменения отображаются за один раз.
На практике это выглядит следующим образом. Мы возьмем для примера игровой цикл из игры 7800 Asteroids:

	checkKeys();
updatePlayer();
updateRocks();
updateMissiles();
checkCollisions();
canvasBD.lock();
drawBackground();
drawPlayer();
drawRocks();
drawMissiles();
drawParts();
canvasBD.unlock();


Как видите, перед операцией отрисовки мы блокируем наш холст canvasBD, который является единственным отображаемым объектом в нашей игре. Все методы draw…() используют метод copyPixels() для блитирования в холст пикселей изображений корабля, астероидов, ракет, частиц взрывов. Блокируя битмапдату, мы не даем canvasBitmap обновляться до тех пор, пока не разблокируем ее битмапдату.

2. Таймер, отслеживающий бездействие.



Таймер, отслеживающий бездействие – это не мое уникальное изобретения и код, который мы будем здесь использовать – также не моя реализация. Идея взята из книги Andrew Davidson «Killer Game Programming in Java».

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

		private function runGame(e:TimerEvent) {

_beforeTime = getTimer();
_overSleepTime = (_beforeTime - _afterTime) - _sleepTime;

checkKeys();
updatePlayer();
updateRocks();
updateMissiles();
checkCollisions();
canvasBD.lock();
drawBackground();
drawPlayer();
drawRocks();
drawMissiles();
drawParts();
canvasBD.unlock();

_afterTime = getTimer();
_timeDiff = _afterTime - _beforeTime;
_sleepTime = (_period - _timeDiff) - _overSleepTime;
if (_sleepTime <= 0) {
_excess -= _sleepTime
_sleepTime = 2;
}
gameTimer.reset();
gameTimer.delay = _sleepTime;
gameTimer.start();

while (_excess > _period) {
checkKeys();
updatePlayer();
updateRocks();
updateMissiles();
checkCollisions();
_excess -= _period;
}

frameTimer.countFrames();
frameTimer.render();
if (aRock.length ==0 && aActiveParticle.length==0) stopRunningGame();
e.updateAfterEvent();
}


В секции объявления переменных добавились новые переменные:

		public static const FRAME_RATE:int = 40;
private var _period:Number = 1000 / FRAME_RATE;
private var _beforeTime:int = 0;
private var _afterTime:int = 0;
private var _timeDiff:int = 0;
private var _sleepTime:int = 0;
private var _overSleepTime:int = 0;
private var _excess:int = 0;


FRAME_RATE количество обновлений экрана в секунду.
_period – количество миллисекунд, за которые мы должны выполнить все операции одного цикла. В этом случае это 25 (1000/40).
_beforeTime и _afterTime создадут простой профайлер. С помощью этих переменных мы можем определить время, затраченное на выполнение кода. Мы надеемся, что это время будет меньше или равно 25 миллисекундам (значение переменной _period).
_timeDiff – разница между _afterTime и _beforeTime.
_sleepTime – разница между _period и _timeDiff. От этого значения мы вычтем значение _overSleepTimer.
_overSleepTimer – переменная, помогающая определить время сна в текущей итерации, в зависимости от времени сна в прошлой итерации
_excess – эта переменная хранит общий избыток времени со всех прошедших циклов. Если ее значение больше значения переменной _period, то мы обновляем все объекты без их отрисовки.

Результатом этого кода будет такой рендеринг объектов, который даст плавное движение объектов даже в тех случаях, когда процессор не сможет выполнить цикл из-за загрузки. В этом случае мы просто обновляем координаты объекта без их отрисовки. В следующем вызове отрисовки эти объекты будут отрисованы в новых позициях.
Мы также поменяли нашу переменную gameTimer():

		gameTimer=new Timer(_period,1); //значение 50 заменено на значение _period
gameTimer.addEventListener(TimerEvent.TIMER, runGame);
gameTimer.start();


Мы запускаем наш таймер один раз, затем определяем сколько наш таймер должен «спать», и перезапускаем таймер с значением задержки таймера, равной значению _sleepTimer:

		gameTimer.reset();
gameTimer.delay = _sleepTime;
gameTimer.start();



3. UpdateAfterEvent()



Третья и последняя оптимизация – добавление в этот таймер метода event.updateAfterEvent().

Этот способ я нашел в книге Кейта Петерса «AdvancED ActionScript Animation» (которая, как я думаю, должна быть на книжной полке каждого Flash-разработчика наряду с работами Колина Мука и Джоба Макара).

Вызывая метод event.updateAfterEvent() объекта TimerEvent, мы вынуждаем Flash обновить экран немедленно. Таким образом, мы можем присвоить частоте кадров нашего ролика относительно низкое значение. Мне нравиться значение 40. Раньше, для получения гарантированных значений частоты кадров в 35-40 в различных браузерах, мы были вынуждены задавать исходное значение равным 60. В результате это конечно больше нагружает процессор, т.к. он будет стремиться к выполнению 60 циклов в секунду. Теперь же я могу установить частоту кадров равную 40 и получить плавное воспроизведение ролика во всех браузерах.
Что вы сделаете, если дизайнер даст вам анимацию, которая должна проигрываться с частотой 18 FPS, а игра должна иметь 30 FPS. Раньше мне нужно было приложить много усилий, чтобы решить эту проблему. Теперь я могу установить для этой анимации FPS, равный 18, а для игры - 30.
Вот весь обновленный код игрового цикла:

		private function runGame(e:TimerEvent) {

_beforeTime = getTimer();
_overSleepTime = (_beforeTime - _afterTime) - _sleepTime;

checkKeys();
updatePlayer();
updateRocks();
updateMissiles();
checkCollisions();
canvasBD.lock();
drawBackground();
drawPlayer();
drawRocks();
drawMissiles();
drawParts();
canvasBD.unlock();

_afterTime = getTimer();
_timeDiff = _afterTime - _beforeTime;
_sleepTime = (_period - _timeDiff) - _overSleepTime;
if (_sleepTime <= 0) {
_excess -= _sleepTime
_sleepTime = 2;
}
gameTimer.reset();
gameTimer.delay = _sleepTime;
gameTimer.start();

while (_excess > _period) {
checkKeys();
updatePlayer();
updateRocks();
updateMissiles();
checkCollisions();
_excess -= _period;
}

frameTimer.countFrames();
frameTimer.render();
if (aRock.length ==0 && aActiveParticle.length==0) stopRunningGame();
e.updateAfterEvent();
}


На этом у меня все. Исходники

4 комментария:

  1. [...] Этот метод – обработчик события TimerEvent.TIMER. В этом методе выполняются все действия по изменению игрового мира (подробнее можно почитать здесь). [...]

    ОтветитьУдалить
  2. Столкнулся с такой проблемой. Допустим игра подтормаживает, идет накопление в счетчике кадров и мы пропускаем часть циклов отрисовки и выполняем пропущенные циклы апдейтов. Но если игра начинает подтормаживать не из-за отрисовки, а например из-за физического движка или большого количество объектов на экране и их затратного расчета в апдейте, что происходит тогда? Игра виснет! Потому что накапливается все больше и больше пропущенных кадров, которые итак не успели обработаться, а их еще прогоняем через цикл while (_excess > _period)

    ОтветитьУдалить
  3. вы правы, в этом случае будет виснуть. придется разбираться с тормозами :)

    ОтветитьУдалить
  4. К сожалению, мне пришлось отказаться от этого метода. Он, конечно, очень интересный и спасибо за него.

    Но производительность игр, например, с библиотекой box2d часто оставляет желать лучшего. У меня тормозили такие больший  box2d  проекты как MINING TRUCK 2,  Crush the Castle 2, Zombotron. Так что если бы они использовали метод из этой статьи, то их игры бы крепко вешали не самые мощные компьютеры.

    ОтветитьУдалить