Field of View for a Roguelike or Dungeon Crawler in Phaser 3

A fast and realistic 2D field of view effect with the MRPAS algorithm

by on 7 minute read


Are you are making a roguelike or dungeon crawler?

Do you want the player to only see what is visible based on where they are standing?

Where enemies or items behind a wall, in the next room, or around the corner should be hidden until the player has line of sight?

Then what you need is a proper field of view algorithm.

Another alternative is this simpler fog of war effect if you don't need a proper field of view..

In this article, we will go over using the MRPAS algorithm to implement a field of view effect in Phaser 3 with tilemaps.

MRPAS

MRPAS is a restrictive field of view algorithm that stands for Mingo's Restrictive Precise Angle Shadowcasting.

This algorithm generally produces a visually appealing and more natural-looking field of view than other algorithms and is popular amongst roguelikes.

It is probably used in a roguelike you recently played!

In this example, we will be using the mrpas NPM package.

You can install it in your project using this command:

npm install mrpas

Setting Up Mrpas in a Scene

Our example will be in TypeScript. You can ignore all the types if you are using modern JavaScript.

If you are using legacy JavaScript then… what are you waiting for? 🤔

We have a free book to help you get started with modern JavaScript in Phaser 3 so there's no excuse. Get the book and upgrade yourself!

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.

Note that we are going to skip over the tilemap set up code and focus on using the mrpas library.

Here's what the Scene looks like:

 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
// other imports...

import { Mrpas } from 'mrpas'

export default class Game extends Phaser.Scene
{
	private fov?: Mrpas

	private map?: Phaser.Tilemaps.Tilemap
	private groundLayer?: Phaser.Tilemaps.DynamicTilemapLayer

	// other properties and preload()

	create()
	{
		// create map and tileset...

		// using a BlankDynamicLayer for procedural dungeon generation
		this.groundLayer = this.map.createBlankDynamicLayer('Ground', tileset)

		// generate ground tile layer procedurally...

		this.fov = new Mrpas(this.map.width, this.map.height, (x, y) => {
			const tile = this.groundLayer!.getTileAt(x, y)
			return tile && !tile.collides
		})
	}
}

We are importing the mrpas library on line 3.

Then on lines 23 - 26 we create an Mrpas instance and store it in the fov class property.

The Mrpas constructor takes width, height, and a function used to determine if a tile can be seen through. The opposite of things like a wall or door–unless you are Superman.

We set it to the inverse of tile.collides which means anything that cannot collide with the player can be seen through.

For example, a floor tile will not block vision. A wall tile will block vision.

Using the Field of View

Getting the field of view effect to work takes just 3 steps:

1. Hide all the tiles.
2. Use MRPAS to calculate the tiles within the field of view.
3. Only make those tiles visible.

To start, add this method to your 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
35
36
37
38
private computeFOV()
{
	if (!this.fov || !this.map || !this.groundLayer || !this.player)
	{
		return
	}

	// get camera view bounds
	const camera = this.cameras.main
	const bounds = new Phaser.Geom.Rectangle(
		this.map.worldToTileX(camera.worldView.x) - 1,
		this.map.worldToTileY(camera.worldView.y) - 1,
		this.map.worldToTileX(camera.worldView.width) + 2,
		this.map.worldToTileX(camera.worldView.height) + 3
	)

	// set all tiles within camera view to invisible
	for (let y = bounds.y; y < bounds.y + bounds.height; y++)
	{
		for (let x = bounds.x; x < bounds.x + bounds.width; x++)
		{
			if (y < 0 || y >= this.map.height || x < 0 || x >= this.map.width)
			{
				continue
			}

			const tile = this.groundLayer.getTileAt(x, y)
			if (!tile)
			{
				continue
			}

			tile.alpha = 0
		}
	}

	// calculate fov here...
}

Notice that we are only turning off tiles that the camera can see. Anything outside of the current camera view is ignored.

To hide the tile we simply set alpha to 0 on line 33.

Add a call to this.computePOV() in the Scene's update() method and your tiles should no longer be visible.

Next, let's calculate FOV and turn on the visible tiles.

 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
private computeFOV()
{
	// previous code...

	// get player's position
	const px = this.map.worldToTileX(this.player.x)
	const py = this.map.worldToTileY(this.player.y)
	
	// compute fov from player's position
	this.fov.compute(
		px,
		py,
		7,
		(x, y) => {
			const tile = this.groundLayer!.getTileAt(x, y)
			if (!tile)
			{
				return false
			}
			return tile.alpha > 0
		},
		(x, y) => {
			const tile = this.groundLayer!.getTileAt(x, y)
			if (!tile)
			{
				return
			}
			tile.alpha = 1
		}
	)
}

We get the current position of the player and then use it as the origin in our call to this.fov.compute() on line 10.

The number 7 on line 13 is the radius. Make this bigger or smaller to adjust the field of view size. You can also use Infinity.

The next two arguments are where the magic happens.

First, is a function that should return whether a tile is currently visible or not. That is determined by alpha greater than 0.

Next, is a function that sets a tile to be visible. We do that by setting the alpha to 1.

Try this out and tiles within the field of view will appear while others will disappear.

Simple Improvements

We can make this effect a little nicer by tinting invisible tiles a dark color and fading out tiles furthest from the player.

Only 3 changes are needed:

 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
private computeFOV()
{
	// get camera view bounds...

	// set all tiles within camera view to invisible
	for (let y = bounds.y; y < bounds.y + bounds.height; y++)
	{
		for (let x = bounds.x; x < bounds.x + bounds.width; x++)
		{
			// previous checks...

			tile.alpha = 1
			tile.tint = 0x404040
		}
	}

	// calculate fov...

	this.fov.compute(
		px,
		py,
		7,
		(x, y) => {
			const tile = this.groundLayer!.getTileAt(x, y)
			if (!tile)
			{
				return false
			}
			return tile.tint === 0xffffff
		},
		(x, y) => {
			const tile = this.groundLayer!.getTileAt(x, y)
			if (!tile)
			{
				return
			}

			const d = Phaser.Math.Distance.Between(py, px, y, x)
			const alpha = Math.min(2 - d / 6, 1)

			tile.tint = 0xffffff
			tile.alpha =  alpha
		}
	)
}

First, instead of setting alpha = 0 when we loop through the visible tiles, we set alpha = 1 and add a tint of 0x404040 on lines 12 and 13.

Then we change our check for whether a tile is visible or not from tile.alpha > 0 to tile.tint === 0xffffff. This means a tile with no tint or a white tint is assumed to be visible.

Lastly, we adjust alpha based on the distance from the player for tiles that are in the field of view on lines 38 - 42.

You should get something that looks like this:

Next Steps

We used a single tile layer to keep the example code simple. You will have to handle any other layers that you might have.

There are more things you can add like fading the tiles as they go in and out of visibility.

An alternative to tinting tiles is to add another layer with only black tiles that you fade in and out. This can also be used to keep unexplored sections hidden.

A great example of these features is Dungeon Dash.

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 field of view fov algorithms roguelike dungeon crawler

Want tips and techniques more suited for you?


You may also like...


Video Guides


Beginner Guides


Articles Recommended For You

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

Add Pizazz with Parallax Scrolling in Phaser 3

by on

Are you looking to create a sense of depth or add a more immersive and believable experience to a 2D game? You may have …

6 minute read

How to Make a Particle Trail Effect in Phaser 3

by on

Does your game feel too static or not reactive enough? Do coins just disappear when the player collects them? A greater …

7 minute read

State Pattern for Changing AI and Player Control in Phaser 3

by on

Are you wondering about code structure and best practices? You've probably come across design patterns but which design …

10 minute read

Didn't find what you were looking for?


comments powered by Disqus