Dialog UI
PanelContainer anchors, CanvasLayer, portraits, and Control vs Node2D positioning.
On this page
PanelContainer under a CanvasLayer, anchors at the bottom center — not world position.Scene shape
scenes/dialog.tscn:
CanvasLayer (dialog_box)
└── PanelContainer [group: dialog_box] ← script lives HERE
└── MarginContainer
└── HBoxContainer
├── Portrait (TextureRect 192×192)
└── VBoxContainer
├── Instructions
├── SpeakerName
└── DialogText (RichTextLabel)
The script extends PanelContainer, not the CanvasLayer. The group is on the panel so get_first_node_in_group("dialog_box") returns the scripted node.
Bottom-center anchoring
In the scene (anchors preset bottom-wide / center):
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -420
offset_top = -236
offset_right = 420
offset_bottom = -20
In _ready() we reinforce the bottom margin:
offset_bottom = -20.0
offset_top = offset_bottom - 216.0
Why not position = Vector2(...)?
Control nodes under a properly anchored parent use anchors + offsets. Setting position like a Node2D fights the layout system and often jumps when the viewport resizes.
Bug: dialog appears at the top of the screen
Symptom
Box stuck under the clock / top edge after a refactor.
Cause
Anchors left at default (top-left) while offsets assumed bottom positioning — or the panel was briefly parented as a Node2D-style child of the world instead of under a CanvasLayer.
Fix
Put UI on a CanvasLayer. Set anchors to bottom (anchor_top = anchor_bottom = 1.0) and use negative offset_top / offset_bottom to rise above the edge.
Lesson
World coords (Node2D.global_position) and viewport UI (Control anchors) are different spaces. Camera movement should not move dialog — that is why CanvasLayer exists.
Stretch / different screen sizes
project.godot uses:
window/stretch/mode="canvas_items"
window/stretch/aspect="expand"
Anchored bottom-center panels survive resolution changes better than absolute pixel placement.
Portraits
Clerk portrait loads from a constant path; guest portrait is passed into start_dialog. is_player on each line swaps which texture the TextureRect shows.
Null path after UI restructure
Symptom
Invalid access to property or key 'position' on a base object of type 'null instance'
(or .text / .texture on null)
Cause
@onready var x = $Old/Path no longer exists after you renamed nodes (e.g. moved labels under VBoxContainer).
Fix
Fix the path, or use %UniqueName / groups. Re-open the scene and reassign @onready targets.
Lesson
Any $A/B/C is a compile-time string, runtime lookup. Refactors break silent until the line runs.