How do you guys structure your UI?

Godot Version

4.5

Question

I was wondering how you guys tend to structure your UI since I feel like my solution has become increasingly messy. I’ve read more than once that you should only have one canvas layer so I create one like so.

public partial class Gui : CanvasLayer
{
    public enum GuiLayers
    {
        Game,
        Scrolls,
        Results, // showing results of a run e.g. win/loss screen
        Debug,
    }
}

And I then use this canvas layer as such:

public partial class Game : Node
{
    private Gui? _gui;

    public override void _Ready()
    {
        _gui = GetNode<Gui>("/root/Gui");
        _gui?.AddChild(_gameGui);
    }
}

There’s a really big downside to managing it this way though, and that is that now with this global canvas layer, I have to make everything cleanup after itself so I find myself writing things like this a lot:

public partial class BossComponent : Node
{

    private VBoxContainer? _gui;
    public readonly HealthBar HealthBar = new();

    public override void _Ready()
    {
        // boss health bar
        _gui = new VBoxContainer();
        _gui.ZIndex = (int)Gui.GuiLayers.Game;
        _gui.SetAnchorsPreset(Control.LayoutPreset.FullRect);
        _gui.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;

        var hBox = new HBoxContainer();
        hBox.AddChild(HealthBar);
        hBox.SizeFlagsHorizontal = Control.SizeFlags.Fill;
        hBox.SizeFlagsVertical = Control.SizeFlags.ExpandFill;

        _gui.AddChild(hBox);
        GetNode<Gui>("/root/Gui")?.AddChild(_gui);
    }

    public override void _ExitTree()
    {
        _gui?.QueueFree();
    }
}

Was interested to see how others manage this because this method is very manual.

I think its uncommon to add all these in code. You should have scenes where you can individually add a canvaslayer with the ui as child. This saves you a lot of headache

Hi,

is there a need to do everything in code?
Usually i have a main frame for my UI.
When i need thinks to be modular, i split the UI elements in different scenes, and load them
during runtime.

I found doing things in the UI even worse, that is you quickly end up with node soup, a good example would be lets say I wanted to draw a UI element that shows how many enemies a mob spawner has spawned.

So first I would have a canvas layer, and under that I would have a label, on that label I would add a script that updates the label from say a signal on the spawner. Then to bind to that signal I would need to export a reference to the spawner so it can bind to the correct signal.

This makes the code for this spawner and it’s relevant UI element spread across multiple scripts and nodes that need references to each other to work, so when I come back to this I find that I end up needing to open up the scene, open up multiple scripts and cross reference things between all of them to remember what it does.

Hi,

there is no need to export reference to the spawner to bind the signal.
You can make use of a singleton.

  • Create a new script that extends the node class, and define one or more signals on it.
    (just put in : “signal enemy_spawned”) i call it SignalManager
  • In the Project Settings, navigate to the Autoloads tab and register your new script as an auto-loaded node.
  • Connect another node to the Events singleton via code like you would with any other: SignalManager.connect(“signal_name”, self, “_on_Events_signal_name”).
    (this will be on the UI label)
  • From any script in your project, you can then write SignalManager.enemy_spawned.emit(some_arguments) to emit the corresponding signal.
    (this will be on the spawner)

source:

And depends how complex your project is, maybe this is helpfull:

What I do is I make one main control node and instantiate other control node scenes as child to it depending on the need. For ex, in my game I made a Main control node and added a texture rect as BG, that’s it. Then I made a different scene by the name, Main Menu Option, that hold all main menu buttons like start, load, option, exit. Then I made a different scene by the name Load Screen, that stores the load state files that the player saves while playing the game.

On public override void _Ready() I instantiate the “Main Menu Option” scene. Click on Load, “Main Menu Option” gets queue freed from memory and “Load Screen” scene gets instantiated.

In your code, what I’m seeing is you made an Enum and you change the ZIndex everytime something is clicked. In godot, the best advice is give everything it’s seperate scene and instantiate it whereever it’s needed. No matter if it’s your player, level, GUI etc.