In this lesson, we’ll create a tooltip, a floating label that’ll give the player information about what a machine is currently doing.
It’ll display its activity and the time left to complete its current job.
In the final demo, I called this the InfoGUI
. It’s a label with a panel that lives as a child of the main GUI
node.
When the player hovers the mouse over an entity or a recipe, we want to display some information.
When not in need or if we have nothing to show, the game hides the label.
To signal to the label that the mouse is hovering over something, we’ll use the event bus from the EntityPlacer
and pass the relevant entity along with the signal.
We’ll also define a function on entities to provide the label with its text description.
Let’s get to it.
We need a signal we can trigger from anywhere to anyone, so let’s go back to the Events.gd
autoload and add a couple of new signals to the list.
We need one for hovering the mouse over an entity and one for when a machine already has its label up, but it has to update.
For example, we can keep the user up to date on the number of power units stored in the battery.
## Emitted when the mouse hovers over any entity. signal hovered_over_entity(entity) ## Emitted when an entity updates its tooltip. signal info_updated(entity)
We’ll return to the script and add more signals as we create the crafting GUI and have machines update their status.
We have two places where we want to keep track of the mouse: the InventoryPanel
and EntityPlacer
. One deals with BlueprintEntity
, and the other with Entity
.
Open up InventoryPanel.gd
, and inside of _gui_input()
, all the way at the end of the function, add some code to detect mouse movement.
func _gui_input(event: InputEvent) -> void: #... # if left_click or right_click: # There are dozens of lines of code in this block. #... # Be sure to place this `elif` block at *one* indent level. elif event is InputEventMouseMotion and is_instance_valid(held_item): Events.emit_signal("hovered_over_entity", held_item)
Because _gui_input()
only reports input events that have to do with any specific panel’s script, it will not trigger if we’re hovering over some other panel. We trigger the event and provide the item in the signal.
In EntityPlacer.gd
, we already detect mouse motion to move the active blueprint preview.
We open EntityPlacer.gd
and head back to that block of code inside _unhandled_input()
. We add a conditional branch for when we are not holding anything in the mouse’s inventory.
func _unhandled_input(event: InputEvent) -> void: #... #elif event.is_action_pressed("right_click") and not has_placeable_blueprint: #... elif event is InputEventMouseMotion: #... if has_placeable_blueprint: #... else: _update_hover(cellv) #... ## Marks the `cell` as hovered if it's within the player's range. func _update_hover(cellv: Vector2) -> void: var is_close_to_player := ( get_global_mouse_position().distance_to(_player.global_position) < MAXIMUM_WORK_DISTANCE ) # If the cell contains an entity and it's in range, we mark it as hovered. if _tracker.is_cell_occupied(cellv) and is_close_to_player: _hover_entity(cellv) else: _clear_hover_entity(cellv) ## Marks the `cellv`'s entity as hovered and emits the `hovered_over_entity` ## signal. func _hover_entity(cellv: Vector2) -> void: var entity: Node2D = _tracker.get_entity_at(cellv) Events.emit_signal("hovered_over_entity", entity) ## Clears any hovered entity and signals the tooltip that we have nothing ## under the mouse. func _clear_hover_entity(cellv: Vector2) -> void: Events.emit_signal("hovered_over_entity", null)
Notice how we split _hover_entity()
and _clear_hover_entity()
into two functions.
This will allow us to return here later and extend them to refine the game’s visuals, for example, by adding outlines to hovered entities.
Many machines update continuously and may have some unique information to display.
For example, the battery stores a certain amount of power, so it’d be useful if we could show how much it currently has.
We open to BatteryEntity.gd
and down in the _on_PowerReceiver_received_power()
and _on_PowerSource_power_updated()
callbacks, we emit the “info_updated” signal.
func _on_PowerReceiver_received_power(amount: float, delta: float) -> void: #... Events.emit_signal("info_updated", self) func _on_PowerSource_power_updated(power_draw: float, delta: float) -> void: #... Events.emit_signal("info_updated", self)
The other machine we have that we can keep up to date is the Stirling engine. Its efficiency starts at 0
and increases over time.
We open StirlingEngineEntity.gd
and edit the way we boot the engine up.
Instead of tweening the efficiency
property directly, we can have the Tween
node call a function instead, where we can do extra work.
In _ready()
, remove the line that animates the efficiency
property: tween.interpolate_property(power, "efficiency", 0, 1, BOOTUP_TIME)
.
Instead, we define a function and call it using Tween.interpolate_method()
.
func _ready() -> void: #... tween.interpolate_method(self, "_update_efficiency", 0.0, 1.0, BOOTUP_TIME) #tween.start() func _update_efficiency(value: float) -> void: power.efficiency = value Events.emit_signal("info_updated", self)
Tween.interpolate_method()
calls a function every frame with the interpolated value, instead of directly assigning it to a property. With a single tween animation, this allows us to execute multiple lines of code.
When in the inventory, it’d be nice to display a tooltip with information about what the player is holding.
Without that, it may not be self-evident to the player that they have a piece of ore or a rock in hand. Or that they can use the object, to begin with.
Head to BlueprintEntity.gd
and add a new variable to describe items.
#... ## Provides a field for information about the blueprint item. What it is and what ## it is used for. export(String, MULTILINE) var description := ""
Now you can open blueprints’ scenes and provide each of them with a description.
The (String, MULTILINE)
export hint results in a large box in the Inspector, allowing you to write long descriptions and insert new lines if you need.
We now have signals coming out of entities and the inventory, providing us with entities that we are touching with the mouse or in need of an update.
In the next part, we will design the tooltip and display info with it.
Here are relevant functions we modified in this lesson. I only included the relevant functions we modified because some scripts are becoming hundreds of lines long.
Events.gd
extends Node signal entity_placed(entity, cellv) signal entity_removed(entity, cellv) signal systems_ticked(delta) signal entered_pickup_area(entity, player) signal hovered_over_entity(entity) signal info_updated(entity)
InventoryPanel._gui_input()
func _gui_input(event: InputEvent) -> void: var left_click := event.is_action_pressed("left_click") var right_click := event.is_action_pressed("right_click") if left_click or right_click: if gui.blueprint: var blueprint_name := Library.get_entity_name_from(gui.blueprint) if held_item: var held_item_name := Library.get_entity_name_from(held_item) var item_is_same_type: bool = held_item_name == blueprint_name var stack_has_space: bool = held_item.stack_count < held_item.stack_size if item_is_same_type and stack_has_space: if left_click: _stack_items() elif right_click: _stack_items(true) else: if left_click: _swap_items() else: if left_click: _grab_item() elif right_click: if gui.blueprint.stack_count > 1: _grab_split_items() else: _grab_item() elif held_item: if left_click: _release_item() elif right_click: if held_item.stack_count == 1: _release_item() else: _split_items() elif event is InputEventMouseMotion and is_instance_valid(held_item): Events.emit_signal("hovered_over_entity", held_item)
EntityPlacer._unhandled_input()
and new functions.
func _unhandled_input(event: InputEvent) -> void: if event is InputEventMouseButton: _abort_deconstruct() var global_mouse_position := get_global_mouse_position() var has_placeable_blueprint: bool = _gui.blueprint and _gui.blueprint.placeable var is_close_to_player := ( global_mouse_position.distance_to(_player.global_position) < MAXIMUM_WORK_DISTANCE ) var cellv := world_to_map(global_mouse_position) var cell_is_occupied := _tracker.is_cell_occupied(cellv) var is_on_ground := _ground.get_cellv(cellv) == 0 if event.is_action_pressed("left_click"): if has_placeable_blueprint: if not cell_is_occupied and is_close_to_player and is_on_ground: _place_entity(cellv) _update_neighboring_flat_entities(cellv) elif event.is_action_pressed("right_click") and not has_placeable_blueprint: if cell_is_occupied and is_close_to_player: _deconstruct(global_mouse_position, cellv) elif event is InputEventMouseMotion: if cellv != _current_deconstruct_location: _abort_deconstruct() if has_placeable_blueprint: _move_blueprint_in_world(cellv) else: _update_hover(cellv) elif event.is_action_pressed("drop") and _gui.blueprint: if is_on_ground: _drop_entity(_gui.blueprint, global_mouse_position) _gui.blueprint = null elif event.is_action_pressed("rotate_blueprint") and _gui.blueprint: _gui.blueprint.rotate_blueprint() func _update_hover(cellv: Vector2) -> void: var is_close_to_player := ( get_global_mouse_position().distance_to(_player.global_position) < MAXIMUM_WORK_DISTANCE ) if _tracker.is_cell_occupied(cellv) and is_close_to_player: _hover_entity(cellv) else: _clear_hover_entity(cellv) func _hover_entity(cellv: Vector2) -> void: var entity: Node2D = _tracker.get_entity_at(cellv) Events.emit_signal("hovered_over_entity", entity) func _clear_hover_entity(cellv: Vector2) -> void: Events.emit_signal("hovered_over_entity", null)
BatteryEntity.gd
’s two callbacks.
func _on_PowerReceiver_received_power(amount: float, delta: float) -> void: self.stored_power = stored_power + amount * delta Events.emit_signal("info_updated", self) func _on_PowerSource_power_updated(power_draw: float, delta: float) -> void: self.stored_power = stored_power - min(power_draw, source.get_effective_power()) * delta Events.emit_signal("info_updated", self)
StirlingEngineEntity.gd
’s _ready()
and _update_efficiency()
func _ready() -> void: animation_player.play("Work") tween.interpolate_property(animation_player, "playback_speed", 0, 1, BOOTUP_TIME) tween.interpolate_property(shaft, "modulate", Color.white, Color(0.5, 1, 0.5), BOOTUP_TIME) tween.interpolate_method(self, "_update_efficiency", 0.0, 1.0, BOOTUP_TIME) tween.start() func _update_efficiency(value: float) -> void: power.efficiency = value Events.emit_signal("info_updated", self)
And in BlueprintEntity.gd
, we only added one line of code:
export(String, MULTILINE) var description := ""