Add Pizazz with Parallax Scrolling in Phaser 3

Create the illusion of depth and greater immersion in 2D games

by on 6 minute read


Are you looking to create a sense of depth in a 2D side-scrolling game?

Or just a more immersive and believable experience?

You've probably heard about parallax scrolling. It is a technique where layers in the back move slower than layers in the front.

Phaser 3 makes this pretty simple to create with setScrollFactor().

Check out this video for implementing parallax scrolling if that's more your preference!

If you prefer to read then this article will show you how to add a parallax scrolling background.

Demo Scene Setup

The demo here we will be using background assets by MarwaMJ.

If you are using different assets then the key is to have the layers separated and repeatable so that they can be scrolled separately like this:

The code in this article will be within the context of this ParallaxDemo Scene:

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

export default class ParallaxDemo extends Phaser.Scene
{
	init()
	{
		this.cursors = this.input.keyboard.createCursorKeys()
	}

	preload()
	{
		// load background images...
	}

	create()
	{
		// setup background...
	}

	update()
	{
		const cam = this.cameras.main
		const speed = 5

		if (this.cursors.right.isDown)
		{
			cam.scrollX += speed
		}
		else if (this.cursors.left.isDown)
		{
			cam.scrollX -= speed
		}
	}
}

Your existing game Scene will likely look very different. The camera probably follows a player and the CursorKeys are used to move the player.

We are including this Scene code to help you better adapt the rest of the examples to your game.

The last thing before we get into parallax scrolling is to preload the various background layer images.

1
2
3
4
5
6
7
8
preload()
{
	this.load.image('sky', 'assets/sky.png')
	this.load.image('mountains', 'assets/mountains.png')
	this.load.image('plateau', 'assets/plateau.png')
	this.load.image('plants', 'assets/plant.png')
	this.load.image('ground', 'assets/ground.png')
}

We have 5 different layers. You might have more or less but the concepts will be the same.

Add a Non-Scrolling Layer

For an outdoor environment, the sun in the sky will likely never move. This is the case for our sky.png. It will be placed first and then set to never scroll.

1
2
3
4
5
6
7
8
create()
{
	const width = this.scale.width
	const height = this.scale.height

	this.add.image(width * 0.5, height * 0.5, 'sky')
		.setScrollFactor(0)
}

The important part is on line 7 where we use setScrollFactor(0). This will keep the sky image from scrolling when the camera moves.

Add a Scrolling Layer

Our first scrolling layer will be mountains and they will scroll slowly.

1
2
3
4
5
6
7
8
create()
{
	// previous code...

	this.add.image(0, height, 'mountains')
		.setOrigin(0, 1)
		.setScrollFactor(0.25)
}

Notice that we've set the origin of the mountains image to (0, 1) and placed it at the bottom left corner of the Scene.

We do this to make it easier to reason about positioning the various background layers.

The mountains.png image expects to be flush against the bottom of the screen so using a (0, 1) origin makes it easy to place.

Instead of calculating half heights or quarter heights, we can just use a position of (0, height).

Then we set the scrollFactor to 0.25. This makes it 4x slower than the camera. Feel free to adjust this value to your liking.

Try it out and you should see this layer scroll slowly over the sky.

The only problem is that it doesn't repeat so you'll eventually run out of mountains. 😨

Repeating Layers

There are multiple ways to handle repeating the mountains layer so that it is visible throughout the entire level.

A simple way is to create as many instances as we need to fill the size of the level.

Other layers will also need to do this so let's make a function that will tile an image as many times as we need depending on the size of the level.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// add this to the same file or a separate file and import it

/**
 * 
 * @param {Phaser.Scene} scene 
 * @param {number} totalWidth 
 * @param {string} texture 
 * @param {number} scrollFactor 
 */
const createAligned = (scene, totalWidth, texture, scrollFactor) => {
	const w = scene.textures.get(texture).getSourceImage().width
	const count = Math.ceil(totalWidth / w) * scrollFactor

	let x = 0
	for (let i = 0; i < count; ++i)
	{
		const m = scene.add.image(x, scene.scale.height, texture)
			.setOrigin(0, 1)
			.setScrollFactor(scrollFactor)

		x += m.width
	}
}

We add a function called createAligned() that takes the Scene, the total width of the level, a texture key, and the desired scroll factor.

The width of the texture is retrieved using its source image on line 11. We use it to determine how many instances of the image we need to cover the entire width of the level.

That value is stored in count by dividing the totalWidth by the width of the texture. We use Math.ceil() to round the number up to ensure the full width is accounted for. The result is then multiplied by the scrollFactor.

We do this multiplication so that slower-moving layers can have less Image instances and faster-moving layers will have more.

After that, we use a for loop to create the Image instances. Each image is placed to the right of the previous by adding the width of the last created Image to the x variable.

Add Repeated Scrolling Layers

Adding parallax scrolling for all your background layers will now be super simple with the createAligned() function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
create()
{
	const width = this.scale.width
	const height = this.scale.height
	const totalWidth = width * 10

	this.add.image(width * 0.5, height * 0.5, 'sky')
		.setScrollFactor(0)

	createAligned(this, totalWidth, 'mountain', 0.25)
	createAligned(this, totalWidth, 'plateau', 0.5)
	createAligned(this, totalWidth, 'ground', 1)
	createAligned(this, totalWidth, 'plants', 1.25)
}

We create a totalWidth variable to hold a demo value for the level width on line 5. Your actual game will probably be different.

Then we simply call createAligned() for every background layer we have with the desired scrollFactor.

Next Steps

That's all there is to parallax scrolling. It is an effect that has a large bang for your buck or return on investment.

Not very complex to implement but creates a much more immersive and believable experience!

Give the YouTube video a try if you find yourself having trouble. It's the same code and concepts but we go through it step-by-step in real-time.

If your game is very large then consider culling images that are not shown by the camera or recycle a smaller number of background layer images by looping them around when they scroll off the screen.

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 parallax scrolling

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