Build UI

UI is where generated scenes go wrong most often. A model writes a Control tree with no styling, then puts the presentation in _ready() with add_theme_font_size_override. The game looks right when it runs and wrong in the editor, and nobody can review the layout without playing it.

The rule that avoids this: anything static goes in the scene file. Runtime code updates values that change during play, such as a score counter.

Set presentation as scene properties

The properties the editor writes are the ones to set:

What you want Property
Font size theme_override_font_sizes/font_size
Padding on a container theme_override_constants/margin_left, _right, _top, _bottom
Position and anchoring anchors_preset, anchor_*, offset_*
How a child fills its parent size_flags_horizontal, size_flags_vertical
A stable cell width custom_minimum_size, for example Vector2(160, 0)
Tint or fill colour modulate, or color on a ColorRect
godot-cli scene node add hud.tscn --parent /root/HUD/Row --name Score --type Label \
  --property theme_override_font_sizes/font_size --value 35 --project-root .

Quote string values so they survive the shell and reach the Variant parser as strings:

godot-cli scene set-property hud.tscn --node-name Score --property text --value '"0"' --project-root .

A top bar, as the editor would build it

{
  "ops": [
    { "op": "node_add", "parent": "/root/Main", "name": "HUD", "type": "Control",
      "properties": { "anchors_preset": "10", "offset_bottom": "48.0" } },
    { "op": "node_add", "parent": "/root/Main/HUD", "name": "Background", "type": "ColorRect",
      "properties": { "anchors_preset": "15", "color": "Color(0, 0, 0, 0.6)" } },
    { "op": "node_add", "parent": "/root/Main/HUD", "name": "Margin", "type": "MarginContainer",
      "properties": { "anchors_preset": "15", "theme_override_constants/margin_left": "16",
                      "theme_override_constants/margin_right": "16" } },
    { "op": "node_add", "parent": "/root/Main/HUD/Margin", "name": "Row", "type": "HBoxContainer" }
  ]
}

The background is a sibling under the same Control rather than a parent, so it fills the bar without affecting layout. Content sits in a MarginContainer so padding is a container property rather than offsets on each child.

For a row with items pushed left, centre, and right, give the spacers size_flags_horizontal of 3 and leave the items at their natural size.

Unique names instead of long paths

$HUD/Margin/Row/Score breaks the moment someone adds a container. Mark the node as a unique name and scripts on the owner scene can use %Score:

godot-cli scene node add hud.tscn --parent /root/HUD/Row --name Score --type Label \
  --unique-name --project-root .

In a patch, the same thing is { "op": "node_set", "path": "/root/HUD/Row/Score", "property": "unique_name_in_owner", "value": "true" }.

Reusable widgets need @tool

When a widget's root script drives its children from @export variables, mark the script @tool and apply values through setters. Without it, the editor shows the unstyled scene and only Play looks right.

@tool
extends MarginContainer

@export var label_text: String = "Health":
    set(value):
        label_text = value
        _apply_label()

func _ready() -> void:
    _apply_label()

func _apply_label() -> void:
    var label := get_node_or_null("Label")
    if label:
        label.text = label_text

get_node_or_null matters because the editor calls the setter before the tree is ready. With this in place, an instance of the widget can be configured from the parent scene with instance_override on label_text, and the editor viewport updates as the value changes.

Once a widget is worth reusing, give it a catalog manifest so agents find it: teach an agent your sub-scenes.

Check it without opening Godot

godot-cli scene inspect hud.tscn --json      # parsed property values, with types
godot-cli scene node list hud.tscn --json    # the Control tree
godot-cli scene validate hud.tscn --project-root . --json

inspect reports each property's Variant kind, which catches a colour written as a string or an anchor written as a float where an int was meant.