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

Scene Transition with Fade Out in Phaser 3

by on

Are you looking for a quick way to improve Scene transitions? A hard cut from one Scene to another is appropriate at …

6 minute read

Typewriter Effect for Text and BitmapText in Phaser 3

by on

Are you making a story-driven game like an RPG or interactive novel? Having text show up one character at a time is …

6 minute read

Firebase Leaderboard with Rex Plugins in Phaser 3

by on

Are you looking to add more replayability to your game? Leaderboards are a long-standing feature of single-player games …

9 minute read

Create an Animated Health Bar in Phaser 3

by on

Using simple numbers to show health is great for prototypes. It is quick to create when you are testing gameplay. But …

6 minute read

Didn't find what you were looking for?


comments powered by Disqus