CS 276 // GAME SYSTEMS IN GODOT

Heads Up.

Lecture W7T1: UI, Menus, and One Careful Global

Lucas P. Cordova, Ph.D.

Willamette University  |  Tuesday, October 6, 2026

SCORE 000276HI 999999

Outline

QUEST LOG

Laptops out today. The proposal is due Thursday, October 8, 9:59 PM (PDF in Canvas and in your team repo). Lab 4 is due Tuesday, October 20, 9:59 PM.

CS 276
LEVEL 01

Lightning Round

Six games, sixty seconds each

SCORE 000276HI 999999

The 60-second pitch

BeatWhat you sayAbout
Name and anchorWorking title, plus your 20 Games Challenge anchor [6].10 s
The loopThe loop in one breath, plus the one twist that makes it yours.20 s
The feelYour aesthetics, and the moment you most want a player to experience.15 s
The riskThe thing you are least sure you can build, and what you try first.15 s
WATCH OUT

No slides or no demo, A pitch that runs long is the game asking for a smaller scope.

CS 276

While you listen

CO-OP MODE

Every pitch, jot two things (index card or notebook):

  1. The moment you would want to play.
  2. The one question you would ask this team.

Two of you become each team's stakeholders at the proposal gate this Thursday. The questions you write today are your first review notes.

One more listening job: for each game, guess what its HUD has to show. You will need that guess at the end of class.

CS 276
LEVEL 02

Screen Space

Control nodes, anchors, and containers

SCORE 000276HI 999999

Two worlds on one screen

The game worldThe interface
Base nodeNode2D (blue icons)Control (green icons)
Placed byA position in world pixelsA rectangle attached to its parent's edges
Lives inThe arena. A camera can scroll past it.The glass of the screen. It stays put.
ExamplesBlitz, the rival, walls, the figScore, timer, menus, buttons, health bars
DEFINITION

A CanvasLayer is a separate sheet of glass drawn over the game. Interface nodes go under one so that no camera, shake, or zoom ever moves them [3].

CS 276

A Control is a rectangle

  • Every Control has four anchors: where each edge attaches to its parent, from 0 (left or top) to 1 (right or bottom).
  • Offsets are the pixel distances from those anchors.
  • Anchor presets set all of it in one click: Top Left, Center, Top Wide, Full Rect.
PREDICT

Fingers up. The window is 1152 pixels wide. A label sits at x = 1000 with the default Top Left anchors. A player drags the window down to 800 pixels wide. Where is the label?

1: it slides left and stays near the right edge. 2: it stays at x = 1000 and falls off the screen. 3: everything shrinks to fit.

CS 276

Containers do the layout

ContainerWhat it does to its children
MarginContainerPads them away from its edges.
HBoxContainer VBoxContainerLines them up in a row, or stacks them in a column.
CenterContainerCenters them.
PanelContainerDraws a background panel behind them.
PATTERN

A child of a container hands over its position and size. You steer it with two things: its minimum size, and its Container Sizing flags (Fill, Expand) [4].

WATCH OUT

Position and size greyed out in the Inspector? The parent is a container. That is the container doing its job.

CS 276

What we are building

RIVAL-BRAINS, BY THE END OF CLASS
root ├─ Scoreboard round 3: an autoload └─ Main (Node2D) game.gd, the brain ├─ Walls, Player, Rival, Fig └─ UI (CanvasLayer) ├─ Banner (Label) from Week 5 ├─ HUD round 1: hud.tscn └─ PauseMenu round 2: pause_menu.tscn
  • Round 1: survival time on the left, best time on the right.
  • Round 2: Escape freezes the chase and opens a menu.
  • Round 3: the best time survives a restart.
CHECKPOINT

Each new piece is its own small scene with its own script. Thursday's git lesson, applied: you could build the HUD while your teammate builds the menu, and never touch the same file.

CS 276
LEVEL 03

Round 1: The HUD

A heads-up display.

SCORE 000276HI 999999

How to play

HOW TO PLAY
  1. You play in co-op pairs: you and your project teammate, each on your own laptop.
  2. Each round, watch the demo first. Keyboards wait for the timer.
  3. A round is finished when it runs on your screens. Then hands up, and your pair's initials go on the board.
  4. Stuck for two minutes? Ask your player two, then a neighboring pair, then jump to a save point (next slides).
  5. Finished early? Each round has a side quest for extra flair while everyone else catches up.

Every step is on these slides. Keep the deck open beside Godot.

CS 276

Loading screen

git clone https://github.com/LucasCordova/rival-brains.git
cd rival-brains
git switch demo5          # where last week ended
git switch -c hud-coop    # your own branch for today
LIVE DEMO

Then, in Godot:

  1. In the Project Manager, click Import, pick the rival-brains folder, and open it.
  2. Run the project. Press Space to play, move with the arrow keys or WASD.
  3. Get caught once, so you know what the rival can do.

Already have the clone? Run git fetch --tags first, then the two switch lines.

CS 276

Save points

Each finished round is saved in the repo. Jumping to one sets aside your own work and gives you a clean, running copy.

To start fromRun this in the rival-brains folder
Round 1 finishedgit stash -u then git switch --detach demo6-lap1
Round 2 finishedgit stash -u then git switch --detach demo6-lap2
Everything finishedgit stash -u then git switch demo6
WATCH OUT

After any jump, accept Godot's reload prompt. After jumping to demo6, also choose Project, then Reload Current Project: that branch changes the project settings, and Godot keeps the old ones in memory.

CS 276

Round 1, step 1: a scene for the HUD

LIVE DEMO

In Godot:

  1. Scene menu, then New Scene.
  2. In the Scene dock, click Other Node. Search for MarginContainer, select it, click Create.
  3. Double-click the new root node and rename it HUD.
  4. With HUD selected, in the Inspector open Layout. Set Layout Mode to Anchors, then Anchors Preset to Top Wide.
  5. In the Inspector open Theme Overrides, then Constants. Check and set Margin Left, Margin Top, Margin Right, and Margin Bottom to 16.
  6. Scene menu, then Save Scene As. Name it hud.tscn.
HUD.TSCN SO FAR
HUD (MarginContainer)

Top Wide pins the HUD to the top edge and stretches it from the left edge to the right edge.

CS 276

Round 1, step 2: a row of labels

LIVE DEMO

Still in hud.tscn:

  1. Right-click HUD, Add Child Node, pick HBoxContainer. Rename it Row.
  2. Right-click Row, add a Label. Rename it TimeLabel. In the Inspector set Text to TIME 0.0.
  3. Right-click Row, add a Control. Rename it Spacer. In the Inspector: Layout, Container Sizing, Horizontal, check Expand.
  4. Right-click Row, add a second Label. Rename it BestLabel. Set Text to BEST 0.0.
  5. For each label: Theme Overrides, Font Sizes, check Font Size, set it to 28.
  6. Save the scene.
HUD.TSCN
HUD (MarginContainer) └─ Row (HBoxContainer) ├─ TimeLabel (Label) ├─ Spacer (Control) └─ BestLabel (Label)

The names matter. The script finds these nodes by path, and $Row/TimeLabel has to match exactly.

CS 276

Round 1, step 3: hud.gd

LIVE DEMO

Give the HUD a script:

  1. Right-click HUD, choose Attach Script. Keep the path hud.gd, click Create.
  2. Replace the whole file with the code on the right.
  3. Save the script.
extends MarginContainer

@onready var time_label: Label = $Row/TimeLabel
@onready var best_label: Label = $Row/BestLabel

func show_time(seconds: float) -> void:
    time_label.text = "TIME %.1f" % seconds

func show_best(seconds: float) -> void:
    best_label.text = "BEST %.1f" % seconds
PATTERN

The HUD has one job: show numbers it is handed. It keeps no clock and makes no decisions. "TIME %.1f" % seconds formats the number with one decimal place.

CS 276

Round 1, step 4: into the game

LIVE DEMO

Put the HUD in the main scene:

  1. Open main.tscn (double-click it in the FileSystem dock).
  2. In the Scene dock, click the UI node (the CanvasLayer).
  3. Click the chain-link Instantiate Child Scene button at the top of the Scene dock. Pick hud.tscn, click Open.
  4. Check the tree: HUD must sit under UI, beside Banner.
  5. Save the scene.
MAIN.TSCN
Main (Node2D) ├─ Walls ├─ Player ├─ Rival ├─ Fig └─ UI (CanvasLayer) ├─ Banner └─ HUD

Running now shows TIME 0.0 and BEST 0.0, frozen. Nobody is handing the HUD any numbers yet.

CS 276

Round 1, step 5: game.gd keeps time

Open game.gd. Three small edits near the top. Lines marked # new are added; the rest already exists and shows you where.

# game.gd: two new variables, under rival_start
var time_alive: float = 0.0
var best_time: float = 0.0

# one new line, under the banner line
@onready var hud: MarginContainer = $UI/HUD

# _delta becomes delta, because now we use it
func _physics_process(delta: float) -> void:
    match state:
        State.PLAYING:
            time_alive += delta           # new
            hud.show_time(time_alive)     # new
CS 276

Round 1, step 6: the door

Still in game.gd, in _change_state (the door from Week 5). Add the marked lines, then save.

# game.gd, inside _change_state
        State.TITLE:
            rival.calm()
            time_alive = 0.0              # new
            hud.show_time(time_alive)     # new
            hud.show_best(best_time)      # new

        State.CAUGHT:
            if time_alive > best_time:    # new
                best_time = time_alive    # new
                hud.show_best(best_time)  # new
PATTERN

Calls down. The game owns the HUD, so it calls the HUD's functions. The door is the one place a round begins and ends, so the clock resets there.

In TITLE, the three lines go right after rival.calm(). In CAUGHT, they go first.

CS 276

Round 1: go

CO-OP MODE

Round clear, on both screens:

  1. Run the project. Press Space. TIME counts up in the top left.
  2. Get caught. BEST updates in the top right. Play again and beat it.
  3. The stretch test: while it runs, drag the window narrower. BEST stays glued to the right edge.
  4. Hands up. Initials on the board.

Side quest: turn TIME yellow after ten seconds, or give both labels a shadow (Theme Overrides, Colors and Constants).

Steps 1 to 6 are the six slides above this one.

CS 276

Round 1: nothing happened?

You seeCause and cure
An error naming show_time and NilThe game cannot find $UI/HUD. The HUD is missing from main.tscn, sits outside UI, or has another name.
An error with Row/TimeLabel in itA node name in hud.tscn differs from the script. Rename the node to match.
delta is not declaredThe parameter still reads _delta. Remove the underscore.
Both labels bunched at the leftSpacer is missing Expand, or it is not between the two labels.
A small HUD that ignores the window widthThe Anchors Preset on HUD is not Top Wide.
TIME never movesAn unsaved script, or the two new lines are outside State.PLAYING.
CS 276
LEVEL 04

Round 2: The Pause Menu

Freeze the world, keep the menu awake

SCORE 000276HI 999999

Pausing is one line

get_tree().paused = true
  • Every node in the tree stops processing: no _process, no _physics_process, no input callbacks [4].
  • Blitz freezes. The rival freezes. The clock in game.gd freezes.
  • A pause menu is in that tree too, so it freezes with everything else. Its buttons would never hear a click.
DEFINITION

Every node has a Process Mode. The default, Inherit, pauses with the tree. Always keeps a node (and the children that inherit from it) running while everything else is frozen.

CS 276

Round 2, step 1: the overlay

LIVE DEMO

A new scene:

  1. Scene menu, then New Scene. Click Other Node, pick ColorRect, click Create. Rename it PauseMenu.
  2. Inspector, Layout: set Layout Mode to Anchors, then Anchors Preset to Full Rect.
  3. Inspector, Color: click the swatch, pick black, and drag the A slider to about 150. The game should show through, dimmed.
  4. Inspector, scroll down to the Node section, open Process, and set Mode to Always.
  5. Scene menu, Save Scene As: pause_menu.tscn.
WATCH OUT

Step 4 is the whole trick. Skip it and the menu opens, then ignores you forever.

CS 276

Round 2, step 2: a centered column

LIVE DEMO

Still in pause_menu.tscn:

  1. Right-click PauseMenu, add a CenterContainer. Rename it Center. Set its Anchors Preset to Full Rect (same two settings as step 1).
  2. Right-click Center, add a VBoxContainer. Rename it Menu.
  3. Right-click Menu, add a Label. Rename it Title. Text: PAUSED. Font Size: 40.
  4. Right-click Menu, add a Button. Rename it ResumeButton. Text: Resume.
  5. Right-click Menu, add a second Button. Rename it RestartButton. Text: Restart.
  6. Save the scene.
PAUSE_MENU.TSCN
PauseMenu (ColorRect) └─ Center (CenterContainer) └─ Menu (VBoxContainer) ├─ Title (Label) ├─ ResumeButton └─ RestartButton

Two containers, zero pixel math. Add a third button later and the column re-centers itself.

CS 276

Round 2, step 3: pause_menu.gd

Right-click PauseMenu, Attach Script, keep pause_menu.gd, Create. Replace the whole file, then save.

extends ColorRect

var allowed: bool = false

func _ready() -> void:
    hide()

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("ui_cancel") and (allowed or get_tree().paused):
        _set_paused(not get_tree().paused)

func _set_paused(paused: bool) -> void:
    get_tree().paused = paused
    visible = paused
    if paused:
        $Center/Menu/ResumeButton.grab_focus()
  • ui_cancel is Godot's built-in action for Escape. Menus use ui_ actions.
  • allowed is set by the game: pausing only makes sense mid-chase.
  • grab_focus() lets the keyboard press the button.
CS 276

Round 2, step 4: wire the buttons

LIVE DEMO

Signals, the Lab 2 way:

  1. In the Scene dock, select ResumeButton.
  2. Open the Node dock (the tab beside the Inspector) and click Signals.
  3. Double-click pressed(). In the dialog, PauseMenu is already selected. Click Connect.
  4. Godot adds an empty function to pause_menu.gd. Fill it in from the right.
  5. Repeat for RestartButton, then save the script and the scene.
func _on_resume_button_pressed() -> void:
    _set_paused(false)

func _on_restart_button_pressed() -> void:
    get_tree().paused = false
    get_tree().reload_current_scene()
PATTERN

Signals up. A button knows nothing about pausing. It announces pressed, and its owner decides what that means.

CS 276

Round 2, step 5: into the game

LIVE DEMO

Back in the main scene:

  1. Open main.tscn. Select UI. Click Instantiate Child Scene and pick pause_menu.tscn.
  2. Check the order under UI: Banner, HUD, PauseMenu. Lower in the dock draws on top.
  3. The dim overlay now covers the editor view. Click the eye icon beside PauseMenu to hide it while you work. The script shows it when needed.
  4. Open game.gd and add the two lines below. Save everything.
# game.gd: one new line, under the hud line
@onready var pause_menu: ColorRect = $UI/PauseMenu

# game.gd, inside _change_state, under the two set_physics_process lines
    pause_menu.allowed = running
CS 276

Predict: the Restart button

PREDICT

Restart calls get_tree().reload_current_scene(). Godot frees the whole Main scene and builds a fresh one from main.tscn.

Before the round starts, fingers up. You play, you set a best time of 8.4 seconds, you pause, you press Restart. What does the HUD say?

1: BEST 8.4.   2: BEST 0.0.   3: the game crashes.

Keep your vote. You are about to find out on your own machine.

CS 276

Round 2: go

CO-OP MODE

Round clear, on both screens:

  1. Mid-chase, press Escape. Everything freezes, the screen dims, the menu appears.
  2. Resume works three ways: a mouse click, the Enter key, and Escape again.
  3. On the title screen, Escape does nothing.
  4. Set a best time, pause, press Restart. Check your prediction.
  5. Hands up. Initials on the board.

Side quest: add a Quit button that calls get_tree().quit(), or wrap Menu in a PanelContainer.

Steps 1 to 5 are the slides above this one.

CS 276

Round 2: nothing happened?

You seeCause and cure
The menu opens and nothing respondsProcess Mode on PauseMenu is still Inherit. Set it to Always (step 1).
Escape does nothing mid-chaseThe pause_menu.allowed = running line is missing from game.gd, or PauseMenu is not under UI.
Clicking a button does nothingThe pressed signal is not connected. Look for the green icon beside the function.
Enter does not press ResumeThe grab_focus() line is missing, or the node is not named ResumeButton.
The menu is visible from the first framehide() is missing from _ready.
The banner draws over the menuPauseMenu must be the last child of UI.
CS 276
LEVEL 05

One Careful Global

Autoloads, and the discipline to keep them small

SCORE 000276HI 999999

Where did BEST go?

THE RUNNING TREE
root └─ Main freed and rebuilt on Restart ├─ game.gd var best_time lived here ├─ Player, Rival, Fig └─ UI ├─ HUD └─ PauseMenu
  • A variable lives exactly as long as the node that owns it.
  • reload_current_scene() replaces every node in the scene. Each variable starts over.
  • The same thing happens when you change scenes: title screen to maze, maze to maze.
CHECKPOINT

Some data has to outlive the scene. It needs a home above Main.

CS 276

Autoloads

DEFINITION

An autoload is a script or scene that Godot adds as a child of root before your first scene loads. It stays there when scenes change or reload, and every script can reach it by name [2].

WITH AN AUTOLOAD
root ├─ Scoreboard stays for the whole session └─ Main freed and rebuilt on Restart
Scoreboard.report(time_alive)

Any script, anywhere, with no node path.

PATTERN

This is Godot's take on the Singleton pattern: exactly one instance, reachable from everywhere [1].

CS 276

The temptation

Reachable from everywhere is exactly what made last Thursday's Berry Bandit fragile. Lines 28 and 29:

        Game.score += 50
        get_node("/root/Main/HUD/ScoreLabel").text = "Score: " + str(Game.score)
  • Any script can change Game.score, so when it is wrong, every script is a suspect.
  • Once one global exists, everything drifts into it: health, the current maze, the player node. That is the god object again, with a shorter name.
  • Nystrom's chapter on Singleton is mostly a warning label for this reason [1].
CS 276

Verdict: global or local?

Thumbs up for autoload, thumbs down for keep it in the scene. Vote before each verdict appears.

On trialVerdictBecause
The best time across restartsGLOBALIt must outlive the scene.
Blitz's current healthLOCALHealthComponent owns it. A new round wants a fresh one.
Music that plays through every screenGLOBALA scene change would cut it off.
The rival's chase speedLOCALIt is tuning data. It lives in a .tres file.
A reference to the HUD's labelLOCALThat node dies with the scene. A global pointing at it crashes.
The player's volume settingGLOBALOne value, every scene, the whole session.
CS 276

Rules of globals

SINGLETON DISCIPLINE
  • Earn it. The data must outlive a scene, or there must be exactly one of the thing.
  • One job. Name it in three words. Scoreboard keeps best times. A file named Globals or Manager has no job yet.
  • Signals up. An autoload announces changes. Scenes connect themselves to it.
  • Hands off the scene. An autoload holds no node paths and no references into the current scene.
PATTERN

Same rules as parts from Week 6. An autoload is a part whose owner is the whole game.

CS 276

Round 3, step 1: scoreboard.gd

LIVE DEMO

A script with no scene:

  1. In the FileSystem dock, right-click res://, choose Create New, then Script.
  2. Path: scoreboard.gd. Inherits: Node. Click Create.
  3. Replace the whole file with the code on the right, then save.
extends Node

signal best_changed(seconds: float)

var best_time: float = 0.0

func report(seconds: float) -> void:
    if seconds > best_time:
        best_time = seconds
        best_changed.emit(best_time)
PATTERN

Ten lines. It owns one number, guards it with one function, and announces when it changes. Nobody else can set a best time lower by accident.

CS 276

Round 3, step 2: register it

LIVE DEMO

Tell Godot to load it first:

  1. Project menu, then Project Settings.
  2. Open the Globals tab and stay on Autoload. (Older Godot 4 releases name the tab Autoload.)
  3. Click the folder icon beside Path and choose scoreboard.gd.
  4. Node Name fills in as Scoreboard. Click Add.
  5. Confirm the new row has Enable checked, then close the window.
DEFINITION

The Node Name is the global name. From this moment every script in the project can write Scoreboard.

CS 276

Round 3, step 3: report and listen

In game.gd: delete the var best_time line and the hud.show_best(best_time) line in TITLE. In CAUGHT, replace the three best-time lines with one:

# game.gd, inside _change_state
        State.CAUGHT:
            Scoreboard.report(time_alive)

In hud.gd, add a _ready function, then save both scripts:

# hud.gd: a new function, above show_time
func _ready() -> void:
    show_best(Scoreboard.best_time)
    Scoreboard.best_changed.connect(show_best)
PATTERN

The game reports. The HUD listens. Neither knows the other exists.

CS 276

Round 3: go

CO-OP MODE

Round clear, on both screens:

  1. Set a best time. Pause. Press Restart. BEST survives.
  2. While the game runs, click Remote at the top of the Scene dock. Find Scoreboard sitting beside Main under root.
  3. Hands up. Final initials on the board.

Side quest: flash BEST yellow on a new record, or show NEW RECORD on the caught banner. Decide first: who should hear best_changed?

Seeing Scoreboard is not declared? Step 2 did not stick: reopen Project Settings and check the name and the Enable box.

CS 276
LEVEL 06

Game Over Screen

What you know now

SCORE 000276HI 999999

Co-op: your HUD on a card

CO-OP MODE

3 minutes, with your teammate, on an index card:

  1. Draw your game's screen as a rectangle. Place at most three things on the HUD.
  2. Mark each one with the edge or corner it is anchored to.
  3. Circle anything that must outlive a scene. That is your autoload's whole job.
  4. Write the name of that autoload in three words or fewer.

This card is a paragraph of your proposal's design detail, already drafted. Compare it with the guess you made during that team's pitch.

CS 276

Checkpoint reached

CHECKPOINT
  • The interface lives in screen space: Control nodes under a CanvasLayer.
  • Anchors attach a Control to its parent's edges. Containers do the arranging.
  • get_tree().paused freezes the tree. Process Mode Always keeps the menu awake.
  • An autoload outlives every scene. Earn it, give it one job, let it signal up.
QUEST LOG

Thursday, October 8: agile for game teams, stakeholders assigned, and the proposal due at 9:59 PM. Thursday, October 15: the prototype gate. Tuesday, October 20: Lab 4.

CS 276

Sources

  • [1] Nystrom, Game Programming Patterns: Singleton (gameprogrammingpatterns.com/singleton.html), today's reading.
  • [2] Godot docs: Singletons (Autoload) (docs.godotengine.org, Scripting section).
  • [3] Godot docs: Canvas layers and Size and anchors (docs.godotengine.org, 2D and User interface sections).
  • [4] Godot docs: Using Containers and Pausing games and process mode (docs.godotengine.org).
  • [5] rival-brains, branch demo6 with tags demo6-lap1 and demo6-lap2 (github.com/LucasCordova/rival-brains).
  • [6] The 20 Games Challenge (20gameschallenge.com), the anchor list for your pitches.
CS 276