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.
All important stages of the project are available in the subfolders.
Please open the folder 01-start in Finder / Explorer. You should see the basic project structure like this:
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:
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:
First, download TexturePacker — it's the tool you are going to use for this:
<caw:download product="TexturePacker"/>
Install and start TexturePacker, activate the trial version for this tutorial.
Here's how you create the sprite sheet in 4 simple steps:
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.
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?
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 effect 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.
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.
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:
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...
Go back to TexturePacker:
So all sprites now have the bottom center as anchor. But we wanted to fix the stand left/right frames... let's do that:
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.
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 classaddTo(container)
— to add the sprite to the game scenesetPosition(x,y)
— to set the initial position of the spriterun(direction)
— to move the sprite and start the running animation. direction=1
is right, direction=-1
is leftstand()
— to make betty stand still, facing in the direction of the last movementThere'll also be one method to update the animations:
changeAnimation(action)
— update animation, privateSo 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 constantdirection
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);
}
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: