G2.3 — Visual 2D Nodes¶
What you'll learn
- Show images with
Sprite2Dand play flipbook animations withAnimatedSprite2D+SpriteFrames. - Frame the view with
Camera2D(make_current), and follow a target by parenting. - Pin UI to the screen with
CanvasLayer, immune to the world camera. - Control draw order with tree order,
z_index, and y-sorting.
How it applies
- UI under the world scrolls and zooms with the camera. A health bar parented into world space
slides and shrinks as the camera moves — it must sit under a
CanvasLayerto stay pinned. This is the most common "my HUD is drifting" bug. - No current camera, or the wrong one, frames the wrong thing. With several
Camera2Dnodes, only the current one is used; forgetting to make a camera current, or making the wrong one current, shows the player the wrong region — a black screen or a fixed view that should have followed. - Draw order decides what is visible. Sprites draw in tree order (and by
z_index); get it wrong and the character hides behind the background, or a top-down sprite that should be "behind" a wall draws in front. Ordering is a correctness concern, not just polish. - The wrong node for the job fights you. A static decoration does not need
AnimatedSprite2D; a pinned HUD does not belong on a worldNode2D. Choosing the right visual node keeps behavior predictable.
Concepts¶
Node2D as a grouping/transform node¶
Node2D itself draws nothing — it is a position/rotation/scale in the world (G2.2) and a parent for
things that do draw. You use it to group a unit (player + sprite + collision + camera) so the whole
group transforms together. When you want "a thing in the world that has parts," the parts hang under a
Node2D.
Sprite2D¶
Sprite2D displays a single texture:
@onready var sprite: Sprite2D = $Sprite
func face(dir: int) -> void:
sprite.flip_h = dir < 0 # mirror horizontally to face left
sprite.modulate = Color(1, 0.5, 0.5) # tint (a Color, 0–1 channels, L1.6)
Key properties: texture (the image), centered (whether the origin is the texture's center or
top-left), flip_h / flip_v (mirroring), and modulate (a color multiply, useful for tints and
fade — alpha included). Setting the texture in the Inspector is the common path; the script tweaks it.
AnimatedSprite2D and SpriteFrames¶
For frame-by-frame animation, AnimatedSprite2D plays named animations defined in a SpriteFrames
resource (you build the frame lists in the editor's SpriteFrames panel):
@onready var anim: AnimatedSprite2D = $Anim
func _ready() -> void:
anim.play("idle")
func start_running() -> void:
anim.play("run")
Each named animation ("idle", "run") is a list of frames with an FPS and a loop flag. AnimatedSprite2D
emits animation_finished when a non-looping animation completes — a signal you wire in G2.6 (and that
the game books use to time attacks). Use a plain Sprite2D for static art; reach for AnimatedSprite2D
only when there are frames to flip through.
Camera2D¶
A Camera2D defines what region of the world is shown. Add one to the scene and make it active:
The simplest follow is to make the camera a child of the thing it follows — parented to the player
Node2D, it inherits the player's transform and tracks automatically (G2.2's compounding). Useful
properties: zoom (a Vector2; larger zooms in), position smoothing, and limits that keep the view
inside the level bounds. With multiple cameras, exactly one is current; make_current() (or the
Current checkbox) selects it.
CanvasLayer — pinning UI¶
UI must not move with the world camera, so it goes under a CanvasLayer, which renders its children
on an independent layer with its own transform — unaffected by Camera2D:
Because HUD is a CanvasLayer, its Control children stay fixed on screen while the player and
camera move through the world. This is the structural fix for the "HUD scrolls with the world" bug from
G1.2: world objects under Node2D, screen UI under CanvasLayer.
Draw order: tree order, z_index, y-sort¶
Within a layer, nodes draw in tree order — later siblings draw on top of earlier ones. So a
background should come before the things drawn over it. z_index overrides this for fine control
(higher draws later/on top). For top-down games where lower-on-screen objects should appear in front,
y-sorting (enabling Y-Sort on a parent) orders children by their y position automatically. Keep it
simple: order siblings correctly first, reach for z_index/y-sort when tree order is not enough.
Example
The canonical 2D scene skeleton, with each node's job:
Player (Node2D) # a unit in the world
├── Sprite (Sprite2D) # its art
└── Camera2D # make_current() in _ready; follows by being a child
HUD (CanvasLayer) # pinned UI layer, immune to the camera
└── ScoreLabel (Control) # stays on screen as the player moves
The camera is a child of the player, so it follows for free; the HUD is a CanvasLayer, so it does
not. That split — followed world versus pinned screen — is the whole reason both node types exist.
Walkthrough¶
- Build a
Node2DPlayerwith aSprite2Dchild (any texture). Add aCamera2Dchild toPlayerand callmake_current()in a script's_ready. MovePlayerin_physics_processand confirm the view follows. - Add a
CanvasLayernamedHUDas a sibling ofPlayer, with aLabelchild. Run and move the player: the label stays pinned while the world scrolls. Then, as an experiment, move theLabelunderPlayerinstead and watch it drift with the world — then move it back. - Tint and flip the sprite from code: set
modulateto a color and toggleflip_h. Observe both. - Draw order: add a second
Sprite2Doverlapping the first. Reorder the two in the Scene dock and watch which draws on top; then set the back one'sz_indexhigher and watch it jump forward.
Optional sanity check
Temporarily remove the make_current() call (and ensure no camera is marked Current). Run and note
the default framing — the camera is not driving the view. Restore make_current() and the view
snaps to the camera. That is "exactly one current camera" demonstrated.
Self-check quiz¶
Q1 — Your health bar slides and shrinks as the player walks. What is the structural fix?
A. Set the health bar's z_index higher.
B. Put the health bar's Control under a CanvasLayer, which renders independently of the world Camera2D.
C. Make the camera a child of the health bar.
D. Disable the camera.
Reveal answer
B. UI under a world Node2D is dragged by the camera; a CanvasLayer renders on its own
layer, immune to the camera, keeping the bar pinned. A changes draw order, not whether it
scrolls. C and D break the world view rather than fixing the UI placement.
Q2 — Two overlapping Sprite2D siblings: which draws on top, by default?
A. The one higher in the Scene dock (earlier sibling). B. The one lower in the Scene dock (later sibling), because siblings draw in tree order, later on top. C. Whichever has the larger texture. D. It is random each frame.
Reveal answer
B. Within a layer, later siblings draw over earlier ones, so the lower entry in the Scene
dock draws on top by default; z_index can override this. A inverts the rule. C and D are not how
draw order is decided.
Q3 — When should you use AnimatedSprite2D instead of Sprite2D?
A. Always; Sprite2D is deprecated.
B. When you need to play frame-by-frame animations defined in a SpriteFrames resource; for static art, Sprite2D is the right, simpler choice.
C. Only for UI.
D. Never; use a script to swap textures manually.
Reveal answer
B. AnimatedSprite2D exists for flipbook animation via SpriteFrames; static images should
use the simpler Sprite2D. A is false (Sprite2D is current and correct for stills). C is wrong
— these are world sprites, not UI. D dismisses the purpose-built node for a manual hack.
Integration question¶
Q4 — open
Assemble a minimal playable view from this chapter and G2.1–G2.2: a world character that animates and
is followed by the camera, plus a score label that stays on screen. Describe the node tree you would
build, which node type each piece is and why, where make_current goes, and how draw order keeps the
character visible above a background — and explain why the score label must not be a child of the
character.
Reveal expected answer
Tree: a Player (Node2D — a world unit with a transform) holding an AnimatedSprite2D
(frame animation via SpriteFrames, play("idle")/play("run")) and a Camera2D child; a
sibling Background (Sprite2D or tilemap) placed earlier in the tree so it draws behind; and
a HUD (CanvasLayer) holding a ScoreLabel (Control). Camera: make_current() in the
Player/camera script's _ready, and because the Camera2D is a child of Player, it follows via
transform compounding (G2.2) with no follow code. Draw order: the background is an earlier
sibling (or lower z_index), so the character, drawn later, appears above it; if needed, z_index
forces the character forward. Why the score label is not a child of the character: a Control
under the world Player would inherit the world transform and the camera, so it would scroll,
zoom, and move with the character instead of staying pinned on screen; placing it under a
CanvasLayer renders it independently of the camera. The chapter's spine: world things are
Node2D (followed by the camera), screen UI is Control under a CanvasLayer (pinned), and draw
order is set by tree order with z_index/y-sort as the override.