Are you are making an action-RPG, RTS, dungeon crawler, or similar type of game using tilemaps in Phaser 3 and want characters to move to where the mouse was clicked?
Maybe you've tried adjusting the velocity or used a tween to move to the mouse position but found that it didn't really work? Especially when there's a wall in the way?
What you need is a pathfinding algorithm!
In this article, we'll show you how to add point & click movement using breadth-first search to discover a path and then have the character move along it.
Example Base Project
We'll be building on top of our Dungeon Crawler Starter Template that uses TypeScript and comes with an 8.5 part YouTube series going over how it was made.
If you are not familiar with TypeScript then we've got a free book to help you get started!
Learn to make an Infinite Runner in Phaser 3 with TypeScript!
Drop your email into the box below to get this free 90+ page book and join our newsletter.
The code in this article should work for any Phaser 3 project that uses tilemaps but you may have to refer to the Dungeon Crawler Starter Template for additional context.
Start with a Click
Let's start by listening for a POINTER_UP
event on the Game
Scene. The pointer data will be used to determine which tile was clicked.
This tile will be used as the target location for our character to move to.
|
|
The worldX
and worldY
values from the pointer
represent where the player clicked. We use object destructuring on line 6 to get the values.
Note: if you get an error about tslib
then you probably need to install it with npm install tslib --save-dev
.
Then we use those values on line 9 to get the tile x
and y
values from the groundLayer
which is a StaticTilemapLayer
.
We do the same thing with the player (faune
) on line 8 to get the starting position.
In breadth-first search, we need a start position to begin traversal and then a target position to know when we're done. The startVec
and targetVec
variables give us this information.
They are suffixed with Vec
because they are of type Phaser.Math.Vector2
.
Find a Path with Breadth-First Search
Next, we will implement the breadth-first search algorithm to create a lookup table that can be used to construct a path. This path will be an array of Phaser.Math.Vector2
values representing the world positions the player needs to move along.
Let's add a findPath.ts
file to the utils
folder like this:
|
|
Notice that we declared an interface called TilePosition
and created the toKey(x, y)
helper function.
These are to help make the implementation code in findPath()
easier to understand.
With breadth-first search, we will add the start tile to a queue. Then we will look at each item in the queue and add all its neighbors. As we do this we will construct a lookup table of parent tiles that we can use to generate a path from the target to where the player is.
We recommend taking a look at this video for an in-depth explanation and step-by-step implementation of breadth-first search.
The groundLayer
and wallsLayer
is passed to this function so that we can check if the target is a valid ground tile for the player to walk to and if there are walls in the way.
Here's the breadth-first search implementation that builds a parent lookup table:
|
|
The bulk of the code is a basic breadth-first search implementation using just the 4-way north, south, east, and west neighbors. Each neighbor is added to the parentForKey
lookup table with the current tile as the parent.
Notice that we are not storing the actual Tile
reference but just its x
and y
positions.
Next, we can use the parentForKey
lookup to get all the tiles that make a path from the target location to the starting location.
|
|
The goal of this code is to find the lineage that takes us from the target position back to the start position.
We do this by adding the target's parent to the path
array and then doing the same for that parent's parent and so forth until we get to the start position.
Another way to look at it:
target -> parent -> grandparent -> great grandparent -> ... -> start
This lineage is what makes up the path
array. The position of each ancestor is added to this list and then the entire list reversed.
Moving the Player Along a Path
Now we can give the player the path
array and have it move to each position one after the other.
Start by adding these properties and methods to the Faune
class:
|
|
This new code should be pretty straight forward. We add the movePath
and moveToTarget
properties that will hold the path of positions and the current position to move to.
Notice that !
operator on line 18. This is the non-null assertion operator. This lets us tell TypeScript that the result of shift()
will return a valid value and not null
or undefined
.
We are going to replace the existing update(cursors: Phaser.Types.Input.Keyboard.CursorKeys)
method with a new one on line 27. The player's movement will be based on moveToTarget
instead of CursorKeys
so we no longer need cursors
to be passed in.
We'll use logic that mimics keys being pressed depending on where moveToTarget
is relative to the player's position. We've left this method relatively similar to the original so that you can more easily compare and see the differences.
|
|
We use dx
and dy
to represent the difference in x
and y
between moveToTarget
and the player's position.
The player is considered to have arrived at the target if dx
and dy
are within 5
pixels of the target location on either side. This check happens on lines 18 and 22.
Then on line 31, a new moveToTarget
position is set by taking the next one from this.movePath
. If there are no more positions left then moveToTarget
is set to undefined
and the player will stop moving.
The subsequent leftDown
, rightDown
, upDown
, and downDown
variables that were used to denote arrow keys being pressed is now set based on whether dx
and dy
is positive or negative.
Lastly, the same animation and velocity logic is used to move the player each frame.
Putting it Together
We created a bunch of code, methods, and functions so let's use them together to achieve point & click movement!
Back in the create()
method of the Game
Scene we need to use findPath()
and call this.faune.moveAlong()
:
|
|
You should end up with something like this that will find an appropriate path around walls and move the player along that path:
Next Steps
This was a meaty article full of code so take some time to digest it!
Try using the NE, SE, SW, and NW diagonal neighbors in the breadth-first search algorithm to see how that changes the path.
We've completely omitted the logic for collecting treasure chests for simplicity so feel free to see how you can add that back!
Be sure to sign up for our newsletter so you don't miss any future Phaser 3 game development tips and techniques!
Drop your email into the box below.
Don't miss any Phaser 3 game development content
Subscribers get exclusive tips & techniques not found on the blog!
Join our newsletter. It's free. We don't spam. Spamming is for jerks.