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

Animate a Compressed Sprite Atlas in a Phaser Game

TwitterFacebookRedditLinkedInHacker News

You might recall that I recently wrote a tutorial titled Animate Spritesheets in a Phaser Game where I created two different sprite animations from a single spritesheet. The process was quite simple, but not very optimized. In this previous tutorial, the spritesheet had each frame being composed of the same resolution image. This allowed us to iterate over the frames kind of like an array. However, because each frame in the spritesheet was the same size, we had a lot of empty space padded between each of the frames. Wasted space in an image means we’re now working with a larger image in resolution and in file size.

We can improve upon this process with what’s known as a sprite atlas.

A sprite atlas is still a spritesheet. We have numerous frames in a single image file, the difference being that all of the padded white-space has been removed. Since the images are of different sizes and positions, we can no longer iterate over them like we did in the previous example because we don’t know where one frame ends and another begins. To know the information of each frame, we need an atlas which is like a map.

In this tutorial we’re going to see how to take a sprite atlas, composed of a compressed spritesheet and map information, and animate it within a Phaser 3.x game.

To get an idea of what we hope to accomplish, take a look at the following animated image:

Animated Sprite Atlas Example

In the above image we have a plane that is animated with jet-stream lines. After a certain amount of time, the plane explodes and shows an explosion animation. Both the plane and the explosion are part of the same spritesheet and has the same end result as what we saw in the previous tutorial. However, this time the approach is different.

Create a New Phaser 3.x Project for the Game

We’re going to start by creating a fresh project to work with. On your computer, create a new directory and within it create an index.html file with the following markup:

<!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 markup is just boilerplate Phaser 3.x code. We’ve included the framework JavaScript file, defined our configuration, and added placeholders for our scene lifecycle functions. We’re going to be spending all of our time in the preloadScene and createScene functions for this example.

With the foundation to our project in place, now we can get our sprites and animations going.

Define the Animations and Animate the Sprite Atlas

Having a properly defined sprite atlas is going to make or break this example. While you could do this by hand using a tool like Affinity Photo or Adobe Photoshop, I encourage you to use a specialized tool for the job. I’m using a tool called TexturePacker, and all you need to do is provide it images to build your sprite atlas. There are other options, so use whatever your preference is.

For the sake of simplicity, we’re going to make use of the following spritesheet:

Airplane Sprite Atlas

If you did happen to see my previous example, you’ll notice that this spritesheet is more complex than the last. For this spritesheet to be usable, we need to download the atlas JSON file.

For this example, I’ll be referring to these two files as plane.png and plane.json, but feel free to name them however you want.

With both files in your project directory, add the following to your preloadScene function in the index.html file:

function preloadScene() {
    this.load.atlas("plane", "plane.png", "plane.json");
}

You’ll notice that we are loading the plane image and the plane JSON files and naming our asset plane which can be used anywhere within our project. With our asset available and properly mapped, we can create some animations around it.

Within the createScene function, add the following:

function createScene() {

    this.anims.create({
        key: "fly",
        frameRate: 7,
        frames: this.anims.generateFrameNames("plane", {
            prefix: "plane",
            suffix: ".png",
            start: 1,
            end: 3,
            zeroPad: 1
        }),
        repeat: -1
    });

    this.anims.create({
        key: "explode",
        frameRate: 7,
        frames: this.anims.generateFrameNames("plane", {
            prefix: "explosion",
            suffix: ".png",
            start: 1,
            end: 3,
            zeroPad: 1
        }),
        repeat: 2
    });

}

You’ll notice that we have two different animations in the above code. While similar to what we saw in the standard spritesheet example, we’re using generateFrameNames instead of generateFrameNumbers. Each frame in our spritesheet has a name, in this case the original filename that composed that particular frame. What we’re doing in the generateFrameNames functions is defining the prefix, suffix, and range of dynamic information that defines each frame. In my spritesheet, each image was called the following:

plane1.png
plane2.png
plane3.png
explosion1.png
explosion2.png
explosion3.png

So in this example the prefix is what comes before the number, the suffix is the file extension, and the start and end represent the numbers in-between. The padding represents how many zeros to put in front of each number. In my example there is no padding between the number and the text.

In regards to the rest of the animation. We are saying that the animation should have seven frames per second and in the scenario of the fly animation, should animate forever. For the explode animation, it will animate two times and then stop.

So we have our animations defined, but they aren’t currently attached to any sprites. Let’s make that happen now.

Within the createScene function of the index.html file, add the following:

function createScene() {

    // Animations ...

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

}

In the above code, we are creating a sprite at a given position, and then playing the fly animation on the sprite. If we did nothing else, the animation would display on the screen forever.

Next we’re going to see how to change between animations and even perform actions based on events in the animation lifecycle.

Switch Animations and Respond to Animation Events on a Timer

In a typical scenario, we’d probably change animations depending on how the sprite interacts with the rest of the game. In our example, we’d probably only show the explode animation if the plane collides with another sprite. However, to avoid introducing complexity into this example, we’re going to change animations based on a timer.

Within the createScene function of the index.html file, add the following:

function createScene() {

    // Animations ...

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

    setTimeout(() => {
        plane.play("explode");
        plane.once(Phaser.Animations.Events.SPRITE_ANIMATION_COMPLETE, () => {
            plane.destroy();
        });
    }, 3000);

}

In the above code we’re using a simple setTimeout in JavaScript to do something after three seconds. After three seconds we start playing the explode animation and we also define a listener to an event in the animation lifecycle. In this example, when the sprite is done animating, we destroy it, which means remove it from the scene. Had we not destroyed the sprite, we would have been left with the last frame of the animation just showing static.

While setTimeout is standard JavaScript, it may not be the best way to take care of things in a Phaser 3.x game. Instead, we might consider the following instead:

function createScene() {

    // Animations ...

    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();
            });
        }
    });

}

In the above code we are using a Phaser timer to delay a function for three seconds. I’m not sure if this method is more optimized for a game environment versus the setTimeout way to do things, but since it existed in Phaser, I thought it would probably be best.

If you run the project, you should have an animated sprite that uses frames from our sprite atlas.

Conclusion

You just saw how to make use of a sprite atlas in a Phaser 3.x game. This is the preferred approach to the standard spritesheet which I demonstrated in a previous tutorial because the sprite atlas has less empty space and is more optimized for gaming.

As I mentioned, I used TexturePacker to generate my sprite atlas. I could do this by hand, but figuring out where one sprite frame ends and the other begins could be a huge hassle.

Nic Raboy

Nic Raboy

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

Search

Follow Us

The Polyglot Developer

Subscribe

Subscribe to the newsletter for monthly tips and tricks on subjects such as mobile, web, and game development.

The Polyglot Developer

Support This Site