Статьи

Спрайт Анимация: Босс Китти

Это продолжение учебника, начатого в Sprite Animations: Vampire Kitty Lives .

Эта статья закончилась обещанием, что мы сделаем некоторые улучшения.

requestAnimFrame

setTimeout хорош, и он работает практически во всех браузерах, но есть еще лучший метод, requestAnimFrame.

requestAnimFrame основном действует как setTimeout , но браузер знает, что вы визуализируете фрейм, поэтому он может оптимизировать цикл отрисовки, а также то, как это взаимодействует с остальной частью перекомпоновки страницы. Он даже обнаружит, видна ли вкладка, и не потрудится нарисовать ее, если она скрыта, что экономит батарею (и да, циклическое вращение веб-игр со скоростью 60 кадров в секунду сожжет батарею). Под капотом браузеры также получают возможность оптимизировать другие загадочные способы, о которых они нам мало рассказывают. По моему опыту с более высокими нагрузками на раму (особенно с сотнями спрайтов) производительность может значительно возрасти; особенно на последних сборках браузера.

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

Призыв к использованию requestAnimFrame различен в разных браузерах, поэтому стандартное регулирование ( спасибо Полу Айришу ) для определения этого:

  window.requestAnimFrame = (function () {
     вернуть window.requestAnimationFrame ||
             window.webkitRequestAnimationFrame ||
             window.mozRequestAnimationFrame ||
             window.oRequestAnimationFrame ||
             window.msRequestAnimationFrame ||
             function (callback) {
               window.setTimeout (обратный вызов, 1000/60);
             };
 }) (); 

Также имеется встроенный setTimeout обычному старому setTimeout если поддержка requestAnimFrame недоступна.

Затем вам нужно изменить метод обновления, чтобы повторно сделать запрос:

  функция update () {
     requestAnimFrame (обновление);
     перерисовывать ();
     кадр ++;
     if (frame> = 6) frame = 0;
 } 

Вызов requestAnimFrame до того, как вы на самом деле выполняете рендеринг / обновление, дает более согласованный результат.

Кстати, когда я впервые начал использовать requestAnimFrame я искал, как он будет рассчитан, но ничего не нашел. Это потому что это не так. Нет никакого эквивалента установке задержки MS, которую вы найдете с помощью setTimeout , что означает, что вы фактически не можете контролировать частоту кадров. Просто делайте свою работу, и пусть браузер позаботится обо всем остальном.

Еще одна вещь, на которую следует обратить внимание: если вы используете requestAnimFrame из своего собственного замыкания, вам нужно будет выполнить внутреннюю упаковку, чтобы вызвать его, например:

  my.requestAnimFrame = (function () {
     var func = window.requestAnimationFrame ||
         window.webkitRequestAnimationFrame ||
         window.mozRequestAnimationFrame ||
         window.oRequestAnimationFrame ||
         window.msRequestAnimationFrame ||
         функция (обратный вызов, элемент)
         {
             window.setTimeout (обратный вызов, 1000 / this.fps);
         };

     // применяем к нашему глобальному окну, чтобы избежать недопустимых вызовов (это нативная функция) return function (callback, element) {
         func.apply (window, [callback, element]);
     };
 }) (); 

Анимация по времени

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

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

Чтобы продвинуть котенка с регулируемой скоростью, нам нужно отследить, сколько времени прошло, а затем продвинуть кадры в соответствии с временем, выделенным каждому. Основы этого:

  1. Установите скорость анимации в терминах кадров в секунду. (MsPerFrame)
  2. По ходу игры выясните, сколько времени прошло с момента последнего кадра (дельта).
  3. Если прошло достаточно времени для перемещения кадра анимации вперед, то передвиньте кадр и установите накопленную дельту в 0.
  4. Если не прошло достаточно времени, запомните (накапливайте) дельта-время (acDelta).

Вот это в нашем коде:

  var frame = 0;
 var lastUpdateTime = 0;
 var acDelta = 0;
 var msPerFrame = 100;

 функция update () {
     requestAnimFrame (обновление);

     var delta = Date.now () - lastUpdateTime;
     if (acDelta> msPerFrame)
     {
         acDelta = 0;
         перерисовывать ();
         кадр ++;
         if (frame> = 6) frame = 0;
     } еще
     {
         acDelta + = delta;
     }

     lastUpdateTime = Date.now ();
 } 

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

Масштабирование и вращение

Вы также можете использовать 2D-холст для выполнения различных операций с изображением при его отображении, таких как вращение и масштабирование.

Например, давайте сделаем несколько котят, уменьшив изображение наполовину. Вы можете сделать это, добавив ctx.scale(0.5, 0.5) к вызову draw:

  функция redraw ()
 {
     ctx.fillStyle = '# 000000';
     ctx.fillRect (0, 0, canvas.width, canvas.height);
     if (imageReady)
     {
         ctx.save ();
         ctx.scale (0.5,0.5);
         ctx.drawImage (img, frame * 96, 0, 96, 54,
                       canvas.width / 2 - 48, canvas.height / 2 - 48, 96, 54);
         ctx.restore ();
     }
 } 

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

Масштабирование также работает с использованием отрицательных значений, чтобы инвертировать изображение. Если вы измените значения шкалы с (0,5, 0,5) на (-1, 1), изображение кошки будет перевернуто горизонтально, поэтому он будет бегать в противоположном направлении. Обратите внимание, что translate используется, чтобы перевернуть начальную позицию X, чтобы сместить реверс изображения.

  function redraw () {
     ctx.fillStyle = '# 000000';
     ctx.fillRect (0, 0, canvas.width, canvas.height);
     if (imageReady) {
         ctx.save ();
         ctx.translate (img.width, 0);
         ctx.scale (-1, 1);
         ctx.drawImage (img, frame * 96, 0, 96, 54,
                       canvas.width / 2 - 48, canvas.height / 2 - 48, 96, 54);
         ctx.restore ();
     }
 } 

Вы можете использовать вращение, чтобы сделать (дух) вращение. Вот котенок карабкается по стенам:

  ctx.rotate (270 * Math.PI / 180);

 ctx.drawImage (img, frame * 96, 0, 96, 54,
                - (canvas.width / 2 - 48), (canvas.height / 2 - 48), 96, 54); 

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

Такой талантливый котенок (хотя вампиры должны уметь взбираться на стены, верно?)

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

Предварительная визуализация

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

В HTML5 вам нужно рисовать на отдельном невидимом холсте, а затем вместо рисования изображения вы рисуете другой холст на его месте.

Вот пример функции, которая представляет котенка как перевернутое изображение.

  var reverseCanvas = null;

 function prerender () {
     reverseCanvas = document.createElement ('canvas');
     reverseCanvas.width = img.width;
     reverseCanvas.height = img.height;
     var rctx = reverseCanvas.getContext ("2d");
     rctx.save ();
     rctx.translate (img.width, 0);
     rctx.scale (-1, 1);
     rctx.drawImage (img, 0, 0);
     rctx.restore ();
 } 

Обратите внимание, что объект Canvas создан, но не добавлен в DOM, поэтому он не будет отображаться. Высота и ширина устанавливаются на исходную таблицу спрайтов, а затем исходное изображение рисуется с использованием 2D-контекста буфера рендеринга.

Для настройки prerender вы можете вызвать его из загруженной функции.

  функция загружена () {
     imageReady = true;
     PreRender ();
     requestAnimFrame (обновление);
 } 

Затем, когда вы делаете обычный вызов перерисовки, используйте reverseCanvas вместо оригинального:

  function redraw () {
     ctx.fillStyle = '# 000000';
     ctx.fillRect (0, 0, canvas.width, canvas.height);
     if (imageReady) {
         ctx.save ();
         ctx.drawImage (reverseCanvas, frame * 96, 0, 96, 96, 
                       (canvas.width / 2 - 48), (canvas.height / 2 - 48), 96, 96);
         ctx.restore ();
     }
 } 

К сожалению, когда мы перевернули изображение, анимация теперь также воспроизводится в обратном направлении, поэтому вам также необходимо изменить последовательность анимации:

  функция update () {
     requestAnimFrame (обновление);
     var delta = Date.now () - lastUpdateTime;
     if (acDelta> msPerFrame) {
         acDelta = 0;
         перерисовывать ();
         Рамка--;
         if (frame <0) frame = 5;
     } еще {
         acDelta + = delta;
     }
     lastUpdateTime = Date.now ();
 } 

Если вам нужно, вы можете преобразовать холст в изображение, установив его источник для использования URL-адреса данных, содержащего закодированные данные изображения. У Canvas есть способ сделать это, так что это просто:

  newImage = новое изображение ();

 newImage.src = reverseCanvas.toDataURL ("image / png"); 

Еще одна приятная манипуляция с изображениями — играть с фактическими данными пикселей. Элементы HTML5 canvas отображают данные изображения в виде массива пикселей в формате RGBA. Вы можете получить доступ к массиву данных из контекста, используя:

  var imageData = ctx.getImageData (0, 0, ширина, высота); 

Который будет возвращать структуру ImageData, содержащую ширину, высоту и элементы данных. Элемент данных — это массив пикселей, которые мы ищем.

Массив данных состоит из всех пикселей, причем каждый пиксель представлен 4 записями, красным, зеленым, синим и альфа-уровнем, все в диапазоне от 0 до 255. Таким образом, изображение шириной 512 и высотой 512 приведет к массив, содержащий 1048576 элементов — 512 × 512 равен 262 144 пикселям, умноженный на 4 записи на пиксель.

Используя этот массив данных, вот пример, где конкретный красный компонент изображения увеличен, в то время как красный и синий компоненты уменьшены, создав таким образом нашего монстра 2-го уровня, hell-spawn-demon-kitty.

  function prerender () {
     reverseCanvas = document.createElement ('canvas');
     reverseCanvas.width = img.width;
     reverseCanvas.height = img.height;
     var rctx = reverseCanvas.getContext ("2d");
     rctx.save ();
     rctx.translate (img.width, 0);
     rctx.scale (-1, 1);
     rctx.drawImage (img, 0, 0);
     // изменить цвета
     var imageData = rctx.getImageData (0, 0, reverseCanvas.width, reverseCanvas.height);
     для (var i = 0, il = imageData.data.length; i <il; i + = 4) {
         if (imageData.data [i]! = 0) imageData.data [i] = imageData.data [i] + 100;  // красный
         if (imageData.data [i + 1]! = 0) imageData.data [i + 1] = imageData.data [i + 1] - 50;  // зеленый
         if (imageData.data [i + 1]! = 0) imageData.data [i + 2] = imageData.data [i + 2] - 50;  // синий
     }
     rctx.putImageData (imageData, 0, 0);
     rctx.restore ();
 } 

Цикл for выполняет итерацию массива данных по четыре шага, каждый раз изменяя три основных цвета. Четвертый канал, альфа, оставлен как есть, но, если хотите, вы можете использовать его для изменения прозрачности определенных пикселей. (Примечание: в приведенном ниже примере JSFiddle мы используем dataURL для данных изображения, особенно во избежание междоменных проблем с прямыми манипуляциями с пикселями. Вам не нужно делать это на вашем собственном сервере.)

Вот наш котенок 2 уровня:

Поскольку манипулирование изображением с использованием массива пикселей требует итерации по всем элементам — в случае с Hell Kitty, это более миллиона раз — вы должны держать вещи довольно оптимизированными: делать предварительные расчеты, насколько это возможно, не создавать переменные / объекты и пропускать пикселей как можно больше.

Вывод

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

В качестве примера, я недавно использовал эти приемы в одной из демонстрационных игр Playcraft , 2D-шутере с 4-сторонней прокруткой. Художники создавали только один кадр каждого корабля (игрока и вражеского истребителя), который я затем поворачивал и делал в зависимости от того, сколько градусов, и, следовательно, насколько плавно мы хотели, чтобы корабли поворачивались. Я мог бы настроить количество углов в зависимости от типа корабля во время выполнения — по умолчанию корабли игрока отображаются с 36 углами поворота (очень плавно), в то время как вражеский и противник — только под 16 углами (прерывистый). Я также добавил опцию, позволяющую игрокам на более мощных компьютерах выбирать увеличение углов сглаживания до 72 по всему кругу (супер плавное). Кроме того, я динамически перекрашиваю эмблемы и маркировку на кораблях (крутые большие полосы вдоль крыльев) в соответствии с командой, в которой вы находитесь. Это опять-таки экономит на рендеринге и ресурсах, но также позволяет динамически настраивать цвета корабля на основе выбранного пользователем цвета команды.

Для получения дополнительной информации о том, что вы можете сделать с canvas, обратитесь к API Canvas Element .

Эта статья первоначально появилась на BuildNewGames.com , совместная работа команд в Bocoup и Internet Explorer.