How to connect my HUD to my Player

Godot Version

4.7.2 Stable

Question

My scene structure is like so:

  • Main (Node)
    • UserInterface (CanvasLayer)
      • HUD (Control)
    • Game (Node3D)
      • Level (Node3D)
      • Player (CharacterBody3D)

I believe the correct way to transmit the player data to the HUD to update is by emitting signals from the player which the HUD will listen to. I’m just not sure what the best way is to initially connect to those signals, for example, if the player is removed from the scene tree and then re-added again at runtime how would the HUD know to find that new instance of the player and listen for signals?

Could you move the CanvasLayer to be a child of the player? Is there any reason for it to be so near the scene root?

The UserInterface parent node would be where I would eventually add my MainMenu and PauseMenu. I just figured since the HUD was part of the interface I should include it there. I could make another CanvasLayer as a child of the player for the HUD, however if I do that then I have to have the player instance in order to access the HUD

You can make a global class that holds all player data and simply have the player character node and the hud both grab their data from that. It could double as a signal relay if you need the hud and player character to interact directly for whatever reason.

You can use the event bus singleton pattern (the tutorial is a bit outdated but you can use it as a guide). I’m not too fond of it but if your use-case is small it does the job.

I prefer to use a custom Resource as the event bus instead. Resources are shared by default so if all the users of that Resource load() or preload() the same Resource they can talk witch each through it. Example:

The custom resource:

class_name PlayerSignals extends Resource


signal health_changed(new_value: int)
signal died()


# optional, you can just emit the signals directly
func emit_health_changed(value: int) -> void:
    health_changed.emit(value)
    
    
func emit_died() -> void:
    died.emit()

The player script:

extends CharacterBody2D


var health: int
var player_signals: PlayerSignals = load("res://player_signals.tres")


func hit(by: int) -> void:
    health -= by
    if health < 0:
        health = 0
        player_signals.emit_died()
        
    player_signals.emit_health_changed(health)

The hud:

extends Control


var player_signals: PlayerSignals = load("res://player_signals.tres")


func _ready() -> void:
    player_signals.health_changed.connect(_on_player_health_changed)
    player_signals.died.connect(_on_player_died)
    
    
    
func _on_player_health_changed(new_value: int) -> void:
    %HealthBar.value = new_value
    
    
func _on_player_died() -> void:
    get_tree().change_scene_to_file("res://game_over.tscn")

I prefer them because autoloads are Nodes and are tied to the SceneTree while Resources aren’t so they can be used in other places like inside another Resource for example and not just in Nodes. Also, I don’t like using Nodes as data containers but that’s a me thing :sweat_smile: