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,
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
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
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
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:
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