Simple Fog of War Effect for a Phaser 3 Roguelike

Simple and efficient implementation with a RenderTexture and Mask

by on 5 minute read


If you are making a roguelike or dungeon crawler then you'll want the right atmosphere.

Being in a dark dungeon where the only light is from your torch means you should not be able to see the whole room.

It should be revealed to you as you move around.

Enemies won't be visible until they get close.

You'll need to search around and keep a mental map of where everything is.

Isn't this how a dungeon crawler should be?

In this article, we'll show you a relatively simple and efficient way to create that kind of atmosphere for a roguelike or dungeon crawler.

What it looks like

This example assumes you are using tilemaps created with Tiled.

The outcome will look something like this:

We are creating a RenderTexture that is the size of the screen and then using a mask to cut out the part where the player is.

The image used as a mask can be created in PhotoShop using a soft brush with a harder brush in the very center.

The tiles are from the Roguelike Indoors pack from Kenney.

Now let's see what the code looks like.

Create the RenderTexture

Our tilemap has a Floor layer that contains the floor and walls.

We make this part visible as WarCraft or StarCraft does for the general terrain.

The objects and details of the room are revealed by the mask when the player moves near them.

The trick is to create a RenderTexture, fill it with a dark color, draw the Floor layer into it, and then add a dark tint.

Here's how we create it in 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
create()
{
	// assume we have a map setup and created...

	// we will draw this floorLayer into the RenderTexture below
	const floorLayer = map.createStaticLayer('Floor', tileset)
	map.createStaticLayer('Carpet', tileset)
	map.createStaticLayer('Objects', tileset)
	map.createStaticLayer('Details', tileset)

	const width = this.scale.width
	const height = this.scale.height

	// make a RenderTexture that is the size of the screen
	const rt = this.make.renderTexture({
		width,
		height
	}, true)

	// fill it with black
	rt.fill(0x000000, 1)

	// draw the floorLayer into it
	rt.draw(floorLayer)

	// set a dark blue tint
	rt.setTint(0x0a2948)
}

We create the RenderTexture on line 15 and add it to the Scene–that's what true means in the second parameter.

Then we fill it a black color, draw the floorLayer, and set it to a dark blue tint.

You'll end up with just a dark empty room without the Carpet, Objects, or Details layers visible.

Dark empty room

Now we can create some limited visibility by adding a mask to the RenderTexture.

Creating a Mask

Recall that we are using an image with a gradient for the mask.

In this example, we've loaded it in preload() with the key 'vision'.

There is also a player property representing the controllable character.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
create()
{
	// player created here...

	// previous code...

	const vision = this.make.image({
		x: this.player.x,
		y: this.player.y,
		key: 'vision',
		add: false
	})
	vision.scale = 2.5

	rt.mask = new Phaser.Display.Masks.BitmapMask(this, vision)
	rt.mask.invertAlpha = true
}

The image to be used as a mask called vision is created on lines 7 - 12. Note that we have add: false because we do not want to add this Image to the Scene.

We are then scaling the image by 2.5 but you might not need to depending on the size of your image.

Lastly, we create a new BitmapMask using the vision image and set it to the mask property of the RenderTexture.

We also set invertAlpha to true but you may not need to depending on your image.

You should get something like this:

Room with some visibility

The items within the player character's vision as defined by the mask image is visible and the rest of the room is not.

Moving the Mask with the Player

The code above won't automatically move with the player.

You'll have to create a class property to store the mask image and then update its x and y based on the player.

One way to do this is in the Scene's update() method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
update()
{
	// other code...

	if (this.vision)
	{
		this.vision.x = this.player.x
		this.vision.y = this.player.y
	}
}

You'll also need to make this change in create():

1
2
3
4
5
6
7
8
9
create()
{
	const vision = this.make.image({
		x: this.player.x,
		y: this.player.y,
		key: 'vision',
		add: false
	})
}

Now it should work like the first example we showed!

Next Steps

Your game should feel a lot more dungeon-y with just this simple effect!

This should be pretty efficient and performant because the Phaser RenderTexture is itself efficient.

However, don't be tempted to make a RenderTexture the size of your entire tilemap if your game scrolls.

Instead, adjust the position of the mask image based on how much the camera has scrolled. Keep the RenderTexture to the size of your 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 roguelike masks rendertexture dungeon crawler

Want tips and techniques more suited for you?


You may also like...


Video Guides


Beginner Guides


Articles Recommended For You

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

Memory Match in Modern Javascript with Phaser 3 - Part 4

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 …

7 minute read

Didn't find what you were looking for?


comments powered by Disqus