Control Nodes and Anchors

Module 2 ยท Lesson 1 ยท ~40 min ยท Godot 4.x

Godot has two coordinate systems for 2D. Node2D-derived nodes use world coordinates โ€” absolute positions in the scene, unaware of screen size. Control-derived nodes use a layout system with anchors and margins, like CSS. For a mobile card game you'll use Controls for the UI (hand, buttons, score display), and Node2Ds for the game world (dueling arena, animated card playing effects).

Getting this split right is the difference between a UI that works on every Android screen size and one that falls apart on phones with notches.

Control vs Node2D

 Node2DControl
PositionAbsolute Vector2Anchor + offset
Responds to window resizeNoYes
Auto-layout (containers)NoYes
Input routingManual (Area2D)Automatic (Control.gui_input)
Use forGame world, effectsUI, menus, HUD
If Node2D is a <canvas> where you paint pixels at specific coordinates, Control is a <div> that flows and anchors. For a card game HUD โ€” hand at the bottom, mana top-left, enemy at the top โ€” you want Control.

Anchors: not as hard as they look

Every Control node has four anchor values (top, bottom, left, right), each a number from 0 to 1 representing a fraction of the parent's size. The control's edges are positioned at those fractions plus pixel offsets.

Godot gives you Layout Presets โ€” a dropdown in the toolbar when a Control is selected โ€” that sets all four anchors sensibly. You'll use these 90% of the time. The interesting ones:

Rule of thumb Click the Layout Preset dropdown first, tweak pixel offsets second. Don't manually drag anchors in the viewport โ€” you'll create fractional values that drift on resize.

Containers: layout you don't hand-compute

Instead of positioning every child Control by hand, wrap them in a Container that arranges them automatically. These are the building blocks:

ContainerWhat it does
HBoxContainerChildren in a horizontal row. The hand of cards.
VBoxContainerChildren in a vertical column. A stats panel.
GridContainerN columns, children flow. A card collection grid.
CenterContainerCenters its single child.
MarginContainerAdds padding. Put inside to force pixel padding on any layout.
PanelContainerAdds a background panel sized to contents.
AspectRatioContainerEnforces a child aspect ratio. Useful for cards.

Containers handle the math. You put cards in an HBoxContainer, and it spaces them evenly. Add or remove cards at runtime, the layout re-flows. This is exactly what you want.

# Spawning cards into a hand at runtime
@onready var hand_container: HBoxContainer = $UI/HandContainer
const CARD_SCENE := preload("res://scenes/card.tscn")

func draw_card(data: CardData) -> void:
    var card := CARD_SCENE.instantiate()
    card.data = data
    hand_container.add_child(card)  # layout is automatic

Common Control types

NodeWhat
LabelText display. Read-only.
ButtonClickable, has pressed signal.
TextureRectDisplay an image. expand_mode controls scaling.
TextureButtonButton that's an image (normal/hover/pressed textures).
ProgressBarHP bars, loading indicators.
LineEditSingle-line text input. For word games, this is your input field.
RichTextLabelBBCode-formatted text. Damage numbers with color, card descriptions.
PanelStyleable background. Apply via StyleBox.

Themes and StyleBoxes

You'll want your UI to have a consistent look. Don't style every Button individually โ€” define a Theme resource and apply it to the root Control.

  1. In the FileSystem: New โ†’ Resource โ†’ Theme. Save as ui/main_theme.tres.
  2. Double-click it. The Theme editor opens at the bottom.
  3. Add types: Button, Label, Panel. Customize font, colors, styleboxes.
  4. In your main UI Control's Inspector, drag main_theme.tres into the Theme slot.

Every Button under that Control now uses your theme. Override per-instance only when needed.

# Example: StyleBoxFlat in code for a specific panel
var sb := StyleBoxFlat.new()
sb.bg_color = Color("1a1f2e")
sb.border_width_bottom = 2
sb.border_color = Color("fca311")
sb.corner_radius_top_left = 8
sb.corner_radius_top_right = 8
$Panel.add_theme_stylebox_override("panel", sb)

Safe-area-aware layouts for phones

Modern phones have notches, rounded corners, and bottom gesture bars. Godot exposes a Safe Area via DisplayServer.get_display_safe_area(). You should not put interactive UI at the very edges of the screen.

Practical trick Wrap your top-level UI in a MarginContainer with ~40px margins on all sides. It won't look cramped on your phone, and the hand of cards will sit above the gesture bar instead of getting eaten by it.

We'll revisit this in the mobile export module. For now, remember: 40px padding everywhere is fine.

The UI scale problem (and the fix)

Project Settings โ†’ Display โ†’ Window โ†’ Stretch:

With these settings, your UI scales uniformly across phone sizes. Without them, a card that's 120px wide looks tiny on a 1440p screen and huge on a 720p one.

Do this now

  1. In your project, Project โ†’ Project Settings โ†’ Display โ†’ Window. Set Stretch Mode = canvas_items, Aspect = keep. Set Viewport Width = 1080, Height = 1920.
  2. Open your main.tscn. Add a CanvasLayer child. Inside it, add a Control node. With the Control selected, click Layout Preset โ†’ Full Rect.
  3. Inside the Control, add an HBoxContainer. Layout Preset โ†’ Bottom Wide. This is your hand container โ€” the rest of the exercise comes next lesson.
  4. Hit F5. Try resizing the window. The container stretches across the bottom. Perfect.