Panda2 sprite sheet animations tutorial

Andreas Löw
Last updated:
GitHub
Panda2 sprite sheet animations tutorial

In this tutorial you are going to learn how to use sprite sheets for animations in your Panda 2 project.

You'll create a simple scene with a walking character that you can control using the keyboard: Click inside the scene, use ←︎ and →︎ to move betty:

You can follow the tutorial step by step to build this game scene, starting from an almost blank Panda 2 project.

The complete tutorial code is available on GitHub. All important stages of the project are available in the sub-folders.

Creating the basic game scene

Please open the folder 01-start in Finder / Explorer. You should see the basic project structure like this:

  • media
    • betty.atlas - the atlas description file
    • betty.png - the sprite atlas / sprite sheet
  • spritesheets
    • betty - Folder containing the sprites
    • betty.tps - TexturePacker project file
  • src
    • engine - Panda 2 engine
    • game
      • config.js - configuration
      • main.js - the source code

The sprite atlas is already packed for you — I'll explain how to add new sprites, edit pivot points and optimize the sprite sheet for faster rendering and downloads later.

Open the project in Panda 2 — you should see a simple game scene with some floor tiles and a background:

Image

Let's have a quick look at the source code. The project is set up for landscape mode in config.js:

game.config = {
    name: 'Sprite Sheet Demo',

    system: {
        width: 1024,
        height: 768,
        scale: true,
        center: true,
        resize: false
    },

    mobile: {
    }
};

Let's have a look at main.js:

First you have to load the sprite sheet:

// load sprite sheet
game.addAsset('betty.atlas');

This loads both — the sprite sheet data and the image. The data file contains the information about the sprites, their sizes and positions in the sheet.

Next is initializing the scene in the init method:

    init: function() {
        // set background color for the scene
        this.backgroundColor = '#cceeff';

        // create floor tiles
        var groundY = this.makeFloor();
    },

The makeFloor method places some ground floor tiles at the bottom of the scene. It returns the y position of the floor tiles which we need to place the character later.

    makeFloor: function() {
        for(var i=0; i<5; i++) {
            var floor = new game.Sprite("floor.png");
            floor.addTo(this.stage);
            floor.y = game.height-floor.height;
            floor.x = floor.width*i;
        }
        return game.height-floor.height;
    }

Note that it references a sprite floor.png — this is the name of a packed in the sheet. It's not a separate file that is loaded from file system or a server. This speeds up the start time of your game since only 1 image has to be loaded instead of loading each animation phase separately.

Here's the complete code of the main.js:

game.module(
    'game.main'
)
.body(function() {

// load sprite sheet
game.addAsset('betty.atlas');

game.createScene('Main', {

    init: function() {
        // set background color for the scene
        this.backgroundColor = '#cceeff';

        // create floor tiles
        var groundY = this.makeFloor();
    },

    makeFloor: function() {
        for(var i=0; i<5; i++) {
            var floor = new game.Sprite("floor.png");
            floor.addTo(this.stage);
            floor.y = game.height-floor.height;
            floor.x = floor.width*i;
        }
        return game.height-floor.height;
    }
});

}); // module

So far you've used the sprite sheet that is part of the demo source. You are not going to learn how you can create your own sprite sheet, add or remove sprites and more:

Creating your own sprite sheet

First, download TexturePacker — it's the tool you are going to use for this:

Install and start TexturePacker, activate the trial version for this tutorial.

Here's how you create the sprite sheet in 4 simple steps:

How to create a sprite sheet in TexturePacker
  1. Drag and drop the folder containing your sprites onto TexturePacker
  2. Select Panda 2 from the Framework dropdown
  3. Set the file name for the .atlas file. The name for the image is automatically updated
  4. Press publish

That's it. Wasn't too complicated, was it?

TexturePacker has some nice features for you: You've added the folder containing the sprites, we call this a smart folder. It checks for all sprites inside the folder and adds them to the sheet.

It also preserves the folder structure as part of the sprite names. E.g. you can access the frame stand/left.png.

TexturePacker detects if you make changes — update, add or remove a sprite and loads the new sprites simply press Publish to write the new sprite sheet.

Optimize your sprites: More FPS and faster loading

Trimming

TexturePacker already does some nice things for you out of the box: It removes additional transparency around your sprites. This is called trimming. Why that?

  1. It reduces the file size
  2. It reduced memory usage
  3. It speeds up the rendering of your game
Bad sprite sheet

Bad sprite sheet: Untrimmed sprites contain transparency which wastes memory and performance. 4680kb RAM

Optimized sprite sheet

Sprite sheet optimized by TexturePacker: Obsolete transparency is removed. 2993kb RAM

This is because transparent pixels are not simply nothing: They are data — costing you 4 bytes of RAM each. The graphics hardware has to read each transparent pixel event if it decides not to paint it.

In terms of RAM usage: The original sprite sheet (with untrimmed sprites) consumes 4680kb. Trimming the sprites reduces the memory usage by 36% to 2993kb.

You might now be concerned about how this will affect your game code in Panda. The good answer is: Not at all.

The .atlas file TexturePacker also creates for you contains the exact positions and the original sprite sizes. You game code behaves as if the sprites still contain the transparency.

Using 8-bit pngs (pngquant)

TexturePacker can even do more in terms of optimization: The current file size for the betty sheet is 407kb trimmed. Reducing the amount of colors to 256 also reduced the file size dramatically: 145 Kb remain — with almost no visual impact! This is a reduction in download size by 64%.

You can easily achieve this by setting Texture Format to PNG-8 (indexed).

Let's not return to the game code.

Playing an animation in Panda 2

Ok - back to our code. Let's add the Betty animation to the scene:

Add the following lines to the init method in main.js:

this.sprite = game.Animation.fromTextures('right/');
this.sprite.play();
this.sprite.addTo(this.stage)

Ok — Betty is now running in the top left corner of the screen.

Panda 2 automatically takes all sprites starting with right/ and builds an animation from them. Just make sure to prefix the sprite names with zeros. This is important because otherwise the order will be 1,10,11,... instead of 1,2,3...

Let's put her on the ground.

As you might remember there's the groundY variable that you returned from makeFloor. Let's use that to place the sprite. Append these lines to the previous block:

this.sprite.position.set(game.width/2,groundY);

6 feet under... not what you expected? Right — Panda's default anchor point is top left.

A better anchor point would be the bottom center. The sprite is 256x256 — so the center is 128/256. Let's change that, append these lines to the previous block:

this.sprite.anchor.set(128,256);

Ok. Let's change the animation to stand. Replace the first line with this:

this.sprite = game.Animation.fromTextures('stand/right.png');

Why is Betty hovering above the floor? The problem is in the sprite:

Sprite with anchor point at the bottom

The anchor point of this sprite is in the bottom center.

Fixed anchor point

There's some space at the bottom of the sprite. The anchor point should be higher.

No problem you might say: Let's nudge the anchor point position up a bit. Maybe 5 px? No? 10? No? 9? That's it!

this.sprite.anchor.set(128,256-9);

Ok. Things to remember: If you play the run animation you have to set the anchor to 128/256. If set the stand frame it's now 128/247. That's not good! There really must be a better solution...

TexturePacker's anchor point editor

Go back to TexturePacker:

Visual anchor point editor
  1. Click Sprite settings in the toolbar to open the editor
  2. Select all sprites except for the floor.png
  3. Click in the center screen and select all anchor points by pressing CTRL-A or CMD-A. You can also use rubber band selection (press mouse button in the center screen and drag the mouse to get a selection rectangle).
  4. Use the right sidebar's Predefined combo box and set the anchor to Bottom center

So all sprites now have the bottom center as anchor. But we wanted to fix the stand left/right frames... let's do that:

Visual anchor point editor
  1. Select the stand folder
  2. Select the anchor points in the middle
  3. Adjust the anchor point - your choice:
    • by dragging the anchors with the mouse
    • by pressing cursor 9 times
    • by entering the coordinates

Finally, press publish to write the sprite sheet with the new pivot points.

TexturePacker also contains a realtime animation preview. Click Anim preview in the toolbar. The selected sprites are played as animation. The animation adapts to changes in the anchor points in real time.

Switch back to panda and reload the scene. CMD-R / CTRL-R. What's that? Betty is hovering? This is because you are now setting the anchor point twice.

Remove the following line from the code:

this.sprite.anchor.set(128,256-9);

Ok — looks good.

Betty's animation is symmetrical, the sprite is lit from the top, not from the side. This allows you to remove the animation phases for left from the sprite sheet because you can simply flip the sprite in the code. This is not always an option — especially in games with strong lighting. E.g. if the character is drawn in a way that he's lit from the right side you can't flip him — this would look very strange — the shadow part would point into the direction of the light source...

But it's fine in our case: Delete the left animation folder, stand/left.png and jump/left.png. Go back into TexturePacker and press publish.

Improving the code

Creating a player class

In this final stage of the tutorial you'll stay in Panda. You've initialised the player animation in the main scene setup. This was ok for the first demo but would not be a good idea for a real game.

Let's create a separate player class to encapsulate all the player data. It'll also contain the sprite itself.

So you'll need the following methods:

  • init() — to initialize the class
  • addTo(container) — to add the sprite to the game scene
  • setPosition(x,y) — to set the initial position of the sprite
  • run(direction) — to move the sprite and start the running animation. direction=1 is right, direction=-1 is left
  • stand() — to make betty stand still, facing in the direction of the last movement

There'll also be one method to update the animations:

  • changeAnimation(action) — update animation, private

So this is the skeleton of the player class. Add this to the main.js before the closing });

game.createClass('Player', {
    init: function() {
    },

    addTo: function(container) {
    },

    setPosition: function(x,y) {
    },

    run: function(direction) {
    },

    stand: function() {
    },

    changeAnimation: function(action) {
    }
});

Let's fill in the methods, start with init:

    init: function() {
        this.sprite = game.Animation.fromTextures('right/');
        this.sprite.anims.run = this.sprite;
        this.sprite.anims.stand = game.Animation.fromTextures('stand/right');

        this.direction = 1;
    },

This creates the animation as variable sprite. The next 2 lines this.sprite.anims.run and this.sprite.anims.stand add child animations. You can later use this.sprite.play('run') to play the run animation. You also set the starting direction to right (1).

You can't add the Player class directly to the scene because it's not derived from Container. The sprite is a member variable. The following method adds the sprite to a container:

    addTo: function(container) {
        this.sprite.addTo(container);
    },

You also have to set the initial position of the player:

    setPosition: function(x,y) {
        this.sprite.position.set(x,y);
    },

The stand() method is also simple. It calls changeAnimation with the stand animation:

    stand: function() {
        this.changeAnimation('stand');
    },

run(direction) sets the direction and moves the player and updates the animation.

The new position is calculated using the following formula: direction * speed * game.delta.

  • game.delta is the time between the last game update and the current. This lets the player move with the same speed on all devices.
  • speed is the speed value, a constant
  • direction is 1 for right, -1 for left
    run: function(direction) {
        this.direction = direction;
        var speed = 400;
        this.sprite.x += direction * speed * game.delta;
        this.changeAnimation('run');
    },

The final method is changeAnimation.

The first line this.sprite.scale.x = this.direction; flips the animation if direction=-1. The second line avoids restarting the animation if it's already playing.

    changeAnimation: function(action) {
        this.sprite.scale.x = this.direction;
        if (this.sprite.currentAnim === this.sprite.anims[action]) return;
        this.sprite.play(action);
    }

Add the player to the scene and add controls

Remove the test code from the previous chapter and update the init() method of the game scene:

    init: function() {
        this.backgroundColor = '#cceeff';

        var groundY = this.makeFloor();

        this.player = new game.Player();
        this.player.addTo(this.stage);
        this.player.setPosition(game.width/2, groundY);
    },

The last thing missing are the controls. Add the following method to the scene:

    update: function() {
        if (game.keyboard.down('LEFT')) {
            this.player.run(-1);
        }
        else if (game.keyboard.down('RIGHT')) {
            this.player.run(1);
        }
        else {
            this.player.stand();
        }
    },

Ok that's it: