Let’s continue our work on the tooltip system.
Now that we have signals coming out of the events bus, we need something to receive them. This will be a new scene, InfoGUI.tscn
.
Create a new scene with a PanelContainer
as its root node named InfoGUI. In our theme, PanelContainer
has a darker color and is slightly transparent, making it perfect for a tooltip.
Add a MarginContainer
node as its child and set its Custom Constants margins to 10
each. This keeps the information text from butting up against the panel’s edges, which might hurt readability.
Finally, add a Label
node as a child of the MarginContainer
for the actual text content.
Select all three nodes in the scene and set their Mouse -> Filter to Ignore, so tooltips don’t intercept mouse events. We’ll place the tooltip at an offset from the mouse, so this shouldn’t be an issue.
However, in your games, you may end up in cases where the mouse has to be over the panel, and you don’t want them to block clicks. This can happen if you write code to keep tooltips within the window’s bounds.
You can save the scene in the GUI/
directory.
Add a new script to the InfoGUI
panel node to connect to Events
’ signals and display some information.
At this point, your scene should look like this.
As ever, we start with doing some setup. We need to set some data and ensure the panel is in the right place when we need it to appear.
extends PanelContainer ## A constant amount of pixels to nudge the panel up and to the right by. ## That way, it isn't directly under the mouse. const OFFSET := Vector2(25, -25) ## The current entity being hovered. Whenever the mouse moves, a new signal will ## be triggered even if the mouse did not move off of the current entity. This ## would result in unwanted flicker. We keep track of the current entity so we ## can choose not to update if we don't need to. var current_entity: Node onready var label := $MarginContainer/Label func _ready() -> void: # Just like the drag preview, the label will be forever glued to the mouse # position. We don't want it to be affected by its GUI parent. set_as_toplevel(true) # Connect to the events we added to the event bus. Events.connect("hovered_over_entity", self, "_on_hovered_over_entity") Events.connect("info_updated", self, "_on_info_updated") # Make sure that the entity is out of the way when the game first starts. hide() ## Every frame, make sure that the panel is by the mouse. func _process(_delta: float) -> void: rect_global_position = get_global_mouse_position() + OFFSET
We have these signal functions we need to code, and we need to display the data when that signal occurs. We can wrap that up inside of a helper function, _set_info()
.
## Display the name of the entity and any special text that should accompany it. func _set_info(entity: Node) -> void: # Get the name of the entity, then use the `capitalize()` function to turn # it into a human readable format. It replaces underscores with spaces and # splits camel case. So our StirlingEngine will become Stirling Engine. var entity_filename: String = Library.get_entity_name_from(entity).capitalize() var output := entity_filename # If the entity is a blueprint, get its description and append it to the # name on a new line. if entity is BlueprintEntity: output += "\n%s" % entity.description else: # Otherwise, attempt to get the entity's data using a `get_info()` # method, but only if it exists, and append the result onto a new line. if entity.has_method("get_info"): var info: String = entity.get_info() if not info.empty(): output += "\n%s" % info label.text = output # We finally make the tooltip visible as it now has text to display. show() ## Sets or clears the current entity, and either shows or hides the label ## depending if there is an entity or not. func _on_hovered_over_entity(entity: Node) -> void: # Keep up to date on which entity is currently being updated. if not entity == current_entity: current_entity = entity # If there is no entity, clear the text and hide the label. if not entity: label.text = "" hide() # if there is one, set the text and reset the size of the label. else: _set_info(entity) # We use `set_deferred()` to attempt to resize the panel to `0` *next # frame*. Changing the rect_size of a container to 0 will force it to # update to its minimum size, which should fit the label snuggly. # # We use `set_deferred()` because we are changing the text this frame, # but the GUI system is only going to be aware of the new text size next # frame. set_deferred("rect_size", Vector2.ZERO) ## Updates the information label if the current entity matches the provided ## entity. func _on_info_updated(entity: Node) -> void: # Only update the text if what we're showing is the current entity's. if current_entity and entity == current_entity: _set_info(current_entity) # Force-reset the size of the container around the label. set_deferred("rect_size", Vector2.ZERO)
If you head back to GUI.tscn
, you can now instance this new InfoGUI.tscn
scene into the scene tree as a child of the GUI node.
If you run the game now and hover over items in your inventory or the ground, you should see names and descriptions appear, just as planned.
But when we hover over the battery or engine blueprint when they are in the world, they do not update with any capacity or efficiency.
That’s because we didn’t code the get_info()
function we use in code. We can do that next.
Head to BatteryEntity.gd
and add a new function to build the string.
## Provides the amount of power relative to the max amount of power. func get_info() -> String: # Uses Godot's string formatting syntax to pad four characters to the right, # and display one decimal number. # The "j" below stands for joules, an internal unit to measure energy. return "Storing %-4.1f/%s j" % [stored_power, max_storage]
For more information on Godot’s string formatting syntax, see the String formatting reference page.
We can define a new get_info()
function in StirlingEngineEntity.gd
too.
## Provides the current amount of power being output by the engine. func get_info() -> String: # We format the power as a number with one decimal. return "%.1f j/s" % power.get_effective_power()
Now we can run the game and, with everything hooked up correctly, the information updates with every tick of the simulation.
Much better.
We’re done setting up tooltips. In the next lesson, we’ll get to work on crafting, starting with the user interface.
InfoGui.gd
extends PanelContainer const OFFSET := Vector2(25, -25) var current_entity: Node onready var label := $MarginContainer/Label func _ready() -> void: set_as_toplevel(true) Events.connect("hovered_over_entity", self, "_on_hovered_over_entity") Events.connect("info_updated", self, "_on_info_updated") hide() func _process(_delta: float) -> void: rect_global_position = get_global_mouse_position() + OFFSET func _set_info(entity: Node) -> void: var entity_filename: String = Library.get_entity_name_from(entity).capitalize() var output := entity_filename if entity is BlueprintEntity: output += "\n%s" % entity.description else: if entity.has_method("get_info"): var info: String = entity.get_info() if not info.empty(): output += "\n%s" % info label.text = output show() func _on_hovered_over_entity(entity: Node) -> void: if not entity == current_entity: current_entity = entity if not entity: label.text = "" hide() else: _set_info(entity) set_deferred("rect_size", Vector2.ZERO) func _on_info_updated(entity: Node) -> void: if current_entity and entity == current_entity: _set_info(current_entity) set_deferred("rect_size", Vector2.ZERO)
BatteryEntity.gd.get_info()
func get_info() -> String: return "Storing %-4.1f/%s j" % [stored_power, max_storage]
StirlingEngineEntity.gd.get_info()
func get_info() -> String: return "%.1f j/s" % power.get_effective_power()