Simple Reveal or Scratch-Off Effect in Phaser 3

Fast and lightweight method for reveal effects.

by on 6 minute read


A reveal or scratch-off effect is where a top layer gets removed to reveal something hidden under it.

Like a scratch-off lottery ticket or a “Pick Your Bonus” mini-game.

There's more than one way to achieve this effect and we are going to look at one simple and performant approach using the Phaser 3 RenderTexture and ERASE blend mode.

How it Works

This approach relies on the image to be revealed being the same size and shape or smaller than the covering image. The top image has to completely hide the bottom image.

Cover Reveal Diagram

In this example, we refer to the top image as the “cover” and the bottom image as the “reveal”.

We will place them in the Scene so that the cover is always on top of the reveal. Then we will erase parts of the cover image by using a RenderTexture and another GameObject called a brush.

The brush will be an image of a radial gradient or a transparent image with one dot from a PhotoShop soft brush. The brush's alpha channel is respected so you can get different erase effects depending on the image used.

First, let's set up the 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
import Phaser from 'phaser'

const KEY_PHASER_LOGO = 'phaser-logo'
const KEY_BRUSH = 'brush'

export default class RevealEraseScene extends Phaser.Scene
{
	constructor()
	{
		super('reveal-erase')
	}

	preload()
	{
		this.load.image(KEY_PHASER_LOGO, 'assets/phaser-logo.png')
		this.load.image(KEY_BRUSH, 'assets/brush.png')
	}

	create()
	{
		const x = 400
		const y = 300

		// reveal image
		this.add.image(x, y, KEY_PHASER_LOGO)

		// cover image
		const cover = this.make.image({
			key: KEY_PHASER_LOGO,
			add: false
		})
	}
}

For simplicity, we are using the same Phaser logo texture as the “cover” and “reveal” images. The difference will be a red tint added to the cover image so that we can tell them apart!

Notice that the cover image above is created with this.make.image but not added to the Scene. We will draw it into a RenderTexture instead.

Using a RenderTexture

A RenderTexture is a texture created at runtime that you can draw into. A normal Texture is like the phaser-logo.png that we load from the computer. It was created ahead of time in a graphics program.

This is different from a RenderTexture that does not exist until we run the game and create it in code.

A RenderTexture is empty until we draw something into it. Phaser lets us draw one or more GameObjects into a RenderTexture and then erase from it afterward.

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

const KEY_PHASER_LOGO = 'phaser-logo'
// ...

export default class RevealEraseScene extends Phaser.Scene
{
	// ...

	create()
	{
		const x = 400
		const y = 300

		// reveal image
		this.add.image(x, y, KEY_PHASER_LOGO)

		// cover image
		const cover = this.make.image({
			key: KEY_PHASER_LOGO,
			add: false
		})

		const width = cover.width
		const height = cover.height

		const rt = this.add.renderTexture(x, y, width, height)
		rt.setOrigin(0.5, 0.5)
		rt.draw(cover, width * 0.5, height * 0.5)
		rt.setTint(0xcc0000)
	}
}

We create and add a new RenderTexture to the Scene on line 27. We want to make sure it sits on top of the reveal image so it uses the same x and y values and an origin of 0.5, 0.5.

Then we draw the cover image into the RenderTexture on line 29. Lastly, we tint it red with the hexadecimal value 0xcc0000.

The last two parameters passed to rt.draw() on line 29 is an x and y offset. We can omit this if we change the cover image's origin from a default of 0.5, 0.5 to 0, 0 like this:

cover.setOrigin(0, 0)
// ...
rt.draw(cover)

Which approach you take doesn't matter. Just know that none of this is magic! 🦄 It's just math. 🤓

If you run this you should see a red-tinted Phaser logo.

Erasing with a Brush

Next, it is time to erase from the RenderTexture and reveal the non-tinted Phaser logo.

The interaction for erasing will be moving the mouse while holding down the left mouse button.

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

const KEY_PHASER_LOGO = 'phaser-logo'
const KEY_BRUSH = 'brush'

export default class RevealEraseScene extends Phaser.Scene
{
	constructor()
	{
		super('reveal-erase')

		this.isDown = false
		this.renderTexture = null
		this.brush = null
	}

	// preload...

	create()
	{
		const x = 400
		const y = 300

		// reveal image
		this.add.image(x, y, KEY_PHASER_LOGO)

		// cover image
		const cover = this.make.image({
			key: KEY_PHASER_LOGO,
			add: false
		})

		cover.setOrigin(0, 0)
		
		const width = cover.width
		const height = cover.height

		const rt = this.add.renderTexture(x, y, width, height)
		rt.setOrigin(0.5, 0.5)
		rt.draw(cover) //, width * 0.5, height * 0.5)
		rt.setTint(0xcc0000)

		rt.setInteractive()
		rt.on(Phaser.Input.Events.POINTER_DOWN, this.handlePointerDown, this)
		rt.on(Phaser.Input.Events.POINTER_MOVE, this.handlePointerMove, this)
		rt.on(Phaser.Input.Events.POINTER_UP, () => this.isDown = false)

		this.brush = this.make.image({
			key: KEY_BRUSH,
			add: false
		})

		this.renderTexture = rt
	}

	handlePointerDown(pointer)
	{
		this.isDown = true
		this.handlePointerMove(pointer)
	}

	handlePointerMove(pointer)
	{
		if (!this.isDown)
		{
			return
		}

		const x = pointer.x - this.renderTexture.x + this.renderTexture.width * 0.5
		const y = pointer.y - this.renderTexture.y + this.renderTexture.height * 0.5
		this.renderTexture.erase(this.brush, x, y)
	}
}

First, we add an isDown property on line 12 to track whether the mouse button is pressed. Then we set our RenderTexture to interactive on line 43 and register 3 events: POINTER_DOWN, POINTER_MOVE, and POINTER_UP

The handler functions for POINTER_DOWN and POINTER_MOVE start on lines 56 and 62. The logic for POINTER_UP was so simple that we used a lambda that simply sets isDown to false.

After that, we create a brush and save the RenderTexture for later use in handlePointerMove(pointer).

The revealing logic is on line 71 where we call this.renderTexture.erase() and pass in the brush and location.

And that is the complete code example! You should see something like this:

Next Steps

This object reveal method is simple and lightweight. It works well in cases where the cover image completely hides the reveal image.

For more complicated cases or effects, there is the alpha mask approach that we will demonstrate in the next article.

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 Scratch-Off Magic Lens Flashlight Masks

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