How to Load Images Dynamically in Phaser 3

Use lazy-loading to download images only when a player needs to see it

by on 6 minute read


If your game uses a ton of images then preloading everything upfront may not be the best idea.

It could be a collectible card game or something similar where not all players will see every card in the game.

Preloading every possible card is unnecessary or even impossible. At the very least it will make players wait much longer than necessary.

One way to solve this problem is to only load images when they will be shown. This is called lazy-loading and will add a bit of extra complexity for the benefit of a more responsive feeling experience.

We'll show you how to do that in this article by going through a 52 card deck and loading the necessary texture if it hasn't been loaded yet.

Example Set-Up

This example uses the phaser3-typescript-parcel-template and the code examples will be in TypeScript.

If you are not familiar with Phaser 3 and 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.

Learn more about the book here.

The concepts and approach should work fine if you are using modern JavaScript or a different project set up.

We'll be using a single Scene and a couple of predefined arrays and helper functions 👇

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

const suits = [
	'Spades',
	'Hearts',
	'Clubs',
	'Diamonds'
]

const values = [
	'2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'
]

const getCardName = (suit: string, value: string) => {
	return `card${suit}${value}`
}

export default class HomeScene extends Phaser.Scene
{
	private suitIndex = 0
	private valueIndex = -1

	private cards: Phaser.GameObjects.Image[] = []

	get currentSuit()
	{
		return suits[this.suitIndex]
	}

	get currentValue()
	{
		if (this.valueIndex < 0)
		{
			return values[0]
		}
		return values[this.valueIndex]
	}

	get currentCardName()
	{
		return getCardName(this.currentSuit, this.currentValue)
	}

	preload()
	{
		// TODO
	}

	create()
	{
		// TODO
	}
}

There is one array that holds the possible card suit values and another with the possible card values from 2 to Ace.

The getCardName() function creates the file name of an image using the given suit and value arguments.

Then the Scene keeps track of the current suit and value indices so that we can deal out cards in order. The 3 other getter properties (currentSuit, currentValue, and currentCardName) are there for convenience.

Using a Placeholder

We have a preload() method even though we'll be loading card images dynamically because we want to have a placeholder to use while the real card is being loaded.

This will add to the feeling of responsiveness as the game will continue operating as normal while images are loading in the background.

We'll switch the placeholder card with the real card once it is loaded.

You can use anything you want but we'll simply use a card back in this example 👇

1
2
3
4
preload()
{
	this.load.image('card-back', 'assets/cardBack_blue1.png')
}

This means we'll use this 'card-back' texture when we deal a card that has not been loaded yet.

Deal a Card

Dealing a card is where the majority of the work will be done.

The idea is to figure out which card should be dealt and then checking if the necessary texture has been loaded yet.

If the texture is already loaded then we can just create a card with it.

If the texture is not already loaded then we need to ask the LoaderPlugin to load it and create a placeholder card.

Then once the card texture we need is loaded we'll swap the placeholder card with the real card.

Here's what it looks like in the deal() method 👇

 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
private deal()
{
	// determine what the next card should be
	++this.valueIndex

	if (this.valueIndex >= values.length)
	{
		this.valueIndex = 0
		++this.suitIndex
	}

	if (this.suitIndex >= suits.length)
	{
		this.suitIndex = 0
	}

	// get the card file name
	const cardName = this.currentCardName
	const { width, height } = this.scale
	const x = width * 0.5
	const y = height * 0.5

	if (this.textures.exists(cardName))
	{
		// texture already exists so just create a card and return it
		return this.add.image(x, y, cardName)
	}

	// texture needs to be loaded to create a placeholder card
	const card = this.add.image(x, y, 'card-back')

	// ask the LoaderPlugin to load the texture
	this.load.image(cardName, `assets/${cardName}.png`)
	this.load.once(Phaser.Loader.Events.COMPLETE, () => {
		// texture loaded so use instead of the placeholder
		card.setTexture(cardName)
	})
	this.load.start()
	
	// return the placeholder card
	return card
}

When developing locally you'll barely see the placeholder card so keep that in mind if it looks like nothing is happening. You can also add a delay before setting the card texture to be sure things are working.

Using the Deal Method

For simplicity, we'll use the left and right arrow keys for dealing a new card and removing the last dealt card.

Add this to create() 👇

 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
create()
{
	const leftArrow = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT)
	leftArrow.on('up', () => {
		this.cards.forEach(card => {
			card.x -= 20
		})

		const card = this.deal()
		this.cards.push(card)
	})

	const rightArrow = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT)
	rightArrow.on('up', () => {
		if (this.cards.length <= 0)
		{
			return
		}

		const card = this.cards.pop()!
		card.destroy()

		this.cards.forEach(card => {
			card.x += 20
		})
	})
}

The left arrow key will deal a new card and move all existing cards 20 pixels to the left.

Then the right arrow key will remove the last dealt card and move all existing cards 20 pixels to the right.

You'll get something that looks like this 👇

Next Steps

You can use this method for loading any asset that Phaser supports from atlases to audio files.

One way to improve the placeholder graphic is to use a loading animation so that players know something is happening or will change.

Another approach is to predict or forecast what assets the player might need soon and then load them as the game is being played. This would create an even more seamless experience!

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 lazy loading loading loading images

Want tips and techniques more suited for you?


You may also like...


Video Guides


Beginner Guides


Articles Recommended For You

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

Memory Match in Modern Javascript with Phaser 3 - Part 6

by on

If you've got the basics of Phaser 3 in modern JavaScript down then it might be time to try making something a bit more …

8 minute read

Memory Match in Modern Javascript with Phaser 3 - Part 5

by on

If you've got the basics of Phaser 3 in modern JavaScript down then it might be time to try making something a bit more …

6 minute read

Didn't find what you were looking for?


comments powered by Disqus