Написать пост

Введение в spritesheet анимацию

Аватар Типичный программист

Анимация, основанная на spritesheet’ах, используется в играх довольно продолжительное время. В том числе в таких популярных играх, как Legend of Zelda: A Link to the Past или Cut the Rope. В этой статье мы поговорим о том, как работает такая анимация и как ее запрограммировать. В качестве языка программирования будем использовать JavaScript, но вы можете выбрать любой удобный для вас.

Прежде чем говорить о том, как программировать spritesheet анимацию, нам следует определить три термина: анимация, спрайт и, собственно, spritesheet.

Анимация

В далеком 1872 году английскому и американскому фотографу Эдварду Мейбриджу было поручено узнать, поднимает ли лошадь все свои 4 ноги во время бега. Мейбридж для этого использовал несколько камер, которые делали снимок, когда лошадь с наездником пробегала мимо. В итоге фотограф получил 16 изображений лошади. Кстати говоря, в парочке из них все четыре ноги лошади действительно находятся в воздухе.

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

Процесс быстрой смены изображений для создания иллюзии движения назвали анимацией.

Спрайт

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

Введение в spritesheet анимацию 3

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

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

Spritesheet’ы

Когда вы кладете множество спрайтов в одно изображение, вы получаете spritesheet.

Введение в spritesheet анимацию 4

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

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

Части spritesheet’а

Сам по себе spritesheet состоит из двух частей: кадров и циклов.

Кадр является обычным изображением (или спрайтом). На примере лошади Мейбриджа: каждое изображение лошади во всей анимации является кадром.

Если кадры поставить в правильном порядке, то получится непрерывное движение. Это и есть цикл.

Программирование spritesheet анимации

Существует 3 этапа создания spritesheet анимации:

  1. Создание изображения.
  2. Обновление изображения для каждого кадра анимации.
  3. Рисование анимации на экране.

Создание изображения

Начнем с создания функции (или класса), который будет обрабатывать нашу spritesheet анимацию. Эта функция будет создавать изображение и установит его путь для того, чтобы мы могли использовать его для дальнейшего рисования на экране.

			function SpriteSheet(path, frameWidth, frameHeight) {
 
   var image = new Image();
   var framesPerRow;
 
   // вычисление количества кадров в строке после загрузки изображения
   var self = this;
   image.onload = function() {
      framesPerRow = Math.floor(image.width / frameWidth);
   };
 
   image.src = path;
}
		

Так как spritesheet’ы могут иметь разные размеры, то, зная размер одного кадра, мы будем знать количество кадров в строке и столбце.

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

Обновление изображений

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

Введение в spritesheet анимацию 5

Менять текущий кадр на следующий нужно не моментально, а через какое-то время. То есть при показе одного из кадров требуется сделать определенную задержку перед тем, как поменять его.

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

Избежать всплесков запросто: мы не будем отрисовывать эти пустые изображения:

			function SpriteSheet(path, frameWidth, frameHeight, frameSpeed, endFrame) {
 
  // код удален для краткости
 
  var currentFrame = 0;  // текущий кадр для отрисовки
  var counter = 0;       // счетчик ожидания
 
  // обновление анимации
  this.update = function() {
 
    // если подошло время смены кадра, то меняем
    if (counter == (frameSpeed - 1))
      currentFrame = (currentFrame + 1) % endFrame;
 
    // обновление счетчика ожидания
    counter = (counter + 1) % frameSpeed;
    }
};
		

При использовании операции по взятию остатка от деления (%) для currentFrame мы создаем непрерывный цикл чисел от 0 до endFrame. Таким образом мы воспроизводим только те кадры, изображения для которых у нас есть в spritesheet’е.

Рисование анимации

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

Используя найденные величины мы можем найти координаты той рамки, внутри которой и находится искомый кадр:

			// отрисовка текущего кадра
   this.draw = function(x, y) {
      // вычисление столбика и строчки с нужным кадром
      var row = Math.floor(currentFrame / framesPerRow);
      var col = Math.floor(currentFrame % framesPerRow);
 
      ctx.drawImage(
         image,
         col * frameWidth, row * frameHeight,
         frameWidth, frameHeight,
         x, y,
         frameWidth, frameHeight);
  };
}
		

Теперь мы готовы создать анимацию:

			spritesheet = new SpriteSheet('Walk_Cycle_Image.png', 125, 125, 3, 16);
 
function animate() {
   requestAnimFrame( animate );
   ctx.clearRect(0, 0, 150, 150);
 
   spritesheet.update();
 
   spritesheet.draw(12.5, 12.5);
}
		

Несколько циклов в одном spritesheet

Приведенный выше код будет работать для любого spritesheet’а, содержащего ровно один цикл. Тем не менее, существуют spritesheet’ы с несколькими циклами (анимациями).

Для работы с несколькими анимациями в одном spritesheet’е, нам понадобится изменить наш принцип работы.

Создание изображения

Храним информацию об изображении и его размерах:

			function SpriteSheet(path, frameWidth, frameHeight) {
  this.image = new Image();
  this.frameWidth = frameWidth;
  this.frameHeight = frameHeight;
 
  // вычисление количества кадров в строке после загрузки изображения
  var self = this;
  this.image.onload = function() {
    self.framesPerRow = Math.floor(self.image.width / self.frameWidth);
  };
 
  this.image.src = path;
}
		

Обновление и рисование анимации

Функция Animation() будет отвечать за обновление и отрисовку картинки:

			function Animation(spritesheet, frameSpeed, startFrame, endFrame) {
 
  var animationSequence = [];  // array holding the order of the animation
  var currentFrame = 0;        // the current frame to draw
  var counter = 0;             // keep track of frame rate
 
  // создание последовательности из номеров кадров анимации
  for (var frameNumber = startFrame; frameNumber <= endFrame; frameNumber++)
    animationSequence.push(frameNumber);
 
  // обновление анимации
  this.update = function() {
 
    // если подошло время смены кадра, то меняем
    if (counter == (frameSpeed - 1))
      currentFrame = (currentFrame + 1) % animationSequence.length;
 
    // обновление счетчика ожидания
    counter = (counter + 1) % frameSpeed;
  };
 
  // отрисовка текущего кадра
  this.draw = function(x, y) {
    // вычисление количества кадров в строке после загрузки изображения
    var row = Math.floor(animationSequence[currentFrame] / spritesheet.framesPerRow);
    var col = Math.floor(animationSequence[currentFrame] % spritesheet.framesPerRow);
 
    ctx.drawImage(
      spritesheet.image,
      col * spritesheet.frameWidth, row * spritesheet.frameHeight,
      spritesheet.frameWidth, spritesheet.frameHeight,
      x, y,
      spritesheet.frameWidth, spritesheet.frameHeight);
  };
}
 
spritesheet = new SpriteSheet('Walk_Cycle_Image.png', 125, 125);
walk = new Animation(spritesheet, 3, 0, 15);
 
function animate() {
   requestAnimFrame( animate );
   ctx.clearRect(0, 0, 150, 150);
 
   walk.update();
 
   walk.draw(12.5, 12.5);
}
		

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

Послесловие

Мы написали простенькую игру-раннер, используя ту самую анимацию, о которой говорилось в статье. Исходный код, приведенный в статье, вы можете увидеть, пройдя по этой ссылке.

Перевод статьи «An Introduction to Spritesheet Animation»

Следите за новыми постами
Следите за новыми постами по любимым темам
15К открытий15К показов