An Introduction to Spritesheet Animation

Spritesheets have been used in games for a long time. Classic games such as Legend of Zelda: A Link to the Past and even modern games like Cut the Rope have used them. In this article, we’ll talk about what spritesheet animation is and how to code it, and we’ll also demonstrate how they can be used in a small game. I’ll be using JavaScript for the code, but you should be able to follow along in any language.

Related Posts

This tutorial is part of a special collaboration between an artist, an animator and a gamedev!

Hit Space to jump.

Before we can begin talking about how to code a spritesheet animation, we should first define a few terms: animation, sprite, and spritesheet.

Back in 1872, Eadweard Muybridge was commissioned to prove whether a horse lifted all four legs off the ground at once when it ran. To do so, he set up a series of cameras along a track and took pictures in quick succession as a horse ran by. This process allowed him to capture 16 pictures of the horse’s run. In one of the pictures, the horse did indeed have all four legs off the ground.

Muybridge race horse image sheet
Muybridge’s “The Horse in Motion” photos with the first photo removed.

Muybridge later repeated the experiment and placed each photo onto a device that could project the photos in rapid succession to give the illusion of the horse running, creating the first movie projector.

Muybridge race horse animated

The process of changing images in quick succession to give the illusion of movement is called animation.

A sprite is a single graphic image that is incorporated into a larger scene so that it appears to be part of the scene.

An example of a sprite

Sprites are a popular way to create large, complex scenes as you can manipulate each sprite separately from the rest of the scene. This allows for greater control over how the scene is rendered, as well as over how the players can interact with the scene.

It is not uncommon for games to have tens to hundreds of sprites. Loading each of these as an individual image would consume a lot of memory and processing power. To help manage sprites and avoid using so many images, many games use spritesheets.

When you put many sprites into a single image, you get a spritesheet.

An example of a shritesheet

Spritesheets are used to speed up the process of displaying images to the screen; It is much faster to fetch one image and display only a part of that image than it is to fetch many images and display them.

Spritesheet animation is nothing more than taking a spritesheet and changing which sprite is rendered in quick succession to give the illusion of movement, much like a film projector displaying a movie.

Spritesheets are made up of two parts: frames and cycles

A frame is a single image (or sprite) from the spritesheet. Going back to the Muybridge’s horse example, each picture of the horse in the image would be a frame.

When the frames are put in an order that creates a continuous movement, it creates a cycle.

Putting the photos of the horse in the order that they were taken produces a “run” cycle since the horse is running (as opposed to a “walk” or “idle” cycle).

There are three parts to coding a spritesheet animation:

  1. Creating the image
  2. Updating the image to each frame of the animation
  3. Drawing the frame to the screen

We’ll start by creating the function (or class) that will handle our spritesheet animation. This function will create the image and set its path so that we can use it to draw.

01
02
03
04
05
06
07
08
09
10
11
12
13
function SpriteSheet(path, frameWidth, frameHeight) {
   var image = new Image();
   var framesPerRow;
   // calculate the number of frames in a row after the image loads
   var self = this;
   image.onload = function() {
      framesPerRow = Math.floor(image.width / frameWidth);
   };
   image.src = path;
}

Since different spritesheets can have different frame sizes, we’ll need to pass the frame width and height so that we can accurately calculate how many frames are in a row and column of the image. We’ll use this information later to draw the animation to the screen.

It is important that each frame of the spritesheet is the same width and height; otherwise, drawing the animation to the screen is very difficult.

To update the spritesheet animation, all we have to do is change which frame we will draw. Below is the spritesheet divided into each of its frames and numbered.

spritesheet divided into frames and numbered

At every frame of the game, we’ll update the spritesheet. However, we don’t want the animation to switch to the next frame every frame, so we need to tell our spritesheet how many frames to wait before transitioning.

It is important to note that not every spritesheet has a sprite in every available frame (such as the image of Muybridge’s “The Horse in Motion”). If we were to try to animate our spritesheet with an empty frame, there would be a blip in the animation every time the blank frame is drawn to the screen.

To compensate for this, we will also tell the spritesheet what the last frame number is, so that we don’t animate empty frames.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
function SpriteSheet(path, frameWidth, frameHeight, frameSpeed, endFrame) {
  // code removed for brevity
  var currentFrame = 0;  // the current frame to draw
  var counter = 0;       // keep track of frame rate
  // Update the animation
  this.update = function() {
    // update to the next frame if it is time
    if (counter == (frameSpeed - 1))
      currentFrame = (currentFrame + 1) % endFrame;
    // update the counter
    counter = (counter + 1) % frameSpeed;
    }
  };

By using the modulo operator (%) for the currentFrame, we can create a continuous loop—every time the endFrame is reached, the currentFrame will revert back to 0, thus looping the animation.

The modulo operator for the counter prevents integer overflow.

Drawing an image from a spritesheet works in exactly the same way as drawing an image from a tile map.

We calculate the row of the image we want to draw by taking the modulo of the current frame and the number of frames per row. We calculate the column by dividing the current frame by the number of frames per row.

Using this row and column, we can then calculate the coordinates of the frame to draw by multiplying them by frameWidth and frameHeight, respectively:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
   // Draw the current frame
   this.draw = function(x, y) {
      // get the row and col of the frame
      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);
  };
}

With the spritesheet function in place, we can now use it to create a spritesheet animation:

01
02
03
04
05
06
07
08
09
10
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);
}

The above code will work for any spritesheet containing one cycle. However, it is not uncommon for a spritesheet to hold multiple cycles, meaning that there will be multiple animations in a single spritesheet.

We will need to change how our spritesheet works in order to handle multiple animations from a single spritesheet.

Since the image remains the same between animations, we will divide our spritesheet into two functions: one for the image and one for each animation from the spritesheet.

A spritesheet will hold the information about the image and the frame sizes.

01
02
03
04
05
06
07
08
09
10
11
12
13
function SpriteSheet(path, frameWidth, frameHeight) {
  this.image = new Image();
  this.frameWidth = frameWidth;
  this.frameHeight = frameHeight;
  // calculate the number of frames in a row after the image loads
  var self = this;
  this.image.onload = function() {
    self.framesPerRow = Math.floor(self.image.width / self.frameWidth);
  };
  this.image.src = path;
}

An animation will be in charge of updating and drawing the spritesheet.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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
  // create the sequence of frame numbers for the animation
  for (var frameNumber = startFrame; frameNumber <= endFrame; frameNumber++)
    animationSequence.push(frameNumber);
  // Update the animation
  this.update = function() {
    // update to the next frame if it is time
    if (counter == (frameSpeed - 1))
      currentFrame = (currentFrame + 1) % animationSequence.length;
    // update the counter
    counter = (counter + 1) % frameSpeed;
  };
  // draw the current frame
  this.draw = function(x, y) {
    // get the row and col of the frame
    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);
}

Since the spritesheet contains more frames than any single animation will need, we’ll need to know which frame number to start and end the animation. Using this information, we’ll create an array of frame numbers so that we can use currentFrame to access the correct frame number.

With the animation ready to handle any spritesheet, we can use it to make a simple Canabalt-style infinite runner:

Hit Space to jump.

You can find the full source code for this in our GitHub repo. What’s your high score?