Game Optimization with Object Pools in Phaser 3

Object Pooling in Phaser 3 with a Dedicated Class

by on 7 minute read


Speed is an important performance metric for games. If players have to react in realtime then the framerate is a big deal.

One common cause of framerate problems is memory allocation inside update loops.

A solution to this is to use Object Pools.

We looked at basic Object Pools in the previous article. Be sure to check that out if you are unfamiliar with using a Phaser Group as an Object Pool.

In this article, we will expand upon the basic example with an Object Pool class.

Object Pool Class

Keeping our Object Pool logic in one place is good for reusability, readabiity, and organization.

We will be spawning crates in our CratePool class just like we did in the basic example.

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

const KEY_CRATE = 'crate'

export default class CratePool extends Phaser.GameObjects.Group implements ICratePool
{
	constructor(scene: Phaser.Scene, config: Phaser.Types.GameObjects.Group.GroupConfig = {})
	{
		const defaults: Phaser.Types.GameObjects.Group.GroupConfig = {
			classType: Phaser.GameObjects.Image,
			maxSize: -1
		}

		super(scene, Object.assign(defaults, config))
	}

	spawn(x = 0, y = 0, key: string = KEY_CRATE)
	{
	}

	despawn(crate: Phaser.GameObjects.Image)
	{
	}
}

export {
	KEY_CRATE
}

The ICratePool interface is not strictly necessary but it is often better to program against an interface than an implementation.

If interfaces are confusing then just omit it. 😎 It is not required to understand Object Pools.

In the basic example, we talked about using classType to specify a different type than Phaser.GameObjects.Sprite. Line 10 is how we can do that.

The maxSize property is set to -1 which means no maximum size. You can change this value to ensure the Object Pool never gets too big.

Finally, the spawn() and despawn() methods are where we will add logic to properly reset instances.

Note that we create the KEY_CRATE constant here and export it for others to use. It does not have to be here. An alternative is to put all shared constants in a separate file.

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

const KEY_CRATE = 'crate'

export default class CratePool extends Phaser.GameObjects.Group implements ICratePool
{
	// ...

	spawn(x = 0, y = 0, key: string = KEY_CRATE)
	{
		const crate: Phaser.GameObjects.Image = this.get(x, y, KEY_CRATE)

		crate.setVisible(true)
		crate.setActive(true)

		return crate
	}

	despawn(crate: Phaser.GameObjects.Image)
	{
		this.killAndHide(crate)

		crate.alpha = 1
		crate.scale = 1
	}
}

// ...

This spawn() and despawn() logic is very similar to what we had in the basic example.

We make sure to set the crate to be active and visible when we spawn it. Then on despawn we set it to inactive and invisible with this.killAndHide(crate).

In this example, it does not matter if we set the alpha and scale to 1 in despawn() or spawn(). We put it in despawn() but you can chose either.

Integrating with the GameObjectFactory

The pattern in Phaser is to use the GameObjectFactory to create GameObjects with code like this.add.group().

Phaser doesn't know about our CratePool class so an alternative is to use this.add.existing() and pass in an instance of CratePool. This will work fine but it is not idiomatic to Phaser.

It would be nicer to be able to use code like this.add.cratePool(). We can do this and Phaser makes it easy!

1
2
3
4
5
// put this above `export { KEY_CRATE }`
Phaser.GameObjects.GameObjectFactory.register('cratePool', function () {
	// @ts-ignore
	return this.updateList.add(new CratePool(this.scene));
})

We just need to register a function with the GameObjectFactory that creates a CratePool instance. The exact logic in this function will vary.

Because we are subclassing the Phaser.GameObjects.Group class we can look at what Phaser does and do the same things.

Lastly, TypeScript won't know about the new cratePool() method and complain. We can fix that by using Declaration Merging.

1
2
3
4
5
6
7
8
// put this in a CratePool.d.ts file
declare namespace Phaser.GameObjects
{
	interface GameObjectFactory
	{
		cratePool(): ICratePool
	}
}

Using the CratePool

Using the CratePool is the easy part. This will look similar to the Scene from the basic example.

 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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import Phaser from 'phaser'

import { KEY_CRATE } from './CratePool'

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

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

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

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

	create()
	{
		this.group = this.add.cratePool()

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

		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)
	{
		if (!this.group)
		{
			return null
		}

		const crate = this.group.spawn(x, y)

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

				this.group!.despawn(crate)
			}
		})

		return crate
	}
}

We create a CratePool on line 27 and then use spawn() and despawn() on lines 64 and 74.

Not many lines were saved by the CratePool class but you can use it in another Scene without duplicating code. 👏

You can also add logic like initializing with a starting size or what rule to use when more objects need to be created.

Initialize with Starting Size

It is a common feature of Object Pools to initialize with a set number of premade objects instead of having a lazy pool that only creates instances when necessary.

Different circumstances can benefit from each approach.

We can implement initializing with a starting size like this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import Phaser from 'phaser'

export default class CratePool extends Phaser.GameObjects.Group implements ICratePool
{
	// ...

	initializeWithSize(size: number)
	{
		if (this.getLength() > 0 || size <= 0)
		{
			return
		}

		this.createMultiple({
			key: KEY_CRATE,
			quantity: size,
			visible: false,
			active: false
		})
	}
}

// ...

On line 9 we do a safety check to ensure that we only do this if the pool has not been initialized and some joker didn't pass in a negative number. 😭

The createMultiple method on line 14 is used to create as many new instances as specified by size. We set visible and active to false because we will not be using these instances immediately.

The method can be used in a Scene like this:

1
2
3
4
5
6
7
8
create()
{
	this.group = this.add.cratePool()

	this.group.initializeWithSize(5)

	// ...
}

The information text should show a starting Size value of 5 opposed to 0.

Next Steps

Object Pools are not without drawbacks and, thanks to some very smart people, JavaScript engines are faster than anyone could have imagined a decade ago.

Check you have a performance problem from object creation before you use Object Pools or you'll just add unnecessary complexity.

In the next article, we will look at using Object Pools with Matterjs physics bodies. Pooling simple images is more academic than practical since games are usually more complex than that!

Let us know in the comments below if anything doesn’t work or is unclear. 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