In this post I’m going to walk through a simple solution I made for being able to select units, as well as more application of the UnitActions
script we started last time.
Node2D
does not have a signal emitted when it is clicked. This is the primary problem that we want to solve for our units, since our UnitActions
singleton will want to know when a unit has been selected. This would be easy enough if we only ever wanted units to emit a “clicked” signal, since we could just check the _input()
method inside our Unit
class. But since we’re going to have a lot of interactable things in our game, I thought it best to find a solution that could be reused for multiple different objects.
The thing I think will work best for this particular problem is making a parent class that any script we choose can extend
. In other words, we’re going to use inheritance.
Making The Listener
Go ahead and create a new script and have it inherit from Node2D
. You can name it something that describes its intention, which in our case is going to be detecting mouse input within a certain area (I’m naming mine mouse_listener.gd
). In order to set the size of the relevant area, we’re going to need an export
statement with the width and height. We could achieve this by using two separate variables, width
and height
, but a Vector2
will work just as well for us, so let’s use that.
export(Vector2) var bounds := Vector2.ZERO
This next part is optional, but I think it could come in handy at some point in the future so I’m going to include it. Say we want to only detect a mouse click when the right or middle mouse button is clicked. How can we control that? The solution most (if not all) game engines use is what’s known as a bit mask, and Godot provides a simple way to create one from the editor via an exported variable.
We can declare a bitmask variable by adding the FLAGS
hint to the export
keyword. (“Hints” are what Godot calls declaring a type on the export, as a broad definition.)
export(int, FLAGS, "left", "middle", "right") var button_mask := 1
When you save the script and look in the editor, you’ll see the following.

Detecting the input
Now that we have the bounds
and button_mask
set, let’s use them to determine if a click event happened on our listener object. For this we can override the _unhandled_input()
function (we’re using _unhandled_input()
instead of _input()
because we don’t want this to override any input that may have already been handled).
func _unhandled_input(event):
# Using a match statement here because we'll be listening for
# more than just the mouse button event eventually
match event.get_class():
"InputEventMouseButton":
pass
Now the things we want to know are whether the click event just happened (we don’t want to trigger this event for every frame that the button is held down) which buttons were pressed, and where it occurred. For this we can use the event.is_action_pressed()
function, the event.button_mask
variable, and the event.position
variable.
To check if the button pressed is one of the buttons allowed by our button_mask
variable, we just need to check whether the AND
bitwise operation between the two is greater than 0
.
var is_valid_button = event.button_mask & button_mask > 0
To check if the button was just barely pressed, we’ll need to create a new action in the Input Map of our project. To do this go to Project > Project settings… and click on the Input Map tab. From there, add a new action and assign three buttons to it: Left mouse button, Middle mouse button, and Right mouse button. When finished it should look similar to this.

And in the _unhandled_input()
function we can add this line.
var just_pressed = event.is_action_pressed("mouse_click")
Finally we need to check whether the event happened within the bounds
that we defined. To keep things simple I decided that we would center the bounds on the Node
itself and not deal with offsets (other than the offset to center the bounds). So to make those calculations we can do the following.
func _get_offset_bounds():
# I renamed bounds to size here since it makes
# more sense in this context
var size = bounds
return Rect2(position - size / 2, size)
func _within_bounds(position: Vector2):
return _get_offset_bounds().has_point(position)
After doing all this we’ll know whether we should emit an event on a mouse click. Our script should look something like this when we put it all together.
extends Node2D
class_name MouseListener
signal clicked
export(Vector2) var bounds := Vector2.ZERO
export(int, FLAGS, "left", "middle", "right") var button_mask := 1
func _unhandled_input(event):
match event.get_class():
"InputEventMouseButton":
var just_pressed = event.is_action_pressed("mouse_click")
var is_valid_button = event.button_mask & button_mask > 0
if just_pressed and is_valid_button and _within_bounds(event.position):
emit_signal("clicked", event)
func _get_offset_bounds():
var size = bounds
return Rect2(position - size / 2, size)
func _within_bounds(position: Vector2):
return _get_offset_bounds().has_point(position)
Visualizing the bounds
This is all well and good, but how can we tell where the bounds extend to? Fortunately Godot makes it easy to make our scripts runnable in the editor via the tool
keyword (which we did in an earlier post).
Once we add tool
to the top of the script, let’s also create a setter function for our bounds
variable so that whenever it changes, our setter function will run. In our setter function we’ll call update()
which tells Godot that this node needs to be redrawn.
export(Vector2) var bounds := Vector2.ZERO setget _set_bounds
func _set_bounds(newVal):
bounds = newVal
update()
Then we can override the _draw()
function to easily draw to the screen.
func _draw():
# This tells Godot to only run this part in the editor view
if Engine.editor_hint:
var draw_bounds = _get_offset_bounds()
var color = Color("#e74322")
draw_rect(Rect2(-draw_bounds.size / 2, draw_bounds.size), color, false, 1.5)
Now when you adjust the bounds of the script, you’ll see the area update in real time!

Using The Listener
Now that we’ve got the basic functionality down for our MouseListener
, let’s change our unit.gd
script to extend MouseListener
instead of Node2D
like it was before. Also, if we want to visualize the bounds of the unit we’ll need to add the tool
keyword to this script too.

Now back in the UnitActions
singleton we worked on last time, we’re going to create that function I said not to worry about at the time: _wait_until_unit_selected()
.
The first thing we want to do in the function is get a list of all the units in the scene and connect their clicked
signal to a function. We can get all the units in the current scene the same way we got the list of our debug buttons by adding the units to their own group. Once the function has been connected to all of the units we’ll yield
for a custom signal we define called unit_selected
. When the unit has been selected we’ll disconnect all the units from listening for the clicked
signal, and emit the unit_selected
signal with the selected unit. That signal will resume our code at the yield
statement and we’ll finally return the unit. (Whew!)
Here’s what that will all look like.
signal unit_selected
func _wait_until_unit_selected():
print("Waiting for unit selection...")
var units = _get_all_units()
for unit in units:
unit.connect("clicked", self, "_select_unit", [unit])
var unit = yield(self, "unit_selected")
return unit
func _select_unit(event, unit):
var units = _get_all_units()
for unit in units:
unit.disconnect("clicked", self, "_select_unit")
print("Unit selected: " + unit.name)
emit_signal("unit_selected", unit)
func _get_all_units():
# All units in the scene should belong to the "units" group
return get_tree().get_nodes_in_group("units")
In the next post I’ll be guiding us through adding basic functionality to the “Attack” action. Like last time I don’t have a link to the code for this partial section, but I will provide it in the next post.
Leave a Reply