Своя игра с JavaScript и Canvas
Код на JavaScript очень критичен к ресурсам платформы. Несмотря, что почти все современные движки перестали тупо интерпретировать JS код, скорость его выполнения по-прежнему очень сильно уступает скорости «родного» кода. Между тем, даже простейшая игра — это много кода, который должен успевать выполниться между отрисовками двух соседних кадров анимации. Кроме того, JS — язык весьма специфический и написание объемного кода на нем сопряжено с рядом трудностей. Все вместе может стать причиной того, что приложение на JS перестанет удовлетворять ожиданиям и быстро принесет разочарование. Попробую немного систематизировать выводы, к которым я пришел путем экспериментов.
1. СовместимостьЕсли мы решили использовать HTML5 и Canvas в частности, то пусть нас больше не беспокоят вопросы совместимости со старыми браузерами – под ними все равно ничего не заработает. Таким образом, можно смело использовать основные нововведения ECMAScript 5. С другой стороны, не стоит обижать презрением пользователей старого доброго ПО, наподобие IE6. Желательно их уведомить, о причинах, почему они видят фигу серый квадрат, вместо нашей замечательной анимации. Сделать это элементарно, достаточно диагностировать поддержку Canvas и используемых языковых конструкций
Все бы хорошо, да вот беда – проблемы кроссбраузерной совместимости до конца не исчерпаны. Среди современных браузеров нет единого мнения насчет названий стандартных функций. Выход – либо вообще отказаться от их использования, либо делать дорогостоящие адаптеры. Например, я не смог отказать себе в использовании описателей свойств и это дало свои негативные последствия. О том, как их использовать кроссбраузерно, хорошо описано здесь, и здесь. А вот как их заставить работать быстро — осталось загадкой.
2. Оптимизацию кода легко сломатьНе так давно на Хабре проскакивала очень полезная статья про движок V8 для Chromium. Самое главное, что я сумел почерпнуть для себя – это скрытые классы и оптимизация кода для работы с ними. Действительно, JS зачастую провоцирует менять структуру объекта после его конструирования. Не стоит этого делать, если цель – создать быстрый и легко поддерживаемый код. Как только я это осознал, работа над игрой пошла веселее, а код стал чище и быстрее.
Так же нужно стремиться сокращать область видимости переменной до минимума – это увеличивает вероятность оптимизации кода.
3. В JS нет классов, наследования и прочего класс-ориентированного программирования.Не следует напрягать движок реализацией классов при помощи прототипирования – выгода сомнительна, а код замедляется в разы (Opera)! Сложное прототипное наследование и честно организованный перенос базового функционала к наследникам сбивают и без того не самую лучшую оптимизацию.
4. Платим за то, что используемПо ходу разработки игры или любого другого ресурсоемкого приложения, неизбежно приходится кэшировать «дорогие» ресурсы, например, предрасчитанные анимации или динамически загружаемые скрипты. У любого ресурса есть время жизни, после которого он оказывается не нужен. И тут важно правильно от него избавиться.
Скорее всего, память не будет освобождена сборщиком мусора (GC). Она будет собрана в произвольный момент времени, и он окажется самым неподходящим, потому что GC постарается удалить весь мусор, который накопится к этому моменту. Вот так уже лучше:
На первый взгляд – ничего сложного, но в более сложных случаях появляются нюансы, и поведение delete не всегда очевидно.
5. Замыкания и свойства – враги быстродействия Windows XP (x86), Core 2 Duo, 3 GHz Opera 12 FireFox 17 Chrome 23 Нет замыканий, прямой доступ к полям объекта 9 6 17 Нет замыканий, доступ к данным через методы 16 11 28 Замыкания, доступ через методы 34 12 23 Замыкания, доступ через свойства 387 899 489 Windows 7 (x64), Core i3-2100, 3.1 GHz Opera 12 Chrome 23 IE 10 Нет замыканий, прямой доступ к полям объекта 7 5 15 Нет замыканий, доступ к данным через методы 13 11 13 Замыкания, доступ через методы 27 9 14 Замыкания, доступ через свойства 222 315 99Как ни удивительно, Opera смотрится в тесте лучше других. К сожалению, общий вывод неутешителен, замыкания оптимизированы только в Chrome, а доступ через свойства – это большая роскошь, способная на порядок ухудшить быстродействие приложения.
Замечания о графическом движкеПрограммирование графики на JavaScript — отдельная большая тема, которую могу затронуть позже, если будет спрос. Здесь я постарался выделить несколько простых приемов, которые было легко использовать и они дали неплохой результат.
1. Покадровая анимация.Есть два подхода к созданию анимации: событийный и покадровый. Первый пригоден в основном для простых задач, наподобие подсветки кнопки при наведении мышки и может выполняться в соответствующих обработчиках. Второй годится для выполнения сложных анимационных задач, которые должны «жить своей жизнью», вне зависимости от действий пользователя, например, динамические игры. При создании игры проще всего (и дешевле в плане вычислительных ресурсов) рассчитывать игровой процесс, полагаясь на стабильность частоты кадров (frame). Можно попытаться использовать запрос кадра анимации у тех браузеров, что его поддерживают. Сделать это не так просто, потому что этот метод стал стандартом де-факто и по какой-то причине не попал в ECMAScript 5.
Выгода RequestAnimationFrame в основном состоит в том, что при ненагруженной анимации (когда сам процесс отрисовки занимает меньше половины времени кадра), он позволяет добиться большей плавности анимации и сокращает потребление ресурсов на мобильных платформах. Но на деле это может оказаться не так. К неудобствам от его использования можно отнести фиксированную частоту кадров (60 fps) и отсутствие компенсации продолжительности следующего кадра, если задержалась отрисовка предыдущего. Однако, что делать, если raf === null? Так может произойти, если ваше приложение попало в руки Opera, которая традиционно идет своим путем. Тогда нам поможет старый добрый setTimeout. В итоге код будет выглядеть примерно так:
Недостаток у такого подхода очевиден – если расчёт кадров будет подтормаживать больше, чем можно скорректировать – игровой процесс перестанет рассчитываться в реальном времени. Но можно пойти на хитрость. Как правило, причина тормозов — отрисовка элементов очередного кадра, т.к. для движка браузера это накладная операция. Поэтому можно написать код так, что в случае, когда коррекция времени не справляется (срабатывает условие if(frameTimeCorrection >= to)), в следующем кадре можно производить только расчет игрового мира без его перерисовки. Появится т.н. «лаг» который в игре выглядит менее раздражающе, чем slow motion.
2. Рисуем только то, что видно на холсте. Windows XP (x86), Core 2 Duo, 3 GHz Opera 12 FireFox 17 Chrome 23 «Умные» спрайты 47 35 25 «Глупые» спрайты 15 14 12 Windows 7 (x64), Core i3-2100, 3.1 GHz Opera 12 Chrome 23 IE 10 «Умные» спрайты 56 32 61 «Глупые» спрайты 19 15 51Разница ощутима (а Хромой снова подкачал – миф развенчан).
3. Кэшируем растеризацию Windows XP (x86), Core 2 Duo, 3 GHz Opera 12 FireFox 17 Chrome 23 Кэширование растеризации 23 32 60 Без кэширования 5 12 47 Windows 7 (x64), Core i3-2100, 3.1 GHz Opera 12 Chrome 23 IE 10 Кэширование растеризации 33 61 61 Без кэширования 5 56 23При таком подходе важно соблюдать баланс между памятью, частотой сброса кэша и быстродействием растеризации. Так, если текст меняется динамически и довольно интенсивно (скажем, раз в 10 кадров анимации), то его кэширование может только ухудшить общее быстродействие, т.к. сама операция кэширования будет вносить больше накладных расходов, чем растеризация.
4. Динамическая загрузка ресурсовЕсли анимация строится на спрайтах из битовых карт (bitmap), то прежде, чем их можно будет рисовать на холсте, следует загрузить эти самые карты в кэш изображений браузера. Для этого достаточно создать элемент Image и в качестве источника передать url ресурса картинки. Сложность состоит в том, чтобы дождаться момента, когда браузер загрузит картинку в свой кэш. Для этого можно использовать событие onload, в котором, увеличивать счетчик уже загруженных картинок. Как только значение это счетчика совпадет с числом картинок, добавленных к загрузке, ресурс станет персистентным, и мы можем выполнять основной игровой код.
В своей игрушке я решил каждый уровень описывать отдельным скриптовым файлом. Понятно, что статическая загрузка таких скриптов вредна, т.к. в каждый момент времени нужен только один из них. Решению задачи помог тот же подход, что и в случае загрузки изображений. Есть только один нюанс — у объекта Script нет событий, но это и не беда, т.к. в код динамически загружаемых скриптов можно вставить глобальную функцию регистрации скрипта в кэше. Дальше поступаем аналогично загрузке картинок — асинхронно ждем, пока скрипт зарегистрирует описанные в нем типы, а затем создаем нужные нам экземпляры зарегистрированных типов. Для того чтобы пользователь не скучал — можно показать процент загрузки всех необходимых ресурсов.