Growing Crystals vol 10. Быстрая анимация в игре на Canvas, requestAnimationFrame

В предыдущей статье, я разбирал способы управления игрой с мобильного устройства. Удалось получить хорошую, хотя и не многочисленную обратную связь от форумчан gamedev.ru.

Сегодня мной будет разработана и разобрана игровая анимация.

Но прежде чем приступить к игровой анимации пришлось достаточно сильно переработать клиент, написанный на JavaScript:

  • Разработана система загрузки ресурсов до запуска Welcome Menu;
  • Изменено управление для изометрии WSAD->WAQS, SPACE -> M;
  • Для плавной синхронизации увеличена точность серверного и клиентского времени до 0.01 сек.;
  • На клиенте реализована логика очередей событий;
  • Сервер: доработана система логирования;
  • Сервер: reject запросов приходящих чаще чем 0,5 секунды;
  • Сервер: игровое время теперь считается в сотых долях секунды.

Оказалось что Canvas отличный, на сегодняшний день весьма эффективный инструмент, с хорошим API, большим количеством библиотек и достаточной производительностью. Я рад выбору сделанному две недели назад. Однако, нельзя назвать Canvas зрелым, поскольку до сих пор нету единого мнения о том, как обеспечить его 100% совместимость, не все функции поддерживаются браузерами, среди библиотек трудно найти что-то по настоящему стоящее, всё сводится к тому, что кто-то что-то писал с помощью Canvas и в результате родилась библиотека, которая не всегда хороша по быстродействию, которая не всегда обладает достаточным количеством примеров.

На хабре нашел таблицу (зеркало) в которой представлено сравнение библиотек.

Я рассмотрю лишь некоторые из них, те, которые пытался использовать при реализации анимации в своём проекте.

libcanvas.com (Upd 2017.01.26: домен умер) — одна из самых документированных, хорошо описанных и функциональных библиотек для работы с Canvas, разработанная Павлом Пономаренко aka TheShock. Но меня смутило, что библиотека опирается на AtomJS framework того же автора, что осложнило понимание, а также смутило отсутствие GettingStarted для совсем новичков в разработке. Даже прослушав лекцию автора на Youtube не удалось проникнуть в суть и начать разработку на базе LibCanvas. Если вас заинтересовала библиотека, рекомендую ознакомиться с весьма неплохими примерами.
FabricJS — в отличие от предыдущей библиотеки, имеется хороший, но не полный туториал (отсутствует демонстрация работы со спрайтами). Библиотека не опирается ни на какие другие библиотеки и достаточно проста и лаконична с точки зрения кодирования.
JCscript — очень простая и легковесная библиотека, с которой оказалось очень легко разобраться. Но проблема в том, что на не очень функциональна и больше практически не поддерживается автором.
Paperjs — самая популярная и производительная библиотека. Огромное количество материалов. Самые впечатляющие примеры. Но, опять же отсутствует туториал по созданию анимаций основанных на спрайтах.
EaselJS — интересная библиотека, понравившаяся тем, что на ней уже реализовано достаточное количество проектов. Разобраться с ней можно за несколько часов, особенно, что касается спрайтовой анимации — достаточно зайти на страницу и посмотреть её исходный код, который будет понятен даже новичку в JS.

Вывод

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

Ознакомившись с пачкой статей о Canvas и о повышении производительности, я применил несколько приёмов:

Для определения момента отрисовки нового кадра я применяю функцию window.requestAnimationFrame, если она имеется в браузере.

        // Если ничего нет — возвращаем обычный таймер
        requestAnimFrame =  window.requestAnimationFrame       ||
                window.webkitRequestAnimationFrame ||
                window.mozRequestAnimationFrame    ||
                window.oRequestAnimationFrame      ||
                window.msRequestAnimationFrame     ||
                function( callback ){
                    window.setTimeout(callback, 1000 / 60);
                };
// ...
//Применение
    var animloop = function(){ 
        draw_game(); //Метод рисования сцены
        if (game=='game'){  //Если состояние игры = game, тогда зацикливаем отрисовку игровой сцены
            requestAnimFrame(animloop);
        }
    };

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

Вся анимация базируется на времени с момента определенного события. Т.е. если у нас есть сцена на 2 секунды содержащая 120 кадров и частота отрисовки (FPS) 60 кадров в секунду, это не значит что метод рисования каждый раз должен увеличивать индекс кадра и рисовать все кадры последовательно.
Функция рисования {
номер_кадра ++;
функция_рисования_кадра(массив_спрайтов[номер_кадра]);
}

Это связано с тем, что компьютер не всегда успевает нарисовать кадр за отведенное время, например, если взять старые низкопроизводительные компьютеры, то они за секунду смогут отрисовывать только 20 кадров а не 60, и поэтому наша анимация будет идти медленее и за две секунды мы увидим только 1/3 от полной длительности. В таких случаях при выборе кадра нужно руководствоваться временем прошедшим с начала анимации сцены.
Например если сцена имеет длину 2 секунды и 120 кадров, то выбор кадра нужно делать по алгоритму. Рассчитаем длительность одного кадра в миллисекундах — для этого делим количество кадров на длительность сцены. 120 кадров /2000 миллисекунд = 16,(6), т.е. один кадр длится 16,(6) миллисекунд. Таким образом чтобы знать какой кадр нужно показывать в текущий момент времени, нужно просто поделить прошедшее от начала сцены анимации время на длительность одного кадра 16,(6), результат деления округлить до целого, т.к. нам нужно получить целый номер кадра. Таким образом функция рисования будет выглядеть вот так:
Функция рисования {
номер_кадра = целое((текущее_время_милисек — время_начала_сцены_милисек) / 16,666666);
номер_кадра ++;
функция_рисования_кадра(массив_спрайтов[номер_кадра]);
}

В итоге, даже на компьютере который успевает нарисовать 20 кадров в секунду, мы увидим анимацию целиком, но мы не увидим все кадры анимации, а только те, которые актуальны для момента времени в который происходила отрисовка сцены, что совершенно не критично, ведь даже при просмотре фильмов в формате mpeg частота кадров редко составляет больше 30.

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

Начал описывать массив анимаций, координат, поведения и понял что действительно не обойтись без библиотеки, в которой были бы реализованы события и последовательности. Решил всё же попробовать LibCanvas.

Прошло 6 часов, выполнял уроки, но постоянно приходилось искать документацию функций в google или на форумах. Остановил попытки, после ошибок вылавливаемых из консоли даже на простом коде из примеров. Невозможно найти документацию на необходимую функцию иногда очень удручает. Я пытался, честно. А изучать библиотеку ковыряясь в готовых больших, но работающих проектах, занятие неблагодарное, но всё же я взялся. В процессе ковыряния на протяжении еще нескольких часов всплыло несколько неприятных подробностей, которые окончательно поставили крест на потенциальном использовании LibCanvas в проекте Growing Crystals.
Однако есть и положительные моменты — удалось подсмотреть изящное описание поведения объектов и анимаций. На реализацию собственной системы анимации ушло менее 2х часов. Да, она гораздо примитивнее и менее функциональная чем библиотечная, но она полностью решает поставленные задачи. В данном случае спрайты успешно анимируются.

Реализация смены спрайтов оказалась примитивной до безобразия.

    var drawani = function (_anim){ //Вывод кадра анимации
        _anim.tp += tpldu; //Увеличение счетчика времени, между итерациями отрисовки
        if(_anim.tp>=_anim.tl)  _anim.tp = ((_anim.tp*1000) %  (_anim.tl*1000))/1000; //Не умею брать остаток от не целочисленного деления

        var i=0;
        while(_anim.tp>_anim.frames[i][1]) {
            i++;
        }

        ctx.drawImage(imganim, //картинка
                    _anim.frames[i][0]*spritesizex, //Смещение к номеру кадра
                    _anim.sprite*spritesizey,//Смещение к номеру спрайта
                    spritesizex,
                    spritesizey,
                    _anim.x, //X
                    _anim.y, //Y
                    spritesizex,
                    spritesizey
                );  // Рисуем сетку из клеток
    }

_anim — это объект, который содержит текущее время tp (time passed) в цикле анимации, массив [n][2] кадров анимации frames, каждый i-й элемент содержит номер кадра в файле спрайтов и его длительность, x,y — координаты указывающие положение объекта на canvas, tl (total lenght) — длительность анимации, sprite — номер строки в файле спрайтов (мне было удобнее все спрайты разместить в один файл размером 38Kb). Также есть пока не используемые type (loop/onetime) который говорит о количестве повторений. В итоге совершенно без оптимизации и буферного холста получается все те же стабильные 60 FPS даже на не очень мощном компьютере 2008 года.

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

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

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

Алгоритм графики оптимизирован на 0%, но при этом выдаёт на устаревшей железяке по 60 FPS, что не может не радовать.
К сожалению эта статья выходит спустя неделю после предыдущей, на самом деле не о чем писать, зато совершенно очевидно, было проделано достаточное количество работы, и я рад представить очередную версию игры, доведенную и улучшенную со всех сторон.

Играть в Growing Crystals v.007r1

Как всегда были подкручены стоимости и скорость роста кристаллов. Как всегда буду рад критике и советам.

Идеи в goods box

  • WebSocket
  • Создать Git для проекта
  • Улучшить управление: не прекращать движение, если клавиша направления не отпущена; учесть зоны кликов при масштабе
  • Создать Git для проекта

Идеи в trash box

  • На всех объектах принадлежащих игроку горит дополнительная голубая лампочка;
  • Если у игрока нету топлива, что его авиасёрф перестаёт работать и садится на землю, а в обычном режиме он слегка покачивается в воздухе и работают 4 реактивных двигателя по краям авиасёрфа;
  • Если на складе что-то есть, то у него мигает индикатор;
  • Апгрейды защитных башень;
  • Взрывающиеся при разрушении склады;
  • Артефакт увеличивающий скорость перемещения игрока;
  • Артефакт увеличивающий радиус действия игрока;
  • Предмет, который можно бросить на несколько экранов, чтобы посмотреть что там происходит;
  • Мины;
  • Кузница опыта, производящая специальное вещество, получаемое из сжигания кристаллов и необходимое для получения звания;
  • Звание — это предмет в инвентаре.
  • Объекты, расползающиеся по карте;
  • Декоративные мегаобъекты;
  • Наличие сверхдорогих объектов с особыми свойствами;
  • Список друзей, чтобы на них не действовала защитная башня;
  • Общие склады для друзей;
  • Шаринг в социальных сетях;
  • Переход на WebGL.

Краткий итог: Внедрена система спрайтовой анимации.

Сводка

Начало: 25 апреля 2014 года.
Команда: 1 человек.
Израсходовано: 96 + 20 = 116 чч.
Дней отдыха: 5+4 = 9
Средняя производительность: 116/15 = 7,73 чч/день

Описание игрового процесса

40/100

Расчет баланса

32/100

Игровая графика

30/100

Веб-клиент

35/100

Игровой сервер

35/100

ИТОГО

172/500

Играть в 0.07r1: Growing Crystals v.007r1

Продолжение: Growing Crystals vol 11. Переход на WebSocket и движение вперед