
Lecture W1R1: From CS 152 to GDScript
August 27, 2026
Tuesday you sketched Pac-Man’s scene tree. Over the last 48 hours, the project got more ambitious.
Welcome to Pac-Blitz.

| Blitz mechanic | Course topic | Week |
|---|---|---|
| Who can fit through which gap | Collision layers & masks | Next week |
| Berry eaten → HUD updates | Signals | 3 |
| First-Year → Sophomore → Junior → Senior | State machines | 4-5 |
| Growth thresholds, berry values | Data-driven design | 6 |
| Buildings and floors as the map | Level building | 10 |
Every lecture from here on upgrades this game. By the showcase, Pac-Blitz will be a complete original game - and you will have watched every system get built.
LECTURE W1R1 // BUILDING PAC-BLITZ
Tuesday you drew the tree. Today we make it real, and learn GDScript on the way:
Before you sketch: the menu you are ordering from. One node, one job.
| It should… | Reach for | Pac-Blitz example |
|---|---|---|
| Move and hit things | CharacterBody2D | Blitz, whatever chases Blitz |
| Just notice overlap | Area2D | Marionberries, figs |
| Draw an image | Sprite2D | Every visible thing |
| Draw text | Label | Berry count, “READY!” |
| Be a wall grid | TileMapLayer | A building floor |
| Play a sound | AudioStreamPlayer | Chomp |
| Count down | Timer | Enemy decisions |
| Float above the game | CanvasLayer | The HUD |
The decision rule: does it move? Body. Does it sense? Area. Does it show? Sprite or Label. This slide stays one keypress away while you work.
Back to Tuesday’s pairs. 3 minutes: finish your scene tree - and sketch it as Blitz’s game now. Decide:

A solution, not the solution. What matters: one scene per kind of thing, node choice is a design statement (movers are CharacterBody2D, overlap-detectors are Area2D), and the berry is authored once, instanced 240 times.
We are building the first slice, and cutting like pros:
| Today | Punted (deliberately) |
|---|---|
| Blitz moves and faces | Walls and real collision |
| Marionberries vanish, count climbs | Berry count on screen |
| A placeholder ghost wanders | Enemy brains (chase!) |
| Blitz’s growth stages | |
| The actual building |
Scoping is a design skill. Everything in the right column has a scheduled week - the cut list is the future lesson plan.
Clone the starter - it has the sprite pack and an empty Main scene, so you can build everything else with me:
Open project.godot in Godot. Run it: an empty dark window is correct - that is the void we fill today.
Stuck, behind, or curious? My finished version of every checkpoint for today lives in the reference repo: github.com/LucasCordova/pac-blitz (branches Build1-Build3 + main).
Exactly the tree you sketched. Build it with me:
CharacterBody2D - Blitz moves, so Blitz is a body.Blitz.See the yellow warning triangle next to the node? Godot is complaining that a physics body has no shape. It is right, and we will feed it in two steps. Nodes tell you what they need.
With Blitz selected, press Ctrl+A (Cmd+A on Mac) to add a child node:
Sprite2D. In the Inspector its Texture slot says <empty>: drag assets/blitz.png from the FileSystem dock (bottom left) onto that slot. Blitz appears in the viewport.Blitz again (children attach to whatever is selected!), Ctrl+A / Cmd+A, add a CollisionShape2D. In the Inspector: Shape → New CircleShape2D, then drag the circle’s orange handles until it roughly covers Blitz.blitz.tscn.First scene: done. But do not press Run yet - there is a trap ahead.
This is the step everyone misses. Run (F5, or Cmd+B on Mac) launches the project’s main scene - and our main.tscn is still an empty Node2D. Blitz exists; nobody invited him in.
main.tscn: double-click it in the FileSystem dock.Main root, then click the chain-link icon at the top of the Scene dock (“Instantiate Child Scene” - Ctrl+Shift+A / Cmd+Shift+A) and choose blitz.tscn.(0, 0) - the top-left corner, half off screen. Drag him to the middle, or set Transform → Position to about (576, 324) in the Inspector.Blitz stands in the void, majestic and inert. Time for GDScript.
Back in blitz.tscn: right-click the Blitz root → Attach Script. Godot pre-fills extends CharacterBody2D - it already knows what your script is-a. Keep the path res://blitz.gd, click Create. (There is also a scroll-with-a-green-plus button at the far right of the Scene dock’s toolbar, past the filter box - it only appears while a scriptless node is selected. Right-click never hides.)
Now type this below the extends line:
var declares, like the languages you know; : float is optional static typing. Use it. Typed code autocompletes better and fails louder.:= infers the type from the value (you will meet it in a minute).@export hoists the variable into the Inspector: tune without touching code.extends names the class this script is. Your script literally is-a CharacterBody2D.func _process(delta: float) -> void:
var direction := Vector2.ZERO
if Input.is_action_pressed("ui_left"):
direction = Vector2.LEFT
elif Input.is_action_pressed("ui_right"):
direction = Vector2.RIGHT
elif Input.is_action_pressed("ui_up"):
direction = Vector2.UP
elif Input.is_action_pressed("ui_down"):
direction = Vector2.DOWNfunc, blocks are indentation (Python students smile, Java students adjust).Vector2.LEFT is (-1, 0).Input is global; "ui_left" is a built-in action. Custom actions (“dash”, “chomp”): next week.Two more lines close the loop:
Save the script (Ctrl+S / Cmd+S - unsaved code does not run), then run the project: F5 / Cmd+B. Arrow keys. Blitz moves. The first two minutes of every game you will ever write look exactly like this.
Experiment: delete * delta and run again. Speed now depends on frame rate - fast laptop, fast Blitz. delta (seconds since last frame) is what makes motion time-based, not frame-based. That is Tuesday’s Game Loop reading, live.
Every one of these is a rite of passage:
| Symptom | Cause and cure |
|---|---|
| Empty dark window | Blitz is not in main.tscn - back to Step 3. F5 runs the main scene, not the scene you are editing. |
| Wrong scene ran | F6 / Cmd+R runs the current scene; F5 / Cmd+B runs the project. Know which you pressed. |
| Keys “work” but Blitz is invisible | The Sprite2D has no texture - drag blitz.png onto it. |
| Blitz hides in the corner | He is at (0, 0). Drag him to the middle of Main and save. |
| Code edited, behavior unchanged | The script is not saved. Ctrl+S / Cmd+S, run again. |
Debugging checklist, forever: which scene ran, is the node in it, is the script saved?
You just used: extends, typed var, @export, func, if/elif, Vector2, delta, and a method call with a return value. That is half of GDScript’s daily vocabulary, and it took one moving bearcat.
In the class repo, this moment is git checkout Build1.
Blitz slides around staring right the whole time. A bearcat should chomp in its direction of travel.
We need: “given a direction, set the sprite’s rotation and flip.” Four cases. We could if/elif it, but GDScript has something better for shape-of-the-value branching:
matchfunc _update_facing(direction: Vector2) -> void:
var sprite := $Sprite2D as Sprite2D
match direction:
Vector2.RIGHT:
sprite.flip_h = false
sprite.rotation_degrees = 0
Vector2.LEFT:
sprite.flip_h = true
sprite.rotation_degrees = 0
Vector2.UP:
sprite.flip_h = false
sprite.rotation_degrees = -90
Vector2.DOWN:
sprite.flip_h = false
sprite.rotation_degrees = 90Call it from _process when direction != Vector2.ZERO.
$Sprite2D is a node reference: “my child named Sprite2D.” The tree you sketched Tuesday is addressable from code.flip_h for LEFT instead of rotating 180°? Predict, then try it.30 seconds with your neighbor: what would rotation_degrees = 180 do to the sprite that flip_h = true does not?
Rotating 180° turns Blitz upside down. Flipping mirrors the sprite. Every 2D game with a walking character makes this exact choice. (Yes, a climbing Blitz tilts sideways for now. It is charming. It ships.)
Same recipe, new root. Scene → New Scene, then:
Area2D, renamed Berry - it never moves; it only needs to notice overlap.Berry selected): a Sprite2D (drag assets/berry.png onto Texture) and a CollisionShape2D (Shape → New CircleShape2D, cover the berry).Berry → Attach Script, path res://berry.gd. Create.Berry root and open the Node dock - the tab next to the Inspector. Under Signals, double-click body_entered(body: Node2D), then click Connect. Godot writes _on_body_entered into the script for you.berry.tscn.A signal is a node announcing that something happened - a doorbell, not a phone call. body_entered rings whenever a physics body overlaps the Area2D. We subscribe to it. (Week 3 is entirely about this idea.)
body_entered → _on_body_entered for us; the connection lives in the scene, not the script.signal eaten declares our own doorbell; emit() rings it. Who listens? Not the berry’s problem. That is the whole point.queue_free(): “remove me from the tree at the end of this frame.” Eaten.Nobody. Open main.tscn, right-click the Main root → Attach Script (res://main.gd):
extends Node2D
const BERRY := preload("res://berry.tscn")
var berries: int = 0
func _ready() -> void:
for i in range(12):
var berry := BERRY.instantiate()
berry.position = Vector2(120 + i * 80, 324)
berry.eaten.connect(_on_berry_eaten)
add_child(berry)
func _on_berry_eaten() -> void:
berries += 1
print("Marionberries: ", berries)preload loads the berry scene as a value; instantiate() stamps out a copy: everything you did in the editor Tuesday, from code.for i in range(12): your CS 152 loops, unchanged.berry.eaten.connect(...): Main subscribes to each berry’s doorbell. Berries never know the count exists.Code can build the level. The editor and the script are two hands on the same scene tree.
Save everything and run (F5 / Cmd+B): chomp down the row, watch the marionberry count climb in the Output panel at the bottom of the editor. git checkout Build3
Every maze game needs pursuit. Pac-Blitz does not have its enemies yet - that is your design decision, coming up - so a placeholder ghost stands in. Same recipe, third verse: new scene, CharacterBody2D root named Ghost + Sprite2D (ghost.png) + CollisionShape2D, plus one new trick: a Timer child (Inspector: Wait Time 1, Autostart ON; connect its timeout signal in the Node dock - same ritual as the berry). Attach ghost.gd, save, then instance ghost.tscn into main.tscn (chain-link icon) and drop it at the far side of the room.
extends CharacterBody2D
const DIRECTIONS := [Vector2.LEFT, Vector2.RIGHT, Vector2.UP, Vector2.DOWN]
@export var speed: float = 160.0
var direction: Vector2 = Vector2.LEFT
func _process(delta: float) -> void:
position += direction * speed * delta
position = position.clamp(Vector2.ZERO, get_viewport_rect().size)
func _on_timer_timeout() -> void:
direction = DIRECTIONS.pick_random()Arrays and pick_random(): a one-line brain.
The ghost is eating our marionberries.
Why? Reread the berry’s code: _on_body_entered(body) fires for any physics body. The berry does not care who touched it - and we never told it to care.
Fixing this properly is called collision layers and masks, and it is exactly where next week begins. Until then: the ghost is hungry too.
Bugs that make sense are the best teachers. This one is a cliffhanger.
It cannot be ghosts. My pitch: our actual rivals.
(Original pixel caricatures only - a wildcat, a logger, a pirate. School logos are trademarks; ours stay ours.)
Between now and Tuesday, think about:
Bring answers Tuesday. The best ones ship.
The best part: you already have it. Your follow-along copy is the game - keep building on it.
For comparison or catch-up, the reference repo holds my version of every checkpoint, one branch per build:
| Branch | State |
|---|---|
| Build1 | Movement |
| Build2 | Facing |
| Build3 | Marionberries + count |
| main | Everything + the placeholder ghost |
Diff your build against mine, break it, tune the @export speeds, add a second ghost. It cannot hurt you.
Optional: Godot can show git diffs and commits inside the editor via the official Godot Git Plugin - install steps are in the repo README. A terminal works just as well; use whichever you will actually use.
var, func, if/elif, match, for, arrays.$Sprite2D) and buildable (preload + instantiate) from code.delta makes motion time-based. Never ship without it.Lab 0: Press Start is due Tuesday, September 1, 9:59 PM via Canvas: Godot installed, Learn GDScript From Zero completed, your first scene pushed. After today, the scripting parts should feel familiar.
Reading for Tuesday (highly recommended): Game Programming Patterns, Game Loop and Update Method - you saw both run today.
Next time: the game loop up close: _process vs _physics_process, and what a frame really is.
