00:00
00:00
View Profile PsychoGoldfish

Male

NG Code Monkey

Hard Knocks

Ur Mom

Joined on 9/26/00

Level:
13
Exp Points:
1,790 / 1,880
Exp Rank:
28,312
Vote Power:
5.53 votes
Rank:
Town Watch
Global Rank:
48,271
Blams:
47
Saves:
123
B/P Bonus:
2%
Whistle:
Normal
Trophies:
46
Medals:
623

Animating Sprites with Phaser

Posted by PsychoGoldfish - January 29th, 2020


I'm back with another tutorial for Phaser. This time I'm going to go over using Sprites and Animation in your games.


I'm assuming by this point you already know how to set up and test a Phaser project. If not, check out my previous guide.


There's been some changes since last time.


Before you get started, you should download this Phaser project, and open up htdocs\js\main.js. There are a few updates I've made since the last tutorial you should be aware of.


First you will notice the config.scene_names value only has one scene in it this time. I plan on keeping this tutorial fairly simple, so we'll just be loading art for a single character and playing some animations. There's no real need for a fancy preload routine this time.


Next, you will find new vars named sprite_path and config.sprite_names. This is where we define our sprite classes and their file location.


I've updated how the script adds <script> tags into the HTML body, and moved where the references to our scene classes get passed to Phaser.


These updates will let us extend the Phaser.Sprite class (more on that later) and load in the appropriate files in the same way we load our scene files.


If you decided to add additional JavaScript files, you could easily pop them in the script_paths array.


As you've probably guessed, we have also added a new directory: 'htdocs\js\sprites'. This directory is where we'll be doing most of our coding in this tutorial, in the SlimeSprite.js file.


The last notable change is instead if a 'lib' directory, I've created the following:


htdocs\lib-raw\
htdocs\lib-spritesheet\
htdocs\lib-free-sprite-sheet-packer\
htdocs\lib-texture-packer\

Each of these folders contains the same artwork; frames for a little green slime to idle and hop around. The difference is in how they are packaged. We're going to learn how to use all these different formats to do the same thing.


About that art...


For the record, I am still using an older version of Flash to do a ton of my sprite work. The .fla and .swf files are available in the Phaser project .zip file.


The 'htdocs\lib-raw\' folder contains the animations exported as a PNG sequence. I used these exported images to build the spritesheet.png files in 'htdocs\lib-spritesheet' and 'htdocs\lib-free-sprite-sheet-packer'.


The spritesheet in 'htdocs\lib-texture-packer' were generated using the .swf files (more on that later).


You can make your own art however you want, and one of these methods of handling animation will work just fine for you.


Let's Dive In!


Let's take a look at htdocs\js\scenes\SlimeScene.js


If you read the last tutorial, you should be able to follow what's going on in this file. We have a preload method where we'll load our art assets (in this case all our slime art), a create method where we'll set up the scene and add our slime sprite, and an update method that handles the game loop once everything is ready.


There's 3 main ways we can create animated sprites. We can use a bunch of individual images, use a sprite sheet, or use a texture atlas.


Using Individual Images


Let's go ahead and update the preload method first. We're going to start by loading all the individual images in the 'htdocs\lib-raw' folder.


	// Load all of our assets
	preload()
	{
		// set our load path
		this.load.path = 'lib-raw/';


		// Load the images for our idle animation
		// note, Phaser automatucally applies the .png extension to load these
		this.load.image('slime-idle0001');
		this.load.image('slime-idle0002');
		this.load.image('slime-idle0003');
		this.load.image('slime-idle0004');
		this.load.image('slime-idle0005');
		this.load.image('slime-idle0006');
		this.load.image('slime-idle0007');
		this.load.image('slime-idle0008');


		// Load the images for our hopping animation
		this.load.image('slime-hop0001');
		this.load.image('slime-hop0002');
		this.load.image('slime-hop0003');
		this.load.image('slime-hop0004');
		this.load.image('slime-hop0005');
		this.load.image('slime-hop0006');
		this.load.image('slime-hop0007');
		this.load.image('slime-hop0008');
		this.load.image('slime-hop0009');
		this.load.image('slime-hop0010');
		this.load.image('slime-hop0011');
		this.load.image('slime-hop0012');
	}

This is pretty similar to how we loaded our assets in the last tutorial. This time, however, I'm setting a load path so I don't have to include that in every image load.


I'm also only passing the filename in the load.image() call. This is sort of a shorthand for calling:

this.load.image('slime-hopXXXX', 'slime-hopXXXX.png');

If you only pass the first parameter, Phaser uses it for both values, and auto-appends the ".png" for the image value.


Next, we need to update the create method and create some animations using our images.


	// create our animations
	create()
	{
		// create our idle animation
		this.anims.create({
			// the name of our animation
			key: 'slime-idle',
			// the image name of each sequential frame
			frames: [
				{ key: 'slime-idle0001' },
				{ key: 'slime-idle0002' },
				{ key: 'slime-idle0003' },
				{ key: 'slime-idle0004' },
				{ key: 'slime-idle0005' },
				{ key: 'slime-idle0006' },
				{ key: 'slime-idle0007' },
				{ key: 'slime-idle0008' }
			],
			// the framerate to render this animation
			frameRate: 12,
			// number of times to repeat the animation, or -1 for infinite
			repeat: -1
		});


		// do the same for our hop animation
		this.anims.create({
			key: 'slime-hop',
			frames: [
				{ key: 'slime-hop0001' },
				{ key: 'slime-hop0002' },
				{ key: 'slime-hop0003' },
				{ key: 'slime-hop0004' },
				{ key: 'slime-hop0005' },
				{ key: 'slime-hop0006' },
				{ key: 'slime-hop0007' },
				{ key: 'slime-hop0008' },
				{ key: 'slime-hop0009' },
				{ key: 'slime-hop0010' },
				{ key: 'slime-hop0011' },
				{ key: 'slime-hop0012' }
			],
			frameRate: 24,
			repeat: 0
		});


		this.cameras.main.setBackgroundColor(0x00AAFF);

		// create our slime sprite
		this.slime = new SlimeSprite(this, this.cameras.main.centerX, this.cameras.main.centerY);


		// add it to the stage
		this.add.existing(this.slime);
	}

The anims.create() method takes an object containing a key we can use to play the animation, and an array of frames that will compose the animation. Each frame is an object containing a key that matches one of our loaded assets.


There are other properties you can pass in each animation and frame object that can tweak the animation. These can set independent durations for individual frames, yoyo the whole animation, etc etc. We're not doing anything that fancy here.


We ARE setting independent frameRate and repeat values. The frameRate property is the number of frames per second the animation will run at, and repeat is the number of times the animation will loop before it's considered completed and stops.


To play an animation once, the repeat value should be set to 0. If you want it to loop forever, set it to -1.


In this example, we created 2 animations: slime-idle and slime-hop.


The other things we're doing in the create() method are setting a background color, creating an instance of our custom SlimeSprite, and adding it to our scene. We'll dig in to the SlimeSprite class later.


Go ahead and test the game out now, and you should see our slime bobbing as it idles. Use the left and right arrows to watch him hop across the screen.


Using a Sprite Sheet


A sprite sheet is an image where all of your animation frames appear on a grid. This is a very common way of adding sprites, and one of the simplest options to set up in Phaser.


Let's update that preload method again to use the sheet in 'htdocs\lib-spritesheet':


	// Load all of our assets
	preload()
	{
		// Load our sprite sheet
		this.load.spritesheet('slime', 'lib-spritesheet/spritesheet.png', { frameWidth: 128, frameHeight: 128 });
	}

How about that? We only have to load ONE asset this way. It's almost exactly the same as loading an image, except we also pass an object in with rules about our grid. In this case the frameWidth and frameHeight of our individual frame images.


Phaser will use those frame sizes and create a frame sequence going from left to right, top to bottom (the image on the top left is frame 1, and the image on the bottom right is the last frame).


Phaser will now refer to this sprite sheet as 'slime'.


Next we'll update the create method:


// create our animations
	create()
	{
		// create our idle animation
		this.anims.create({
			// the name of our animation
			key: 'slime-idle',
			// the image name of each sequential frame
			frames: this.anims.generateFrameNumbers('slime', { start: 12, end: 20 }),
			// the framerate to render this animation
			frameRate: 12,
			// number of times to repeat the animation, or -1 for infinite
			repeat: -1
		});


		// do the same for our hop animation
		this.anims.create({
			key: 'slime-hop',
			frames: this.anims.generateFrameNumbers('slime', { start: 0, end: 11 }),
			frameRate: 24,
			repeat: 0
		});


		this.cameras.main.setBackgroundColor(0x00AAFF);


		// create our slime sprite
		this.slime = new SlimeSprite(this, this.cameras.main.centerX, this.cameras.main.centerY);


		// add it to the stage
		this.add.existing(this.slime);
	}

Now that we're working with a sprite sheet, we can use one of Phaser's handy methods for building frame arrays.


anims.generateFrameNumbers() takes 2 parameters. The first is the name of the sprite sheet you want frames from, the second is an object of rules.


In the .png file, the frames for the idle animation are at the end, so I am telling it to generate frame data using frames 12 through 20 for the 'slime-idle' animation.


I could do the same thing manually like so:


		frames: [
			{"key":"slime","frame":12},
			{"key":"slime","frame":13},
			{"key":"slime","frame":14},
			{"key":"slime","frame":15},
			{"key":"slime","frame":16},
			{"key":"slime","frame":17},
			{"key":"slime","frame":18},
			{"key":"slime","frame":19}
		]

Essentially the frame objects in the array tell it the key name of the spritesheet, and the frame number to show for each frame. You could also add other rules (like duration) if you did this manually.


Note: The first frame in a spritesheet is 0. In a 20-frame animation, the last frame will be 19.


You can test the game again, and it'll look and work exactly the same as before, but should load a bit quicker because we're only making ONE request for assets.


Using a Texture Atlas


A texture atlas is a lot like a spritesheet. The big difference is it does not use a static grid. Each frame is an independent rectangle within the overall image. The atlas requires both the image file, and a .json file containing all the information about where each frame rectangle is, what size it should be rendered at, etc etc.


There's a few free tools that can create Texture Atlases for you. For this tutorial, I used the one at https://www.codeandweb.com/free-sprite-sheet-packer. The assets are in 'htdocs\lib-free-sprite-sheet-packer'.


I added all the sprites in 'htdocs\lib-raw' to this tool, set the format to JSON Hash, the layout to compact and the padding to 1px.


The JSON hash format is ideal for Phaser, but you can use this tool to make atlases for other game dev frameworks, or even make compact css rules for web design.


The compact layout will usually do the best job of cramming things into the tightest space (thus keeping your image smaller).


Adding some padding will make sure you don't see any pixel bleeding from neighboring frames when you start messing with image scaling.


Update the preload function to use the atlas:


	// Load all of our assets
	preload()
	{
		// Load our sprite sheet
		this.load.atlas('slime', 'lib-free-sprite-sheet-packer/spritesheet.png','lib-free-sprite-sheet-packer/spritesheet.json');
	}

Like the spritesheet, we're loading everything in a single line, but this time it's going to load two files that work together. Also like the spritesheet, our atlas is now named 'slime'.


Next, we update the create method:


	create()
	{
		// create our idle animation
		this.anims.create({
			// the name of our animation
			key: 'slime-idle',
			// the image name of each sequential frame
			frames: this.anims.generateFrameNames('slime', {prefix:'slime-idle', start: 1, end: 8, zeroPad: 4, suffix:'.png' }),


			// the framerate to render this animation
			frameRate: 12,
			// number of times to repeat the animation, or -1 for infinite
			repeat: -1
		});
		// do the same for our hop animation
		this.anims.create({
			key: 'slime-hop',
			frames: this.anims.generateFrameNames('slime', {prefix:'slime-hop', start: 1, end: 12, zeroPad: 4, suffix:'.png' }),
			frameRate: 24,
			repeat: 0
		});


		this.cameras.main.setBackgroundColor(0x00AAFF);


		// create our slime sprite
		this.slime = new SlimeSprite(this, this.cameras.main.centerX, this.cameras.main.centerY);


		// add it to the stage
		this.add.existing(this.slime);
	}

Because an atlas doesn't use a flat grid, and doesn't necessarily put your frames in any specific visual order, we can't generate animations using frame numbers.


Thankfully, Phaser has a handy method to get around that as well: anim.generateFrameNames().


Each frame in our atlas is named after the source image that it came from. Since the first image in our idle animation is named slime-idle0001.png, the key for that frame is also 'slime-idle0001.png'.


The anim.generateFrameNames() method takes 2 params. First is the name of the atlas, 'slime. Second is an object of rules we'll use to generate the frame names.


This object contains a prefix, start and end numbers, a zeroPad value and a suffix.


All of our frames in the idle animation start with 'slime-idle' and end with '.png'. That's pretty straightforward.


Each individual frame also has the frame number with leading zeros so that the numeric value is always 4 characters long. This is why we set zeroPad to 4.


Because we are using names, it's important to note the first animation frame is named 'slime-idle0001.png' and the last is named 'slime-idle0008.png'. This is why we start at 1 (instead of 0) and end at 8 (instead of 7).


We could also create the animation frames manually like so:


		frames: [
			{"key":"slime","frame":"slime-idle0001.png"},
			{"key":"slime","frame":"slime-idle0002.png"},
			{"key":"slime","frame":"slime-idle0003.png"},
			{"key":"slime","frame":"slime-idle0004.png"},
			{"key":"slime","frame":"slime-idle0005.png"},
			{"key":"slime","frame":"slime-idle0006.png"},
			{"key":"slime","frame":"slime-idle0007.png"},
			{"key":"slime","frame":"slime-idle0008.png"}
		]

If you did this manually you could add other rules, like duration, if you needed to.


Test the game again, and you'll see it working exactly the same again.


So which method is the best?


The answer to this is a bit complicated; you have to account for a few different things.


The most obvious is file size. The more bytes you have to download, the longer your game will take to load.


Every individual file will have some meta info, like its mime-type, publication information, etc etc. You can shave a ton of this bloat using a program like PNGGauntlet but you will always see an incremental increase using individual files.


Texture Atlases have a few drawbacks with size as well. While you can put more items in a single image, unless you have the perfect amount of sprites to fill it, there will be notable chunks of wasted blank space. Each frame also adds to the .json file.


Spritesheets are fairly optimized. Yes, each sheet will have some overhead, but there is no wasted space, nor is there any .json file to worry about.


Another factor to consider is total files. Every time you have to request a file from a web server, there is some delay. The more requests you have to make to load your game, the longer it will take.


Individual files are pretty much the worst case scenario here.


As you add more sprites, texture atlases start looking better than sprite sheets.


Then you have to consider memory management. The less images you have to store in memory, the quicker your game will render. A single texture atlas will give you improved performance over multiple sprite sheets. WebGL, in particular, works best when using minimal textures.


At this point, it really comes down to spritesheets and texture atlases.


The final consideration is how you prefer to work.


If you are happy building grid-based sheets, and having to keep track of frame numbers to build animations, you'd be best sticking with sprite sheets.


You may also be downloading a bunch of pre-made sheets that you don't want to break apart to make a texture atlas. There's no shame in making less work for yourself.


If you still use Flash for your art, like I do, you may prefer using Texture Atlases.


If you use Animate CC, you have atlases as a built-in option. If you use older version of Flash, you can easily export to png sequences. Either way, you will get atlases with easy-to-use key names, and will have some tool you can use to do the dirty work of building your sheets and json files for you.


If you are using old Flash, and can afford a copy of TexturePacker, things get even better.


And now I shamelessly endorse TexturePacker!!!!!


TexturePacker just does so many things right. First off, you don't even need to export your Flash animations to png sequences. You can actually import your swf files directly, and it will process your frames automatically.


The other thing is does well is recognizing duplicate images. If you have animationa that use the same image on multiple frames, TexturePacker will only add the image to the png once, but still create the frame info for it in your json file.


Even better, TexturePacker detects white space in each of your frames and can trim it off (it gets added back in when your frame is rendered).


Unike the fee tool, TexturePacker lets you set up smart folders that will automatically find any new swf or image files you save. All you have to do is publish your sprite sheet when you are ready.


TexturePacker also has a Phaser 3 specific output format and bunch of compression options that can really shave down your file sizes. You can compare the files it produces with the free online texture packer by looking in 'htdocs\lib-texture-packer' and 'htdocs\lib-free-sprite-sheet-packer'.


Even if you prefer to use Unity or some other game development platform, texture atlases are widely supported, and TexturePacker builds them better than anything else.


You will find that TexturePacker creates slightly different frame keys than you would get manually exporting images. The first frame in our idle animation, for example, would be named 'slime-idle/0000'.


If you are interested in seeing this slime demo using a TexturePacker atlas, check out htdocs\js\scenes\SlimeScene-texture-packer.js. You can see the completed code for all of our different versions in the 'htdcos\js\scenes' folder if you like.


That about covers the image and animation stuff, so let's dive into...


Custom Sprites!


The Phaser.Sprite class lets you put an object on the stage, set an image or animation in it, scale, rotate, and resposition, etc etc. It's what makes all the interactive parts of your game.


In the same way we can extend the Phaser.Scene class, we can also extend Phaser.Sprite.


In our 'SlimeScene' class, we are creating an instance of a custom 'SlimeSprite'. You can open up 'htdocs\js\sprites\SlimeSprite.js' and see how it works.


The Sprite constructor needs a scene to be linked to. You can also pass along an x and y position to start at. This information gets passed to the superclass.


We can set up anything else we need here as well. In this case, I'm creating references to the left and right key, and setting the initial idle animation.


The update method, in this class, does NOT get called automatically like it does in a scene class. We are calling it manually in the 'SpriteScene' class when ITS update method is called.


This simple interactive sprite isn't much good for a proper game, but if you are looking at it's code there's some tidbits you might appreciate.


First, you can see how we set scaleX to either 1 or -1 to change the direction of the sprite.

You can also see how we play animations with the play() method.

And you can see how we use the once() method to add a single-use event listener, in this case for the animation complete event.


Keep Learning!


I hope I've been able to give you guys enough inspiration to really give Phaser a solid go. You still have a lot to learn, but you should have a solid enough foundation to take things to the next level.


If you are comfortable digging through code to learn how it works, you should really enjoy poking around the many exaples at https://phaser.io/examples/v3


If you still need a bit more guidance, jump over to https://phaser.io/tutorials/making-your-first-phaser-3-game and learn how to build a simple platform game!


Tags:

8

Comments (2)

Ok, starting to be swayed toward trying out Phaser...

Bang something really simple out for the game jam!

@BoMToons @PsychoGoldfish Gotta find some time.