Game Optimization with Basic Object Pools in Phaser 3

Use Object Pooling in Phaser 3 with the Group Class

by on 6 minute read


Speed is often an important performance metric in a game. If players have to react in realtime then framerate is a big deal.

One common reason for framerate problems is memory allocation inside update loops.

Memory allocation happens when creating new instances.

You generally want to avoid doing that as much as you can but making things appear in response to player interaction is unavoidable.

The solution is to use Object Pools.

In this article, we will take a look at using Object Pools in Phaser 3.

What is an Object Pool?

The big idea behind an Object Pool is to create instances of objects ahead of time to reuse them later.

Instead of creating a new instance of something you would take an unused instance from the pool.

Once finished with the instance it is returned to the pool for later use.

Like recycling. ♻️

Object Pools in Phaser

Phaser 3 has Groups that are designed to let you create, manipulate, or recycle similar GameObjects.

While not described as an Object Pool, they have the fundamental features of an Object Pool.

Our example will look something like this:

Setting Up the Scene

We will keep all logic in a single Scene for simplicity. In the next article, we will look at encapsulating the Object Pool logic.

For now, just keep it simple. Note that 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import Phaser from 'phaser'

const KEY_CRATE = 'crate'
const INFO_FORMAT = 
`Size:       %1
Spawned:    %2
Despawned:  %3`

export default class CratesScene extends Phaser.Scene
{
	private group?: Phaser.GameObjects.Group
	private infoText?: Phaser.GameObjects.Text

	constructor()
	{
		super('crates-scene-basic')
	}

	preload()
	{
		this.load.image(KEY_CRATE, 'assets/crate.png')
	}

	create()
	{
		this.group = this.add.group({
			defaultKey: KEY_CRATE
		})

		this.infoText = this.add.text(16, 16, '')
	}

	update()
	{
		if (!this.group || !this.infoText)
		{
			return
		}

		const size = this.group.getLength()
		const used = this.group.getTotalUsed()
		const text = Phaser.Utils.String.Format(
			INFO_FORMAT,
			[
				size,
				used,
				size - used
			]
		)

		this.infoText.setText(text)
	}

	private spawnCrate(x = 400, y = 300)
	{

	}
}

All the code in update() is only to show information about the state of our Object Pool.

The Group instance that will act as our Object Pool is created on line 26.

crate.png is an image we are using from Kenney.nl and the project structure is from the phaser3-parcel-template.

Next, let's add logic to the spawnCrate(x, y) method on line 56 to handle spawning crates.

Creating or Reusing

The Phaser.GameObjects.Group class has a get() method that will create a new instance under 2 cases:

  • when the group is empty
  • when all member instances are active

When there are inactive instances in the group it will return the first inactive instance it finds by checking the active property.

Let's see how we can use get() and add a simple scale and fade tween animation to each spawned crate.

 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
import Phaser from 'phaser'

const KEY_CRATE = 'crate'
// ...

export default class CratesScene extends Phaser.Scene
{
	private group?: Phaser.GameObjects.Group

	// ...

	private spawnCrate(x = 400, y = 300)
	{
		if (!this.group)
		{
			return null
		}

		const crate: Phaser.GameObjects.Sprite = this.group.get(x, y, KEY_CRATE)

		crate.alpha = 1
		crate.scale = 1
		crate.setVisible(true)
		crate.setActive(true)

		this.tweens.add({
			targets: crate,
			scale: 2,
			alpha: 0,
			duration: Phaser.Math.Between(500, 1500),
			onComplete: (tween) => {
				this.group!.killAndHide(crate)
				this.tweens.killTweensOf(crate)
			}
		})

		return crate
	}
}

We use this.group.get() on line 19 to get a member instance. It will be of type Phaser.GameObjects.Sprite by default. We can change that by providing classType when adding the group in create().

The next 4 lines are there to make sure the instance is properly reset because it could be a reused instance.

The active and visible properties need to be reset because we set them to false at the end of the tween by calling this.group!.killAndHide(crate).

The alpha and scale properties are reset to 1 because they are changed by the tween.

Now let's use this method to see some crates!

 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
48
49
50
51
52
53
54
55
56
57
58
import Phaser from 'phaser'

const KEY_CRATE = 'crate'
const INFO_FORMAT = 
`Size:       %1
Spawned:    %2
Despawned:  %3`

export default class CratesScene extends Phaser.Scene
{
	private group?: Phaser.GameObjects.Group
	private infoText?: Phaser.GameObjects.Text

	// ...

	create()
	{
		this.group = this.add.group({
			defaultKey: KEY_CRATE
		})

		this.input.on(Phaser.Input.Events.POINTER_DOWN, pointer => {
			this.spawnCrate(pointer.x, pointer.y)
		})

		this.infoText = this.add.text(16, 16, '')
	}

	// ...

	private spawnCrate(x = 400, y = 300)
	{
		if (!this.group)
		{
			return null
		}

		const crate: Phaser.GameObjects.Sprite = this.group.get(x, y, KEY_CRATE)

		crate.alpha = 1
		crate.scale = 1
		crate.setVisible(true)
		crate.setActive(true)

		this.tweens.add({
			targets: crate,
			scale: 2,
			alpha: 0,
			duration: Phaser.Math.Between(500, 1500),
			onComplete: (tween) => {
				this.group!.killAndHide(crate)
				this.tweens.killTweensOf(crate)
			}
		})

		return crate
	}
}

We added lines 22 - 24 to listen to the POINTER_DOWN event and then spawn a crate at the pointer location.

Run the Scene and you'll get something this:

The information text shows the size of the pool, the number of active instances spawned, and the number of inactive instances despawned.

You'll notice that the pool only gets bigger when there are no inactive instances to reuse.

Next Steps

That's the basics for using Object Pools with Phaser 3 Groups.

In the next article, we will take this one step further and create an Object Pool class based on Phaser.GameObjects.Group to keep specific spawn and despawn logic in one place.

Using the Phaser Group is not strictly necessary and you can roll your own Object Pool implementation. Let us know in the comments below if you'd like to know more.

If anything doesn’t work or is unclear please leave it in the comments as well. We’ll be happy to fix or clarify!

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 TypeScript Optimization Object Pools

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