Our website is made possible by displaying online advertisements to our visitors. Please consider supporting us by disabling your ad blocker.

Animate Spritesheets in a Phaser Game

TwitterFacebookRedditLinkedInHacker News

When it comes to 2D game development, sprite animations are going to be a critical part of the game experience. No one wants to play a game with a static unappealing image that moves around on the screen. They are going to want vibrant animations that add a certain realism to the game-play experience, even with it being 2D.

There are a few ways to accomplish animations in game development. You could develop complex logic that swaps images on a sprite every time the render function updates, or you could work from a single spritesheet and iterate over the frames in that spritesheet.

One of the main benefits to spritesheets, beyond them being easy to animate in modern game development frameworks, is they are easy on the computing resources. Loading one image file is less resource intensive than loading several image files, and when it comes to smooth performance in games, how you manage your resources can make or break your game.

In this tutorial, we’re going to see how to animate 2D sprites in a Phaser 3.x game using simple spritesheets with JavaScript.

To get a better idea of what we hope to accomplish in this tutorial, take a look at the following animated image:

Phaser 3.x Spritesheet Animations

We have two things happening in the above image. We have a crude looking plane that looks like it is flying, hence the jet-stream behind it. We also have a crude looking explosion that triggers after a certain amount of time. These are both animations with several frames within the same spritesheet.

So what do we have to do to accomplish this effect?

Create a Phaser 3.x Project with HTML and JavaScript

Before we look at creating, using, and animating a spritesheet, let’s focus on getting started with some boilerplate Phaser 3.x code. On your computer, create a new directory with an index.html file in it. Within this index.html file, include the following code:

<!DOCTYPE html>
<html>
    <head>
        <script src="//cdn.jsdelivr.net/npm/phaser@3.24.1/dist/phaser.min.js"></script>
    </head>
    <body>
        <div id="game"></div>
        <script>

            const phaserConfig = {
                type: Phaser.AUTO,
                parent: "game",
                width: 1280,
                height: 720,
                scene: {
                    init: initScene,
                    preload: preloadScene,
                    create: createScene,
                    update: updateScene
                }
            };

            const game = new Phaser.Game(phaserConfig);

            function initScene() {}
            function preloadScene() {}
            function createScene() {}
            function updateScene() {}

        </script>
    </body>
</html>

The above HTML markup and JavaScript code will create a canvas for rendering our game. Because we are using Phaser.AUTO, we might not actually be using a canvas, but instead WebGL, but I’m going to refer to it as a canvas.

For the rest of this tutorial, we’re going to be spending our time in the preloadScene and createScene functions. However, the initScene and updateScene functions would probably be used in a more extensive tutorial.

Defining Animation Frames and Animating a Spritesheet

With Phaser configured and ready to go, we can work towards getting our animations working. There are a few ways to make a spritesheet for game development, and a few different ways to format a spritesheet.

To make life easier and keep us consistent, we’re going to be using the following spritesheet:

Airplane Spritesheet for Game Development

There are a few things worth mentioning regarding the above spritesheet.

In the above spritesheet, every frame has the same dimensions and the same padding. In other words, if I were to break this image into six images based on the frame, they would have the same size and same position. I made this spritesheet with TexturePacker, but it can easily be reproduced with a general image editing tool like Affinity Photo or Adobe Photoshop. In a later tutorial we’re going to explore sprite atlases, where the spritesheets are compressed and of not the same size per frame.

With the spritesheet under control, lets work towards adding it to Phaser. Copy the image file to the same directory as your index.html file. I’m calling the image file plane.png, but feel free to call it whatever you want.

Within the preloadScene function of the index.html file add the following line:

function preloadScene() {
    this.load.spritesheet("plane", "plane.png", { frameWidth: 512, frameHeight: 512 });
}

The preloadScene is where you should load all of your media assets prior to the game being created. In this example, we’re loading our spritesheet and naming it plane for future use throughout the game. The spritesheet name does not need to match the actual file name. It is important we define the frameWidth and frameHeight as it tells our animator how large each frame is in our spritesheet.

Within the createScene function, now we can define the animations. Take a look at the following:

function createScene() {
    this.anims.create({
        key: "fly",
        frameRate: 7,
        frames: this.anims.generateFrameNumbers("plane", { start: 3, end: 5 }),
        repeat: -1
    });

    this.anims.create({
        key: "explode",
        frameRate: 7,
        frames: this.anims.generateFrameNumbers("plane", { start: 0, end: 2 }),
        repeat: 2
    });
}

In the above function we have two different animations using the same spritesheet media asset. Let’s look at the animation that I’m calling fly in our code:

this.anims.create({
    key: "fly",
    frameRate: 7,
    frames: this.anims.generateFrameNumbers("plane", { start: 3, end: 5 }),
    repeat: -1
});

For this particular animation, we are cycling through our frames at a rate of seven frames per second. The frames in our spritesheet for this particular animation are defined by the start and end fields. If we look back at our image, we know that the actual plane is the 3rd through 5th image if looking at the spritesheet from left to right, top to bottom. The index of these frames starts at zero and not one. For this particular animation we are repeating until we specifically tell it to stop.

Now let’s revisit the other animation:

this.anims.create({
    key: "explode",
    frameRate: 7,
    frames: this.anims.generateFrameNumbers("plane", { start: 0, end: 2 }),
    repeat: 2
});

Rather than repeating the animation until we tell it to stop, this animation will repeat twice and then stop.

It is important to note that stopping an animation does not remove the sprite. We’ll get to that later. So this means when an animation stops, the sprite will stay there appearing frozen on the screen.

With two different animations defined, we need to associate them to an actual sprite game object. To do this, let’s create the following variable to represent our plane:

var plane;

The above variable should exist outside of any scene functions so that way it can be used throughout the game with the correct scope.

Inside the createScene function, below the animation definitions, let’s initialize our sprite and start the fly animation:

function createScene() {

    // Animation definitions here ...

    plane = this.add.sprite(640, 360, "plane");
    plane.play("fly");

}

In the above code, the sprite is using the plane spritesheet and the fly animation associated to that spritesheet. Given the dimensions of our game canvas, the sprite is centered.

This is great so far, but when we run our game we only see a plane flying forever. We need to see how to change animations and know when animations end.

Changing Animations and Reacting to Animation Events on a Sprite

Being able to change an animation is important because eventually you’ll want to respond to interactions within the game. For example, if our plane were to crash, we’d probably want it to explode, hence the explode animation. This isn’t the only scenario, but it works for our example.

Rather than complicating this example with collisions and things like that, let’s change the animation based on a timer. Within the index.html file and the createScene function, add the following:

function createScene() {

    // Animation definitions here ...

    plane = this.add.sprite(640, 360, "plane");
    plane.play("fly");

    setTimeout(() => {
        plane.play("explode");
    }, 3000);

}

In the above code, we’re using a setTimeout in JavaScript to wait three seconds before playing a different animation on our sprite. If we wanted to do things in the spirit of Phaser, we could use a special class for this like the following:

function createScene() {

    // Animation definitions here ...

    plane = this.add.sprite(640, 360, "plane");
    plane.play("fly");

    this.time.addEvent({
        delay: 3000,
        callback: () => {
            plane.play("explode");
        }
    });

}

Both should accomplish the same thing. However, because of frame rate considerations, the this.time.addEvent may be the proper way to go as it might do calculations that the setTimeout does not.

So we know how to change sprite animations now. The next step is to figure out when an animation ends and potentially even remove the sprite. To do this, we could take a look at the following:

plane.once(Phaser.Animations.Events.SPRITE_ANIMATION_COMPLETE, () => {
    plane.destroy();
});

The above code says that once the animation stops, the plane should be destroyed, which means it is removed from the game.

To put things into perspective, our index.html file might look like the following as of now:

<!DOCTYPE html>
<html>
    <head>
        <script src="//cdn.jsdelivr.net/npm/phaser@3.24.1/dist/phaser.min.js"></script>
    </head>
    <body>
        <div id="game"></div>
        <script>

            const phaserConfig = {
                type: Phaser.AUTO,
                parent: "game",
                width: 1280,
                height: 720,
                scene: {
                    init: initScene,
                    preload: preloadScene,
                    create: createScene,
                    update: updateScene
                }
            };

            const game = new Phaser.Game(phaserConfig);

            var plane;

            function initScene() { }

            function preloadScene() {
                this.load.spritesheet("plane", "plane.png", { frameWidth: 512, frameHeight: 512 });
            }

            function createScene() {

                this.anims.create({
                    key: "fly",
                    frameRate: 7,
                    frames: this.anims.generateFrameNumbers("plane", { start: 3, end: 5 }),
                    repeat: -1
                });

                this.anims.create({
                    key: "explode",
                    frameRate: 7,
                    frames: this.anims.generateFrameNumbers("plane", { start: 0, end: 2 }),
                    repeat: 2
                });

                plane = this.add.sprite(640, 360, "plane");
                plane.play("fly");

                this.time.addEvent({
                    delay: 3000,
                    callback: () => {
                        plane.play("explode");
                        plane.once(Phaser.Animations.Events.SPRITE_ANIMATION_COMPLETE, () => {
                            plane.destroy();
                        });
                    }
                });

            }

            function updateScene() {}

        </script>
    </body>
</html>

Just to reiterate, we probably could have used setTimeout, but since functionality in Phaser 3.x exists, it probably makes sense to use that instead.

There are other animation events, but for this example knowing when to remove our sprite makes SPRITE_ANIMATION_COMPLETE a great choice.

Conclusion

You just saw how to animate a spritesheet in a Phaser 3.x powered game. Just a few points to reiterate on regarding the approach that we used.

  • We used a uniformly spaced and sized sheet of sprite images that have not been optimized.
  • We used a spritesheet, not a sprite atlas.

In a future tutorial we’ll explore the sprite atlas option for more optimized spritesheets and animations.

While you can create a spritesheet using pretty much any image editing software, if you want to make it easier on yourself, you can consider TexturePacker which is what I used. Essentially you just drop some images into the software and it generates a spritesheet for you.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in C#, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Unity. Nic writes about his development experiences related to making web and mobile development easier to understand.