How to Make Top Down Movement With Animations using Phaser 3

How to Make Top Down Movement With Animations using Phaser 3

Published

Introduction

In this tutorial we are going to be making a basic character with movement and animations using Phaser 3 and Typescript. We will be making a new project from scratch but this will easily work in any of your projects as well. With that being said, let’s get started!

The demo project I will be making uses the Sprout Lands asset pack made by Cup Nooble

Create a New Phaser Project

If you already have a project, you can skip this step.

We can easily create a new Phaser project using the Phaser Create Game App

Using NPM, it looks something like this:

npm create @phaserjs/game@latest

After running the command it will take you through a few steps to customize your new project. In this case, I chose the Vite template, minimal extra scenes, and Typescript.

After we have our project, follow the steps to start the dev server.

1. cd [project-name]
2. npm install
3. npm run dev

After that, we will load any necessary assets (like our player sprite sheet) to the /public/assets folder.

With that out of the way, let’s get started with the project!

Creating the Main Scene

Open the Game.ts file found at /src/scenes/Game.ts. After removing some extra stuff from the create() function, we will preload a tileset we will use for our player.

// Game.ts

import { Scene } from 'phaser'

export class Game extends Scene {
    constructor() {
        super('Game')
    }

    preload() {
        this.load.setPath('/assets')
        this.load.spritesheet('player', 'Basic Charakter Spritesheet.png', {
            frameWidth: 48,
            frameHeight: 48,
            //margin: 0,
            //spacing: 0,
        })
    }
}

Make sure you use the correct frameWidth and frameHeight, and also add the margin and/or spacing if your spritesheet has any

We will create the animations when we create the Player, but first we need to enable physics in our game.

Enabling Arcade Physics

Before we get started with our character, we quickly need to enable the arcade physics engine in our project.

This is very easy to do, we just need to add the following to the config:

physics: {
	default: 'arcade',
	arcade: {
    	gravity: { x: 0, y: 0 },
    	debug: true, // shows the collision shape and velocity
	},
},
antialias: false, // make our pixel art less blurry

We will also disable antialiasing since our game is using pixel art.

Our full config file now looks something like this:

// main.ts

import { Game as MainGame } from './scenes/Game'
import { AUTO, Game, Scale, Types } from 'phaser'

//  Find out more information about the Game Config at:
//  https://newdocs.phaser.io/docs/3.70.0/Phaser.Types.Core.GameConfig
const config: Types.Core.GameConfig = {
    type: AUTO,
    width: 1024,
    height: 768,
    parent: 'game-container',
    backgroundColor: '#C0D470',
    scale: {
        mode: Scale.FIT,
        autoCenter: Scale.CENTER_BOTH,
    },
    physics: {
        default: 'arcade',
        arcade: {
            gravity: { x: 0, y: 0 },
            debug: true, // shows the collision shape and velocity
        },
    },
    antialias: false, // make our pixel art less blurry
    scene: [MainGame],
}

export default new Game(config)

Now we can get started on the Player file!

Creating the Player

First we will create a new folder to hold our player as well as any other entities our game will have. We can call it entities

Create Player.ts in the folder and create a new class that extends Phaser.Physics.Arcade.Sprite

// Player.ts

export default class Player extends Phaser.Physics.Arcade.Sprite {
    constructor(scene: Phaser.Scene) {
        super(scene, scene.scale.width / 2, scene.scale.height / 2, 'player')

        this.setScale(4) // the player sprite is too small by default
        this.scene.add.existing(this)
        this.scene.physics.add.existing(this)

        this.body?.setSize(16, 16) // the collision shape is now too big
    }
}

Note that you might not need to call this.setScale(4) or this.body?.setSize(16, 16) if you are using another spritesheet, I just needed to add them to make our sprite fit correctly.

Now we can give our player some movement!

Moving the Player

Update the player class to include the following:

// Player.ts

export default class Player extends Phaser.Physics.Arcade.Sprite {
    private cursors?: Phaser.Types.Input.Keyboard.CursorKeys
    private wasd?: { [key: string]: Phaser.Input.Keyboard.Key }
    private speed: number

    constructor(scene: Phaser.Scene) {
        // Previous constructor code goes here

        this.cursors = scene.input.keyboard?.createCursorKeys()
        this.wasd = scene.input.keyboard?.addKeys({
            up: Phaser.Input.Keyboard.KeyCodes.W,
            down: Phaser.Input.Keyboard.KeyCodes.S,
            left: Phaser.Input.Keyboard.KeyCodes.A,
            right: Phaser.Input.Keyboard.KeyCodes.D,
        }) as { [key: string]: Phaser.Input.Keyboard.Key }

        this.speed = 250
    }
}

This allows us to get the input from the arrow keys and the WASD keys. When we set the this.wasd variable, we add as { [key: string]: Phaser.Input.Keyboard.Key }. This is because addKeys can return many things so we want to specify what it is returning.

Now lets create an update function to read these inputs. This will go right below our constructor:

// Player.ts

update() {
	let direction = new Phaser.Math.Vector2(0, 0)
	if (this.cursors?.left.isDown || this.wasd?.left.isDown) {
    	direction.x -= 1
	}
	if (this.cursors?.right.isDown || this.wasd?.right.isDown) {
    	direction.x += 1
	}
	if (this.cursors?.up.isDown || this.wasd?.up.isDown) {
    	direction.y -= 1
	}
	if (this.cursors?.down.isDown || this.wasd?.down.isDown) {
    	direction.y += 1
	}

	direction.normalize().scale(this.speed)
	this.setVelocity(direction.x, direction.y)
}

Now our player has all the code needed to move, but we still need to create an instance of it in our scene. Lets go do that.

We will create

Back in our Game.ts file, add the following below the preload function:

// Game.ts

// Other variables go here
private player: Player


// Other functions go here
create() {
	this.player = new Player(this)
}

update(_time: number, _delta: number): void {
	this.player.update()
}

Now if we run our game, it should show a moving character!

Lets get to adding the animations.

Animating the Player

We will start by creating a function in our Player class that creates all the animations from our spritesheet:

// Player.ts

constructor(scene: Phaser.Scene) {
	// Previous constructor code goes here

	this.createAnimations()
}

private createAnimations() {
    this.anims.create({
        key: 'walk-down',
        frames: this.anims.generateFrameNumbers('player', {
            start: 2,
            end: 3,
        }),
        frameRate: 6,
        repeat: -1,
    })

    this.anims.create({
        key: 'walk-up',
        frames: this.anims.generateFrameNumbers('player', {
            start: 6,
            end: 7,
        }),
        frameRate: 6,
        repeat: -1,
    })

    // Do this for each animation
}

The actual createAnimations() function is quite a bit longer, but it should be as easy as copying and pasting. All we are changing is the frames and sometimes the framerate.

We do this for every animation we need for our player. In this case that means an idle animation and walk animation for each direction.

When we want to play the animations, it will make our lives much easier if we stick to a specific way of naming our animations. We will see this shortly.

We want all of our animations to loop which is why we include repeat: -1, but if we want it to play only once it is as easy as getting rid of that line.

Now that we have all the animations created. We want to make a way to play them. We have a slight problem though. We currently don’t have any way to see waht direction the player is facing.

We will need to add a variable called currentDirection, and update it whenever we change directions.

// Player.ts

export default class Player extends Phaser.Physics.Arcade.Sprite {
	// Other variables go here
	private currentDirection: string

	// Other functions go here
	update() {
    	let direction = new Phaser.Math.Vector2(0, 0)
    	if (this.cursors?.left.isDown || this.wasd?.left.isDown) {
        	direction.x -= 1
        	this.currentDirection = 'left'
    	}
    	if (this.cursors?.right.isDown || this.wasd?.right.isDown) {
        	direction.x += 1
        	this.currentDirection = 'right'
    	}
    	if (this.cursors?.up.isDown || this.wasd?.up.isDown) {
        	direction.y -= 1
        	this.currentDirection = 'up'
    	}
    	if (this.cursors?.down.isDown || this.wasd?.down.isDown) {
        	direction.y += 1
        	this.currentDirection = 'down'
    	}

    	direction.normalize().scale(this.speed)
    	this.setVelocity(direction.x, direction.y)

    	if (direction.length() > 0) {
        	this.play(`walk-${this.currentDirection}`, true)
    	} else {
        	this.play(`idle-${this.currentDirection}`, true)
    	}
	}

With the way that we named our animations, we can easily pick between the walk and idle animation depending on our velocity.

Now whenever we move, the correct animation should play. If not, you might need to mess with the frame numbers until it does.

Conclusion

With that, we have successfully created a player controller with animations in Phaser! Hopefully this project helped explain how to get user input in Phaser as well as create custom animations. If you have any suggestions, let me know. Remember that the demo project can be found here. Check out our other tutorials if you found this one helpful, and thank you for reading!