Quicksand Effect for Platformers using Arcade Physics in Phaser 3

Give players a sinking feeling when they stand on some sandy tiles

by on 8 minute read


The quicksand effect is somewhat common in platformers but you probably haven't found too many tutorials for it. 🤔

Unlike something simpler such as water or lava, quicksand might take a few days to figure out and implement.

In this article, we'll show you how to create a basic quicksand effect for a Phaser 3 game using Arcade Physics!

Example Overview

We will assume that you are making a platformer with player logic already implemented and using tilemaps but not necessarily from Tiled.

The code will be in modern JavaScript with ES6 classes and JSDoc annotations to help with type information.

If you are not familiar with using modern JavaScript with Phaser 3 then we have a free book to help you get started!

Learn to make an Infinite Jumper in Phaser 3 with modern JavaScript!

Drop your email into the box below to get this free 60+ page book and join our newsletter.

Learn more about the book here.

Scene set up code has been omitted so that we can focus directly on creating a Quicksand class that can be used in a Scene you already have.

Creating the Quicksand Class

The Quicksand class will create an object that is placed over the tiles representing quicksand.

We don't have any animations in our tiles but they can be added separately from the quicksand logic.

This is what the quicksand effect will look like 👇

Let's start with a basic skeleton:

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

export default class Quicksand
{
	/** @type {Phaser.GameObjects.Rectangle} */
	display

	/**
	 * @param {Phaser.Scene} scene 
	 * @param {number} x
	 * @param {number} y
	 * @param {number} width
	 * @param {number} height
	 */
	constructor(scene, x, y, width, height)
	{
		this.display = scene.add.rectangle(x, y, width, height, 0, 0)
			.setOrigin(0, 1)

		scene.physics.add.existing(this.display, true)
	}
}

Notice that we are not extending from an existing Phaser class. Instead, we are just going to create the visual elements we need and keep a reference to them.

In the constructor, we create an invisible Rectangle on line 17 with an origin set to the bottom left corner. This is for ease of positioning in this example. You may prefer to keep it t at (0.5, 0.5) or (0, 0).

Then we inject Arcade Physics components into the Rectangle on line 20. We'll add a collider with the player instance in the Game Scene later.

Creating the Quicksand Effect

With a physics-enabled Rectangle, the player will be able to stand on it but not sink into it. The sinking effect will be created by adjusting the size of the Rectangle collision box.

We'll start making the Rectangle smaller as soon as the player collides with it. Then if the player jumps or is no longer touching the collision box, we will expand the size of the Rectangle.

Quicksand is also harder to move through so we will clamp how high the player can jump while in quicksand.

Let's start by adding a handleCollidePlayer() method and a touchingPlayer class property to store the player instance when it collides with the Rectangle:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export default class Quicksand
{
	/** @type {Phaser.Physics.Arcade.Sprite} */
	touchingPlayer

	// other code...

	/**
	 * 
	 * @param {Phaser.GameObjects.GameObject} obj 
	 * @param {Phaser.Physics.Arcade.Sprite} player 
	 */
	handleCollidePlayer(obj, player)
	{
		if (this.touchingPlayer)
		{
			return
		}

		this.touchingPlayer = player
	}
}

Note that this quicksand example assumes only 1 player character. There will be more work involved to have it support multiple players and is outside the scope of this article.

The handleCollidePlayer() method will be set as the callback when we create a collider in the Game Scene.

Next, let's create a changeCollisionBoxBy(diff) method that will adjust the size of the Rectangle collision box. We'll use it in the update() method to make the collision box bigger or smaller depending on what the player is doing.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
changeCollisionBoxBy(diff)
{
	/** @type {Phaser.Physics.Arcade.StaticBody} */
	// @ts-ignore
	const body = this.display.body

	if (diff > 0 && body.height >= this.display.height)
	{
		return
	}

	if (diff < 0 && body.height <= 0)
	{
		return
	}

	body.setSize(body.width, body.height + diff)
	body.setOffset(
		0,
		this.display.height - body.height
	)
}

Notice that we don't do anything if the collision box is as big as the display's original height or if it is already zero or less. These two checks are on lines 7 and 12.

Then on line 18, we set the offset so that the collision box changes height while being anchored to the bottom. Without this offset, you'll see that the height shrinks and expands from the middle.

Now, we can implement the core quicksand logic in update():

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

	let change = -0.5

	const rect = this.display.getBounds()
	const playerRect = this.touchingPlayer.getBounds()
	const overlapping = Phaser.Geom.Rectangle.Overlaps(rect, playerRect)

	const touching = !this.touchingPlayer.body.touching.none
	const wasTouching = !this.touchingPlayer.body.wasTouching.none
	const jumpingOrFalling = this.touchingPlayer.body.velocity.y < 0 || this.touchingPlayer.body.velocity.y > 10

	if (overlapping && jumpingOrFalling)
	{
		change = 0.5
	}

	this.changeCollisionBoxBy(change)

	// adjust max jump velocity
	if (this.touchingPlayer.body.velocity.y < -100)
	{
		this.touchingPlayer.setVelocityY(-100)
	}

	// if not overlapping and not touching then out of quicksand
	if (!overlapping && (!touching && !wasTouching))
	{
		this.touchingPlayer = undefined

		// reset to full height
		/** @type {Phaser.Physics.Arcade.StaticBody} */
		// @ts-ignore
		const body = this.display.body
		this.changeCollisionBoxBy(this.display.height - body.height)
	}
}

First, we early exit and do nothing if this.touchingPlayer is not set. This means that the player is not currently in a quicksand area.

Then we do some logic to determine if the player is touching the collision box or within the bounds of the quicksand display.

We do this because the player can be surrounded by quicksand while jumping. This would result in the player not colliding with the collision box but still being affected by quicksand.

The collision box will continue to shrink as long as the player is touching it unless a jump occurs. Then the collision box will expand instead. This will allow the player to eventually jump out of the quicksand even though jump height is reduced.

That jump height reduction happens on line 26.

Finally, the last check on line 32 will determine when the player has jumped out of the quicksand area and should no longer be affected. We remove the reference to the player and immediately set the collision box to full size.

If the player lands on the quicksand again then the sinking effect will start over from the beginning.

Using the Quicksand Class in a Scene

The Quicksand class can be easily used by creating a new instance in the create() method like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// import at top
import Quicksand from './Quicksand'

// then in the Scene's create()
create()
{
	// other code...

	const quicksand = new Quicksand(this, 192, height, 448, 128)
	
	this.physics.add.collider(quicksand.display, player, quicksand.handleCollidePlayer, null, quicksand)
}

We are assuming that the player instance is stored in a player variable. Your actual game code may be different.

Notice that we use the handleCollidePlayer() method from the Quicksand class as the collider callback. We also pass in the quicksand reference as the context argument.

The position and size values that we are using to create a new Quicksand instance are just for this example and your game code will probably be different.

If you are using an editor like Tiled then you will want to use objects on an Object Layer. That data can then be used to create a Quicksand instance with the right position and size.

Lastly, we'll want to call the update() method on the quicksand instance in the Scene's update() method. You may need to create a class property with the quicksand reference to do this.

Be sure that the player's movement logic is run before the call to update() on the Quicksand instance. Otherwise, clamping the player's movement speed from the Quicksand class won't work.

Multiple Quicksands

Having 1 Quicksand object is a good example but not realistic for a game.

Odds are your level will have a few quicksand areas for the player to deal with.

The Quicksand class we made easily supports multiple instances. All you need to do is to add a collider and call the update() method for each created Quicksand instance.

It'll look something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
create()
{
	// other code...

	const quicksand1 = new Quicksand(this, 192, height, 192, 128)
	const quicksand2 = new Quicksand(this, 448, height, 192, 128)

	this.quicksands.push(quicksand1)
	this.quicksands.push(quicksand2)

	this.physics.add.collider(quicksand1.display, player, quicksand1.handleCollidePlayer, null, quicksand1)
	this.physics.add.collider(quicksand2.display, player, quicksand2.handleCollidePlayer, null, quicksand2)
}

Then in the Scene's update():

1
2
3
4
5
6
update()
{
	// other code...

	this.quicksands.forEach(qs => qs.update())
}

The main difference to note is that we created an Array class property called quicksands to store each created Quicksand instance.

The end result should look something like this 👇

Next Steps

Quicksand is definitely more complicated than just adjusting physics for water or taking damage from landing in lava but not impossible!

We didn't go over how to determine when the player is so deep that they suffocate and we'll leave that to you.

Other physics values can be adjusted or clamped when the player is inside of quicksand as well. It may be similar to how we clamp the player's jump height.

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 arcade physics quicksand platformer

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