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.
This tutorial is part of a special collaboration between an artist, an animator and a gamedev!
Before we can begin talking about how to code a spritesheet animation, we should first define a few terms: animation, sprite, and spritesheet.
Additional resources you may find useful:
- Animating With Asset Sheets: An Alternative to Blitting
- 10 Great Full Game Sprite Sheets From GraphicRiver
- Creating Sprite Sheets in Five Minutes With Texture Packer
Animation
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 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.
The process of changing images in quick succession to give the illusion of movement is called animation.
Sprite
A sprite is a single graphic image that is incorporated into a larger scene so that it appears to be part of the scene.
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.
Spritesheet
When you put many sprites into a single image, you get a spritesheet.
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.
Parts of a Spritesheet
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).
Coding Spritesheet Animations
There are three parts to coding a spritesheet animation:
- Creating the image
- Updating the image to each frame of the animation
- Drawing the frame to the screen
Creating the Image
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.
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.Updating the Image
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.
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.
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 the Image
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:
// 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:
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); }
Multiple Cycles in One Spritesheet
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.
Creating the Image
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.
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; }
Updating and Drawing the Image
An animation will be in charge of updating and drawing the spritesheet.
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.
Putting the Spritesheet to Use
With the animation ready to handle any spritesheet, we can use it to make a simple Canabalt-style infinite runner:
You can find the full source code for this in our GitHub repo. What’s your high score?