Yet Another Roguelike Tutorial, Part 7

Publish date: Jul 31, 2023
Tags: godot4 tutorial roguelike

Part 7: Creating the Interface

Welcome back to the roguelike tutorial series. This tutorial will continue from where the last one left off. You can find the previous tutorial here: https://selinadev.github.io/10-rogueliketutorial-06/

A note beforehand: I noticed that in the current version the parser won’t properly reparse the project, or at least some changes are not registered from other scripts. Sometimes throughout writing this code that has caused issues for me, when something would report an error, or a new option was not shown in an exported variable. Should you encounter such issues, the best way I have found to deal with that problem is to reload the project. You can do so under Project > Reload Current Project.

This part will deal with displayed text for the first time. So we need some fonts. I used Kenney’s font pack, which you can find here: https://kenney.nl/assets/kenney-fonts, or included in the project files for this part in the GitHub repository: https://github.com/SelinaDev/Godot-Roguelike-Tutorial. I have placed them under res://assets/fonts.

We will now start with our interface. This is the part where we will probably diverge from the original python tutorial the most, to do things the Godot way. We did a bit of that in the last tutorial, with the state machine and the signal that triggered its transition, but this time we will go all out. Right away we will see a huge refactor of our scene tree. We will use that to create a panel at the bottom of the screen, where we show the player’s health bar, as well as a log for the messages we have only printed to the console so far. These messages will have a few different colors to differentiate the message type, so let’s start out with a file to hold some color definitions. For that create a new script extending RefCounted at res://src/Utils/colors.gd, and fill it with the following:

class_name GameColors
extends RefCounted

const PLAYER_ATTACK = Color("e0e0e0")
const ENEMY_ATTACK = Color("ffc0c0")

const PLAYER_DIE = Color("ff3030")
const ENEMY_DIE = Color("ffa030")

const WELCOME_TEXT = Color("20a0ff")

Here we define a few constant color values for different kind of messages. As they are constants and this script has a class_name we will be able to easily access these values in code later.

Now for the big remodel. What we want is a bar across the bottom of the screen. That bar should be segmented into two panels, one of which showing the player’s health bar, the other showing the message log. We could simply overlay these on top of what we already have. However, we show the player centered on the screen, if we simply overlayed the GUI on one side of the screen we would introduce some asymmetry. By blocking of a bar on the bottom of the screen the center of the screen, where the player is drawn, is now closer to the edge of that bar than it is to the top of the screen. This could seem weird. To circumvent that, we have to tell Godot that info bar is one area, and that it should use the rest for drawing from the camera, so the camera centers in that remaining part of the screen. Fortunately, Godot’s UI system makes this relatively easy, if you know what you have to do. The interface we will build might seem overwhelming, because it will consist of quite a few nodes, but each of those nodes has a specific purpose, so I do hope that it will make sense in the end.

To start of, go into the scene tree of the Game scene and add another Control node as a child. Call it InterfaceRoot. Right-click it and select Make Scene Root. Now it should be at the top of the tree, with Game being a child of it. First things first, click the InterfaceRoot and make sure you are in the 2D editing workspace. In the main window select the Anchor presets (the icon that looks like a little plus in a circle, left of the anchor icon), and select the bottom right option Full Rect.

Now, we said we wanted to have the game world and the info panel one on top of the other, so in order to split things vertically, we need a new VBoxContainer as a child of InterfaceRoot. Set this to Full Rect as well. Create a new SubViewportContainer as a child of that VBoxContainer. It will hold the viewport our camera will draw to. Check the Stretch property, and under Layout > Container Sizing > Vertical check the Expand property. Now create a SubViewport as a child of SubViewportContainer. On that check Viewport > Disable 3D Also scroll down to Viewport > Canvas Items > Default Texture Filter and set it to Nearest (otherwise our pixel art would be blurry).

The last part of this refactor is to take the Game node and drag it into the SubViewport. With that the game will be rendered right inside our defined little window, which incidentally is the whole screen right now. You should be able to run the project at this point, and everything should look just like it did before.

Let’s start assembling the bottom panel now. We want two panels side by side, horizontally, so we add a HBoxContainer as a child of the VBoxContainer and call it InfoBar. In Layout > Container Sizing check the Vertical Expand. This would cause the bottom panel to take up half of the vertical space, which is a bit much. We could also set a fixed height, but instead let’s go back into SubViewportContainer and into Layout > Container Sizing. There, set the Stretch Ratio to 5. As SubViewportContainer and InfoBar are the two children of VBoxContainer, and both are set to expand vertically, they will share the available vertical space. However, now that SubViewportContainer has a stretch ratio of 5, it will take up five parts of that space, while InfoBar will only take up 1. This results in the InfoBar (and thereby also our panels) only taking up a sixth of the vertical space (1 part of a total of 5 + 1 = 6 parts). Lastly, to make sure there is no gap between the two panels we will create as the part of the InfoBar, we set Theme Overrides > Constants > Separation to 0.

Now we start filling the InfoBar. Create two PanelContainers as children of InfoBar. Name one StatsPanel and the other MessagesPanel. In the StatsPanel we set the Layout > Container Sizing > Horizontal Expand to true. We do the same for the MessagesPanel, but we want that to take up more of the horizontal space, so we set the Stretch Ratio to 2. This means the MessagesPanel will be double as wide as the StatsPanel.

We still need to style these panels. To do that, we use another trick involving our sprite sheet. In the StatsPanel under ThemeOverrides > Styles click into the slot for Panel and create a new StyleBoxTexture. For the texture create a new AtlasTexture, and set our familiar sprite sheet as texture. Then edit the region and select the last tile at the bottom right. You should see a little rectangle. Back in the StyleBoxTexture set all the Texture Margins to 5 px, and all the Content Margins to 8 px. Now this rectangle should stretch properly. Click on the downwards arrow next to the StyleBoxTexture in the Panel slot, and select Copy at the bottom of the list. Now go into the MessagesPanel, and under ThemeOverrides > Styles click into the slot next to Panel and select Paste. Now both our containers are styled.

First, let’s put our attention towards creating the health bar. We don’t want the health bar to take up the whole left panel, but instead to sit at the top of it. That basically means aligning it vertically, so we create a new VBoxContainer as child of StatsPanel. The health bar itself will consist of the bar and a label sitting on top of it. The best way to align multiple elements on top of each other and stretch them to the available space is to use a MarginContainer, so create a new one as a child of the VBoxContainer we just created. Call that MarginContainer HpDisplay. We’ll create two children for it, first a ProgressBar which we call HpBar and then a Label which we call HpLabel.

On the HpBar untick Show Percentage and set the Step to 1. Then, under Theme Overrides > Styles create a new StyleBoxFlat in both the Back and the Fill slot. I set the Color in the Back style box to #7f0000, and the Color in the Front one to #007f00. On the HpLabel set both Horizontal Alignment and Vertical Alignment to Center. Then create a new Label Settings resource. I will use “Kenney Pixel.ttf” as the Font and set the Size of the Outline to 4 and its Color to black. Let’s also make these two nodes uniquely accessible. I doubt we will change the node structure of the health bar much, but it’s good practice anyway. So right click HpBar and select Access as Unique Name, then do the same for HpLabel. You should see percentage signs next to their names in the scene tree now.

Now, to make this do something we create a script on HpDisplay, and save it at res://src/GUI/hp_display.gd. Here’s the top of the script:

extends MarginContainer

@onready var hp_bar: ProgressBar = $"%HpBar"
@onready var hp_label: Label = $"%HpLabel"

Here we get the two children with their scene unique names. If you are unfamiliar with that feature, the nice thing about this is that you could now change the scene tree around, create intermediate nodes, etc., but this script still won’t need to change, as it will still find those nodes. Next, we need a function to actually change the values here.

func player_hp_changed(hp: int, max_hp: int) -> void:
	hp_bar.max_value = max_hp
	hp_bar.value = hp
	hp_label.text = "HP: %d/%d" % [hp, max_hp]

We have a function that takes in the hp and the max hp of the player. This is a bit lazy, as we could also store those values and only handle max hp when that value actually changes. But we expect the player’s hp to change infrequently enough that this small overhead should not matter that much, so we prefer the easy solution. We change the value and max value of the HpBar, then construct a string showing the hp and set that as the text of the HpLabel. We need to set up a way to call that, so we will create a function to initialize the label:

func initialize(player: Entity) -> void:
	await ready
	player.fighter_component.hp_changed.connect(player_hp_changed)
	var player_hp: int = player.fighter_component.hp
	var player_max_hp: int = player.fighter_component.max_hp
	player_hp_changed(player_hp, player_max_hp)

Here, we get the player. The problem here is that the way the scene tree is structured this function will be called before this node is ready, i.e., before it has references to its children, which would cause an error when we want to set those variables. We could ensure that this function is called with call_deferred(), but the other option is to have this function wait until it is ready by calling await ready. Next we see a signal on the fighter component, which we will shortly create, and which will inform us about changes to an entities hp. We connect that signal from the player’s fighter component to our player_hp_changed function. That now handles most of the work. However, this will only fire once the player’s hp changes, but starting out the state of the bar is still undefined. That’s why we get the player’s current hp and max hp and call player_hp_changed() manually right now, which will set the state of the bar to the starting hp and max hp of the player.

Let’s handle the signal we mentioned real quick. In fighter_component.gd modify the top as follows:

signal hp_changed(hp, max_hp)

var max_hp: int
var hp: int:
	set(value):
		hp = clampi(value, 0, max_hp)
		hp_changed.emit(hp, max_hp)
		if hp <= 0:
			die()

We add that signal, and then, in the setter we emit it after the hp have been changed. Right now we don’t change the max hp in any way, so we don’t need to handle that at the moment. Now that we have that in place, it’s time to call the initialize function we created. We could pass around references to HpDisplay and call it directly somehow, but all the initialize function really needs is a reference to the player. So to get the player from the Game node to the HpDisplay node we use a signal. In game.gd add a signal at the top:

class_name Game
extends Node2D

signal player_created(player)

Then, at the top of the ready function, once we created the player, emit that signal:

func _ready() -> void:
	player = Entity.new(null, Vector2i.ZERO, player_definition)
	player_created.emit(player)

The Game node will now emit a signal containing a reference to the player as soon as the player is created. However, at the moment no one is listening to that signal. So in the scene tree select Game, and switch the Inspector tab to the Node tab. In the Signals sub-tab you should now see the player_created signal. Double click that and select the HpDisplay as the target node. Under Receiver Method click the Pick button, and select initialize() in the window that appears. Click Ok on that window and Connect in the other window. Now you should be able to run the project and see a health bar. If you find an enemy and let them hit you you should see the health bar change according to the damage the player receives.

The next challenge is to get the messages on the screen. First, we create a class for messages. Create a new script extending Label at res://src/GUI/message.gd. Here is the script:

class_name Message
extends Label

var plain_text: String
var count: int = 1:
	set(value):
		count = value
		text = full_text()


func _init(msg_text: String, foreground_color: Color) -> void:
	plain_text = msg_text
	text = plain_text
	autowrap_mode = TextServer.AUTOWRAP_WORD_SMART


func full_text() -> String:
	if count > 1:
		return "%s (x%d)" % [plain_text, count]
	return plain_text

We store the plain_text and a count variable. This is to group messages in case we send the same messages multiple times in a row. We discuss the setter of count in a moment. In _init() we simply set the plain text, then set the actual text of the label to the plain text. We also set the autowrap mode to smart autowrapping, in case we have long messages that need to take up multiple lines. The full_text() function assembles the message together with the count. In case we have a count larger than 1, this function appends that count. E.g., if the message is “You hit the Orc.” and the count is as 3, then this returns “You hit the Orc. (x3)”. That’s why in the setter, when the count is changed we also set the text of the label to full_text().

You notice that wi did not handle the color so far, and if you know Godot well enough you might also have caught that we didn’t set a font so far, and would use the not so fitting default font. So create a new LabelSettings resource under res://assets/resources/message_label_settings.tres, and change the font to the font you want (I used “Kenney Pixel.ttf” again). Then, let’s modify the script again:

class_name Message
extends Label

const base_label_settings: LabelSettings = preload("res://assets/resources/message_label_settings.tres")

At the top we load that resource, so we can access it in the _init() function:

func _init(msg_text: String, foreground_color: Color) -> void:
	plain_text = msg_text
	label_settings = base_label_settings.duplicate()
	label_settings.font_color = foreground_color
	text = plain_text
	autowrap_mode = TextServer.AUTOWRAP_WORD_SMART

Here we set the label_settings of our label to a duplicate of the resource we just created. Then we set label_settings.font_color to the color we gave to the _init() function. That’s also re reason we need to duplicate our label settings. If we just assigned them, then all the message labels would share the same label settings, and each time we set the color on one message labels all other message labels would change color as well.

The Messages are ready now, so we need a place to put them, meaning we will expand our node tree again. MessagesPanel still empty, so we add a new ScrollContainer as a child and call it MessageLog. Then, as we want to list the messages, we add a new VBoxContainer as a child of that and call it MessageList. For the MessageLog set Horizontal Scroll to Disabled, as we want messages that are too long to wrap instead of extending to the right. Set Vertical Scroll to Never Show, which will allow scrolling, and even controlling that UI element with the mouse wheel, but won’t show the scroll bar, which would not really fit the rest of our aesthetic. For MessageList check Layout > _ Container Sizing_ > Horizontal Expand. We don’t need vertical expand, as the messages will stretch the container. Also set the MessageList to be accessible via a unique name.

Now we can create the code for that. We want to be able to send a message from anywhere, without knowing too much about the message log system or where it is. To do that, we need the signal bus again. So open up signal_bus.gd and change id to the following:

extends Node

signal player_died
signal message_sent(text, color)

Now we have a message_sent signal which we will use in the code for the message log. So create a new script on the MessageLog node and save it at res://src/GUI/message_log.gd. Here is the top of the script:

class_name MessageLog
extends ScrollContainer

var last_message: Message = null

@onready var message_list: VBoxContainer = $"%MessageList"

Most messages can just be children, but for the count thing we established earlier to work we need a reference to the last message. Of course we also need one to the MessageList node.

func add_message(text: String, color: Color) -> void:
	if (
		last_message != null and
		last_message.plain_text == text
	):
		last_message.count += 1
	else:
		var message := Message.new(text, color)
		last_message = message
		message_list.add_child(message)
		await get_tree().process_frame
		ensure_control_visible(message)

This ist the function to add a message to our log. It first checks if, in case we do have a last message, that messages plain_text is identical to the text we want to log right now. If so, we simply increment the last message’s count by one. Otherwise we create a new message, set that as the new last message and then add it to our list. If we would leave it at that you would only see the first few messages appear, but the scroll system would not follow the action as new messages appear. We can do that with ensure_control_visible(). However, that function needs to know the sizing of all the involved UI elements, which is only updated once per frame. So we have to use await get_tree().process_frame to wait a frame, and only call it after that.

func _ready() -> void:
	SignalBus.message_sent.connect(add_message)

Here in the _ready() function we connect that message_sent signal from the signal bus we created a moment ago to the add_message() function. The only thing that’s left now is to emit that signal somewhere:

static func send_message(text: String, color: Color) -> void:
	SignalBus.message_sent.emit(text, color)

Here we use a static function that wraps the emitting of the signal. You see how we will use this in a bit, and it is just more intuitive that way than to directly emit the signal from everywhere we want to create a message. Speaking of which, let’s create a little welcome message. In game.gd add the following to the end of _ready():

func _ready() -> void:
	player = Entity.new(null, Vector2i.ZERO, player_definition)
	player_created.emit(player)
	remove_child(camera)
	player.add_child(camera)
	map.generate(player)
	map.update_fov(player.grid_position)
	MessageLog.send_message.bind(
		"Hello and welcome, adventurer, to yet another dungeon!",
		GameColors.WELCOME_TEXT
	).call_deferred()

We have the same problem we had earlier with UI, which is that when we do stuff in the _ready() function the other nodes might not be ready yet themselves. To work around this we use call_deferred(), which makes our function call look a bit weird. We need to take the function object send_message from MessageLog, bind() the two arguments we want to pass it to it, and then use call_deferred(). That will make sure the function is only called later, when all the nodes are ready. This should work now, and when you start the project now, you should see the welcome message.

However, we still have a few messages that we need to move from console printing to the message log. One of those is in melee_action.gd:

func perform() -> void:
	var target: Entity = get_target_actor()
	if not target:
		return
	
	var damage: int = entity.fighter_component.power - target.fighter_component.defense
	var attack_color: Color
	if entity == get_map_data().player:
		attack_color = GameColors.PLAYER_ATTACK
	else:
		attack_color = GameColors.ENEMY_ATTACK
	var attack_description: String = "%s attacks %s" % [entity.get_entity_name(), target.get_entity_name()]
	if damage > 0:
		attack_description += " for %d hit points." % damage
		MessageLog.send_message(attack_description, attack_color)
		target.fighter_component.hp -= damage
	else:
		attack_description += " but does no damage."
		MessageLog.send_message(attack_description, attack_color)

We rewrite the perform action a bit, by first adding an attack color, and deciding which attack color to use depending on whether the attack was made by the player or an enemy. We then replace the calls to print() with calls to MessageLog.sent_message(). Remember, we don’t need to know a specific message log for this, as this is a static function. This static function will find our singleton signal bus, which will then emit the signal, to which the actual message log listens. The other place we need to modify is the die() function of figher_component.gd:

func die() -> void:
	var death_message: String
	var death_message_color: Color
	
	if get_map_data().player == entity:
		death_message = "You died!"
		death_message_color = GameColors.PLAYER_DIE
		SignalBus.player_died.emit()
	else:
		death_message = "%s is dead!" % entity.get_entity_name()
		death_message_color = GameColors.ENEMY_DIE
	
	MessageLog.send_message(death_message, death_message_color)
    ...

Again, we need to decide the color depending on who died. Of course we also need to decide the text according to that, but we already did that before. Then, instead of printing we call the message log again. And with that our message system works. When running the project you should see new messages appearing, and you can already scroll the log with the mouse wheel. It would be nice though to be able to control the log with the keyboard. To do that, we will need another input handler. However, we will use a little trick for which we need to expand base_input_handler.gd ab bit:

class_name BaseInputHandler
extends Node


func enter() -> void:
	pass


func exit() -> void:
	pass


func get_action(player: Entity) -> Action:
	return null

We get this closer to an actual state machine. We add a function that is called when we enter an input handler, and another that is called when it is exited. This will be called in input_handler.gd in the transition_to() function:

func transition_to(input_handler: InputHandlers) -> void:
	if current_input_handler != null:
		current_input_handler.exit()
	current_input_handler = input_handler_nodes[input_handler]
	current_input_handler.enter()

We call the exit() function before we change the input handler, then call the enter() function once current_input_handler points to the new input handler. We also have a check if the current_input_handler is null for the very first transition, when we don’t have an input handler yet.

Before we create a new input handler we will expand our input map a bit. Go into Project > Project Settings > Input Map again. First, we modify our quit action a bit. Most users might expect the escape key to take them back to the previous menu, not straight up quit the game. So remove the escape key from quit, then add the Q key to it. However, before confirming, check Ctrl in the Additional Options. This will mean that Ctrl+Q will be our quitting shortcut. Add a new action called view_history, and add the V key to that. Then create another action called ui_back and add the escape key to that action.

Now we can create a new input handler. Create a new Node child under InputHandler and call it HistoryViewer. Then, create a new script on that node extending BaseInputHandler at res://src/Game/EventHandlers/history_viewer_input_handler.gd. Here is the top of that script:

extends BaseInputHandler

const scroll_step = 16

@export_node_path("PanelContainer") var messages_panel_path
@export_node_path("MessageLog") var message_log_path

@onready var message_panel: PanelContainer = get_node(messages_panel_path)
@onready var message_log: MessageLog = get_node(message_log_path)

First we create a constant that sets how many pixels we will scroll with one step. Then we export a few variables. We need to access some other nodes in the tree. To do that we first export node paths, which we can then set via the editor. After that we get the actual nodes as @onready variables using get_node and the paths we will set.

func enter() -> void:
	message_panel.self_modulate = Color.RED


func exit() -> void:
	message_panel.self_modulate = Color.WHITE

Next we use the enter() and exit() functions to toggle the self_modulate of the panel. When we enter this input handler / state, the panel border of the message log will turn red, and once we return to another state, it will turn white again. Now, for the actual input handling:

func get_action(player: Entity) -> Action:
	var action: Action
	
	if Input.is_action_just_pressed("move_up"):
		message_log.scroll_vertical -= scroll_step
	elif Input.is_action_just_pressed("move_down"):
		message_log.scroll_vertical += scroll_step
	elif Input.is_action_just_pressed("move_left"):
		message_log.scroll_vertical = 0
	elif Input.is_action_just_pressed("move_right"):
		message_log.scroll_vertical = message_log.get_v_scroll_bar().max_value
	
	if Input.is_action_just_pressed("view_history") or Input.is_action_just_pressed("ui_back"):
		get_parent().transition_to(InputHandler.InputHandlers.MAIN_GAME)
	
	if Input.is_action_just_pressed("quit"):
		get_tree().quit()
	
	return action

First, we use the action keys to move the log around. With the up and down movement keys we go down or up our scroll_step, and with the left and right movement keys we go to the beginning (0) or the end (message_log.get_v_scroll_bar().max_value). Then we check for the view_history action or the ui_back action, which triggers a input handler transition back to the main game input handler. Lastly, we check for the quit action, and if we encounter that we do quit the game.

Now, after saving the script, select the HistoryViewerInputHandler node in the editor. In the inspector click on the slot next to Messages Panel Path and in the window that pops up select the MessagesPanel. Then do the same with Message Log Path and MessageLog. We still need to integrate this input handler with the rest of the game. Let’s start with input_handler.gd:

enum InputHandlers {MAIN_GAME, GAME_OVER, HISTORY_VIEWER}

@export var start_input_handler: InputHandlers

@onready var input_handler_nodes := {
	InputHandlers.MAIN_GAME: $MainGameInputHandler,
	InputHandlers.GAME_OVER: $GameOverInputHandler,
	InputHandlers.HISTORY_VIEWER: $HistoryViewerInputHandler,
}

We add a new entry to the enum, and a reference to the dictionary. Then, move to main_game_input_handler.gd. Modify get_action() as follows:

func get_action(player: Entity) -> Action:
	var action: Action = null
	
	for direction in directions:
		if Input.is_action_just_pressed(direction):
			var offset: Vector2i = directions[direction]
			action = BumpAction.new(player, offset.x, offset.y)
	
	if Input.is_action_just_pressed("wait"):
		action = WaitAction.new(player)
	
	if Input.is_action_just_pressed("view_history"):
		get_parent().transition_to(InputHandler.InputHandlers.HISTORY_VIEWER)
	
	if Input.is_action_just_pressed("quit") or Input.is_action_just_pressed("ui_back"):
		action = EscapeAction.new(player)
	
	return action

Here we add a check for the view_history action and have it trigger a transition to our new history viewer input handler. With all that in place you can now run the project, and once you have a few messages in the log you can press v to control the log.

We will have one last addition to our interface. Right now we can hit an orc to let the message log tell us that it’s an orc, and with only two types of enemies we can easily remember what’s what. But it would be nice if we had a way to identify entities on the screen. We will implement a simple label that shows a list of the entities under the mouse cursor.

Let’s start with the part that gets the information. Create a new Node2D as child of Map and call it MouseoverChecker. Create a new script on that node at res://src/GUI/mouseover_checker.gd. Here’s the top of that script:

extends Node2D

signal entities_focussed(entity_list)

@onready var map: Map = get_parent()

We have the entities_focussed signal we will use to relay the information to the UI later. Also, we get a reference to the map that’s the parent of this node. Next we have _process():

func _process(_delta: float) -> void:
	var mouse_position: Vector2 = get_local_mouse_position()
	var tile_position: Vector2i = Grid.world_to_grid(mouse_position)
	var entity_names = get_names_at_location(tile_position)
	entities_focussed.emit(entity_names)

_process() will run every frame. Inside the function we get the current mouse position as a local position, which takes the camera into account. We can use this to calculate the tile position of the tile the mouse is currently over. Next, we get the entity names from a function we’ll shortly write, and then emit that with that signal. Here’s that entity name function:

func get_names_at_location(grid_position: Vector2i) -> String:
	var entity_names := ""
	var map_data: MapData = map.map_data
	var tile: Tile = map_data.get_tile(grid_position)
	if not tile or not tile.is_in_view:
		return entity_names
	var entities_at_location: Array[Entity] = []
	for entity in map_data.entities:
		if entity.grid_position == grid_position:
			entities_at_location.append(entity)
	entities_at_location.sort_custom(func(a, b): return a.z_index > b.z_index)
	if not entities_at_location.is_empty():
		entity_names = entities_at_location[0].get_entity_name()
		for i in range(1, entities_at_location.size()):
			entity_names += ", %s" % entities_at_location[i].get_entity_name()
	return entity_names

Here, we first need the map_data. We use it to check if the tile we’re hovering is in view, as we don’t want the mouseover to reveal entities we can’t actually see. If we can’t see the tile, we return early with an empty name list string. Otherwise we start building the list. We go through all the entities and put all the ones that are on that position in an array. Then we use sort_custom() with an anonymous function that sorts the entities in that list by their z_index. This z_index corresponds to the entity type, and this way we will list more important things, like alive entities, first in the list. Once we have that sorted list we assembly it into a string. We use the first entity name separately, so in the loop that goes through the remaining entities we can prepend a comma.

A quick note. In a previous version of this tutorial I used a caching mechanism to only run this code if the mouse moved AND that changed the tile the mouse was over. This filtering helps with performance, but each of those filters creates a problem. If we only run this if the mouse gets moved, and the player runs around the map the tile below the mous changes even if the mouse does not move. Ok, so let’s say we check every frame (like here), but only run the slightly more expensive get_names_at_location() function if the tile under the mouse cursor changes. That still won’t work as we want it to, because the contents of a tile can change, e.g., when an “orc” is slain and turns into a “remains of orc”. A better way would probably to have this update in a function, and call that from mouse updates as well as a signal emitted whenever the player takes a turn. However, our game is relatively light on it’s resource requirements, so for now this solution works and doesn’t negatively impact the performance, so I’ll leave this solution as it is.

We have the system for getting what entities are under the mouse cursor, now we need a way to display it. We will just put a label below the hp bar. Create a new Label node as a child of StatsPanel/VBoxContainer and call it MouseoverLabel. Create a new LabelSettings resource and fill it with a font again (I again used “Kenney Pixel.ttf”). Also under Layout > Container Sizing > Vertical check Expand and select the option Shrink End. That way the label will sit at the bottom of that panel. As a last step we will use the signal from MouseoverChecker to change the text of MouseoverLabel. Luckily that signal has a String argument, meaning we can directly use signals for that. Click MouseoverChecker in the scene tree, and switch from the Inspector to the Node tab. In the Signals sub-tab you should see the entities_focussed signal. Double click that to get to the connection dialog. You need to activate the Advanced options at the bottom left to be able to select MouseoverLabel. Once you did that and MouseoverLabel is selected, click the Pick button. Deactivate Script Methods Only and now you’re presented with a list of all the builtin functions of the Label class, including set_text(), which we will select. Confirm all the dialogs and the systems are hooked up.

Now our MouseoverChecker will generate the signal whenever we hover over a new tile, and that signal will directly trigger a change of the text of our MouseoverLabel. You can now run the project and try out the mouseover system. And that concludes this part concerning the interface. If you want to look at the code in its entirety, you can do so in the github repository: https://github.com/SelinaDev/Godot-Roguelike-Tutorial

Find the next part of the tutorial here: https://selinadev.github.io/12-rogueliketutorial-08/