Mouse: Drag-select multiple units

Problem

You want to click-and-drag to select multiple units, RTS style.

Solution

Realtime strategy (RTS) games often require giving orders to many units at once. A typical style of selecting multiple units is to click-and-drag a box around them. Once the units are selected, clicking on the map commands them to move.

Here’s an example of what we’re going for:

alt alt

Unit setup

To test this out, we’ll need some basic RTS-style units. They are set up to move towards a target and to avoid running into each other. We won’t go into too much detail on them in this tutorial. The unit script is commented if you’d like to use it as a base for creating your own RTS units. See below for a link to download the project.

World setup

Processing the unit selection will happen in the world. We’ll start with a Node2D called “World” and add a few unit instances in it. Attach a script to the World node and add the following variables:

extends Node2D

var dragging = false  # Are we currently dragging?
var selected = []  # Array of selected units.
var drag_start = Vector2.ZERO  # Location where drag began.
var select_rect = RectangleShape2D.new()  # Collision shape for drag box.

Note that once we’ve drawn the box, we’ll need a way to find what units are inside it. The RectangleShape2D will allow us to query the physics engine and see what we collided with.

Drawing the box

We’ll be using the left mouse button for this. Clicking starts a drag and then letting go ends it. During dragging, we’ll draw the rectangle for visibility.

func _unhandled_input(event):
    if event is InputEventMouseButton and event.button_index == BUTTON_LEFT:
        if event.pressed:
            # We only want to start a drag if there's no selection.
            if selected.size() == 0:
                dragging = true
                drag_start = event.position
        elif dragging:
            # Button released while dragging.
            dragging = false
    if event is InputEventMouseMotion and dragging:
        update()

func _draw():
    if dragging:
        draw_rect(Rect2(drag_start, get_global_mouse_position() - drag_start),
                Color(.5, .5, .5), false)

Selecting the units

Now that we’ve got a selection box, we need to find the units that are inside it. When we release the button and the drag ends, we must query the physics space to find the units. Note that the units are KinematicBody2D, but Area2D or other bodies would work as well.

We’ll use Physics2DDirectSpaceState.intersect_shape() to find the units. This requires a shape (our rectangle) and a transform (our location). See Godot docs for details.

elif dragging:
    dragging = false
    update()
    var drag_end = event.position
    select_rect.extents = (drag_end - drag_start) / 2

We start by recording the location when we released the button, and use that to set the RectangleShape2D’s extents (remember: extents are measured from the rectangle’s center, so they’re half the full width/height).

    var space = get_world_2d().direct_space_state
    var query = Physics2DShapeQueryParameters.new()
    query.set_shape(select_rect)
    query.transform = Transform2D(0, (drag_end + drag_start) / 2)
    selected = space.intersect_shape(query)

Now we must get a reference to the physics state and set up our shape query using Physics2DShapeQueryParameters, assigning it our shape, and using the center of the dragged area as the origin for the query’s transform. Our result after calling intersect_shape() is an array of dictionaries, which looks like this:

[{collider:[KinematicBody2D:1149], collider_id:1149, metadata:Null, rid:[RID], shape:0},
{collider:[KinematicBody2D:1144], collider_id:1144, metadata:Null, rid:[RID], shape:0},
{collider:[KinematicBody2D:1154], collider_id:1154, metadata:Null, rid:[RID], shape:0},
{collider:[KinematicBody2D:1159], collider_id:1159, metadata:Null, rid:[RID], shape:0}]

Each of those collider items is a reference to a unit, so we can use this to notify them that they’ve been selected, activating the outline shader:

    for item in selected:
        item.collider.selected = true

alt alt

Commanding the units

Finally, we can command the selected units to move by clicking somewhere on the screen:

func _unhandled_input(event):
    if event is InputEventMouseButton and event.button_index == BUTTON_LEFT:
        if event.pressed:
            if selected.size() == 0:
                dragging = true
                drag_start = event.position
            else:
                for item in selected:
                    item.collider.target = event.position
                    item.collider.selected = false
                selected = []

The else clause here triggers if we click the mouse when selected is greater than 0. Each item’s target is set, and we make sure to deselect the units so we can start again.

Wrapping up

This technique can be expanded to a wide range of RTS or other game styles. Download the full project below and use it as a base for your own game.

Like video?