How to Make Enemy Sprite Track the Player in Phaser 3

Use this to have enemies, opponents, or just NPCs look at the player in a top-down game.

by on 4 minute read


Are you making a top-down game where enemies or opponents should track or keep their eye on the player?

Maybe you have a turret that should face a moving target before firing?

You know that you need to rotate the turret but how do you know what that angle should be?

If you remember your trigonometry classes then you can probably figure it out but Phaser can do the math work for us!

In this article, we will show you how to get that angle using the player's position and the position of the enemy that should face the player.

Angle from Points

You can get an angle from 2 points using Phaser.Math.Angle.Between().

That means we just need the player's position and the enemy's position.

Our code to get an angle from those two points would look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// target or player's x, y
const tx = this.target.x
const ty = this.target.y

// enemy's x, y
const x = this.x
const y = this.y

const rotation = Phaser.Math.Angle.Between(x, y, tx, ty)

// use rotation...

Another way to accomplish the same thing is to use a Vector2 from enemy position to player position like this:

1
2
3
4
5
6
// same variables defined as above

const vec = new Phaser.Math.Vector2(tx - x, ty - y)
const rotation = vec.angle()

// use rotation...

Either implementation will do what you need but we'll provide a full example with zombies to help you see how it can be used within a Phaser game.

Zombies Example

We are going to put the rotation logic above into a custom Zombie class so that it will rotate to always look at the player whether the player is stationary or moving.

The code is in TypeScript.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import Phaser from 'phaser'

export default class Zombie extends Phaser.GameObjects.Sprite implements IZombie
{
	private target?: Phaser.GameObjects.Components.Transform

	constructor(scene: Phaser.Scene, x: number, y: number, texture: string)
	{
		super(scene, x, y, texture)
	}

	setTarget(target: Phaser.GameObjects.Components.Transform)
	{
		this.target = target
	}

	update(t: number, dt: number)
	{
		if (!this.target)
		{
			return
		}

		const tx = this.target.x
		const ty = this.target.y

		const x = this.x
		const y = this.y

		const rotation = Phaser.Math.Angle.Between(x, y, tx, ty)
		this.setRotation(rotation)
	}
}

Our Zombie extends from Phaser.GameObjects.Sprite and implements the IZombie interface that we define. It just requires a setTarget(target: Phaser.GameObjects.Components.Transform) method.

Then the update(t: number, dt: number) method should look familiar. We use the resulting rotation on line 32 with this.setRotation(rotation) to rotate the Zombie.

Creating Zombies in a Scene

Our Scene will have 4 zombies placed near each corner and then a 5th one that randomly picks a position.

The player will be represented by a white circle. The keyboard input code to move the player is omitted from the example to focus on the zombies.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import Phaser from 'phaser'
import Zombie from './Zombie'

export default class HelloWorldScene extends Phaser.Scene
{
	private zombies?: Phaser.GameObjects.Group
	private player?: Phaser.GameObjects.Arc

	// ...

	preload()
    {
		this.load.image('zombie1', 'assets/zombie1_hold.png')
		this.load.image('zombie2', 'assets/zombie2_hold.png')
    }

    create()
    {
		this.zombies = this.add.group({
			classType: Zombie,
			runChildUpdate: true
		})

		// one zombie at each corner
		this.zombies.get(200, 150, 'zombie1')
		this.zombies.get(600, 450, 'zombie2')
		this.zombies.get(200, 450, 'zombie1')
		this.zombies.get(600, 150, 'zombie2')

		// random positioned zombie
		this.zombies.get(
			Phaser.Math.Between(100, 700),
			Phaser.Math.Between(100, 500),
			'zombie2'
		)

		this.player = this.add.circle(400, 300, 10, 0xffffff, 1)

		// set each zombie's target to be the player
		this.zombies.children.each(child => {
			const zombie = child as IZombie
			zombie.setTarget(this.player!)
		})
	}

	// update...
}

We are using a Group to let Phaser handle creating and adding new Zombie instances to the Scene. You can use new Zombie(...) as well and then manually add them to the Scene.

The important part is on line 42 where we set each zombie's target to be the player.

This is what the end result looks like:

Next Steps

From here you can use distance to determine when a zombie should face and try to attack the player.

In the case of a turret, you can start firing bullets, lasers, or other projectiles at the player.

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.

Phaser 3 trigonometry zombie turret track

Want tips and techniques more suited for you?


You may also like...


Video Guides


Beginner Guides


Articles Recommended For You

Fix Stretched Image Distortions in Phaser 3 with 9-Slice Scaling

by on

Are you having image distortion problems when scaling to make a button or panel graphic bigger? Multiple versions of the …

5 minute read

Command Pattern to Undo Player Actions

by on

Are you looking for a clean and reusable way to implement undo for player actions? Perhaps you are making a turn-based …

15 minute read

Advanced Logging with the Strategy Pattern

by on

Have you ever tried debugging a problem with your game that only seems to happen in production? The Developer Tools …

7 minute read

State Pattern for Character Movement in Phaser 3

by on

Writing clean and well-organized code is something all game developers aspire to. We want code to be reusable and easy …

7 minute read

Didn't find what you were looking for?


comments powered by Disqus