Detect Overlap Between Selection Box and Sprites in Phaser 3

Use a selection rectangle for multi-unit selection in an RTS-style game

by on 6 minute read


Are you building an RTS game where you can select multiple units by dragging a selection box over them?

Have you seen an example for Arcade Physics Sprites but are using a different physics engine or no engine at all?

Did your initial attempt result in a box that only works when it is created by dragging from the top left to the bottom right but not in reverse?

In this article, we'll go over how to multi-select Arcade Physics Sprites and regular Sprites with a selection box.

And have it work no matter what direction it was created in! Something like this:

Creating a Selection Box

First, we need a selection box. We'll use a blue Rectangle at 50% opacity for this and store it as a class property of the Scene.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
export default class SelectionDemoScene extends Phaser.Scene
{
	/** @type {Phaser.GameObjects.Rectangle} */
	selection

	create()
	{
		// level setup or other code...

		this.selection = this.add.rectangle(0, 0, 0, 0, 0x1d7196, 0.5)
	}
}

The comments above the selection property declaration are JSDoc annotations to help denote type.

Notice that we set the Rectangle position and size to be 0.

We will adjust where it is and how big it should be in the POINTER_DOWN and POINTER_MOVE events.

 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
create()
{
	// level setup or other code...

	this.selection = this.add.rectangle(0, 0, 0, 0, 0x1d7196, 0.5)

	this.input.on(Phaser.Input.Events.POINTER_DOWN, this.handlePointerDown, this)
	this.input.on(Phaser.Input.Events.POINTER_MOVE, this.handlePointerMove, this)
	this.input.on(Phaser.Input.Events.POINTER_UP, this.handlePointerUp, this)
}

/**
 * @param {Phaser.Input.Pointer} pointer
 * @param {Phaser.GameObjects.GameObject[]} currentlyOver
*/
handlePointerDown(pointer, currentlyOver)
{
}

/**
 * @param {Phaser.Input.Pointer} pointer
 * @param {Phaser.GameObjects.GameObject[]} currentlyOver
*/
handlePointerMove(pointer, currentlyOver)
{
}

/**
 * @param {Phaser.Input.Pointer} pointer
 * @param {Phaser.GameObjects.GameObject[]} currentlyOver
*/
handlePointerUp(pointer, currentlyOver)
{
}

We add event listeners for POINTER_DOWN, POINTER_MOVE, and POINTER_UP on lines 7 - 9.

Then we add the corresponding handler methods to the Scene class.

Similar to the selection property declaration above, the comments above each handler method are JSDoc annotations to help denote the type of parameters.

Let's implement each handler method in turn. The handlePointerDown method is the simplest:

1
2
3
4
5
handlePointerDown(pointer, currentlyOver)
{
	this.selection.x = pointer.x
	this.selection.y = pointer.y
}

This will position our selection box to where the mouse was first pressed.

Then in handlePointerMove, we will set the width and height based on how far the mouse was moved.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
handlePointerMove(pointer, currentlyOver)
{
	if (!pointer.isDown)
	{
		return
	}

	const dx = pointer.x - pointer.prevPosition.x
	const dy = pointer.y - pointer.prevPosition.y

	this.selection.width += dx
	this.selection.height += dy

	const selected = this.physics.overlapRect(
		this.selection.x,
		this.selection.y,
		this.selection.width,
		this.selection.height
	)

	// do something with selected
}

First, we only update the selection box if the mouse pointer is down as checked on line 3.

Then on lines 8 and 9, we calculate how much the mouse has moved since the last time the POINTER_MOVE event was fired.

We add this change in movement to the width and height of the selection box to update its size.

Lastly, we use this.physics.overlapRect() with the dimensions of the selection box to determine the physics Sprites that are selected.

This does not account for creating a selection box by dragging from the bottom right to the top left. We will handle that next.

But before that, here's what handlePointerUp looks like:

1
2
3
4
5
handlePointerUp(pointer, currentlyOver)
{
	this.selection.width = 0
	this.selection.height = 0
}

We simply set the width and height to 0 to hide it.

Handling a Reversed Selection Box

You can try creating a selection box in reverse with the implementation we have now to see what happens.

The selected variable will be an empty list.

This is because the width and height of the selection box is negative!

The fix is actually pretty simple. We just need to check if width or height is negative and then adjust.

But we should do this adjustment on a new Phaser.Geom.Rectangle and not this.selection.

 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
handlePointerMove(pointer, currentlyOver)
{
	if (!pointer.isDown)
	{
		return
	}

	const dx = pointer.x - pointer.prevPosition.x
	const dy = pointer.y - pointer.prevPosition.y

	this.selection.width += dx
	this.selection.height += dy

	// create a new Rectangle
	const selectionRect = new Phaser.Geom.Rectangle(
		this.selection.x,
		this.selection.y,
		this.selection.width,
		this.selection.height
	)

	// check if width or height is negative
	// and then adjust
	if (selectionRect.width < 0)
	{
		selectionRect.x += selectionRect.width
		selectionRect.width *= -1
	}
	if (selectionRect.height < 0)
	{
		selectionRect.y += selectionRect.height
		selectionRect.height *= -1
	}

	// use the new Rectangle to check for overlap
	const selected = this.physics.overlapRect(
		selectionRect.x,
		selectionRect.y,
		selectionRect.width,
		selectionRect.height
	)

	// do something with selected
}

We create a new Phaser.Geom.Rectangle on line 15 as a data representation of the selection box.

You can also use a plain JavaScript object but the key is to not modify properties on this.selection because they are used for proper rendering.

Don't just take my word for it! Try it for yourself and see what happens. 😜

Then on lines 24 and 29, we check if width or height is negative. If so, we adjust the Rectangle by adding the width to x or height to y and then turn the negative value into a positive one.

This will create an equivalent Rectangle with the x and y at the top left.

Finally, we use the values from the Rectangle on lines 37 - 40 where we previously used the selection box.

Now we can handle a selection box no matter how it is created!

What about regular Sprites?

To handle non-physics Sprites we'll have to store each sprite in a list or set.

If you are using a Group then just get the list of children with getChildren().

The code will be mostly the same as what we already have except we exchange this.physics.overlapRect() with this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
handlePointerMove(pointer, currentlyOver)
{
	// previous code...

	// remove call to this.physics.overlapRect()

	// assume we have a class property called tanks with Sprites in it
	const selected = this.tanks.filter(tank => {
		const rect = tank.getBounds()

		return Phaser.Geom.Rectangle.Overlaps(selectionRect, rect)
	})

	// do something with selected...
}

Assume that we have a class property called tanks with different Sprites in it.

We iterate over this.tanks and check for the ones where the bounds overlap the selectionRect created earlier.

The bounds is retrieved on line 9 using getBounds() and then the overlap check is on line 11 with Phaser.Geom.Rectangle.Overlaps().

Next Steps

Now you have a robust selection box that will work for any Sprite and can be created from left to right, bottom to top, right to left, or top to bottom!

What you do with these selected Sprites is up to you!

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 selection box rts

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