Lecture W2R1: Input, Real Movement, and the Physics Conversation
September 3, 2026
LECTURE W2R1 // COLLISION COURSE
velocity + move_and_slide(), on the physics clock, into actual walls.Housekeeping: Lab 1 is due Thursday, September 10, 9:59 PM. Push as you go.
2 minutes with your neighbor:
Blitz moves, faces, eats; the count climbs; the ghost wanders, and two things are wrong on purpose:
position around in _process: no physics, no walls, and the wrong clock the moment collisions matter.body_entered fires for any body, so the ghost eats our marionberries.Switching branches with Godot open? Scenes and scripts reload from disk, but Project Settings do not: the editor keeps its cached Input Map and layer names, and will happily save them back over your checkout. After any checkout that touches project.godot: Project → Reload Current Project (and do not save before you do).
The ui_left family got us through week one. It was never meant for gameplay:
ui_* actions belong to menus. They are what the engine uses to navigate buttons and lists.move_left, dash, chomp. The action names what the player means, not which key they pressed.An input action is a named abstraction over physical inputs. Your code should ask “does the player want to move left?” and never “is the A key down?”
move_left, click Add.move_right (Right, D), move_up (Up, W), move_down (Down, S). Close the dialog.The actions live in project.godot. Your input scheme is part of the game, exactly like a scene or a script. (Do this on main, where no actions exist yet. On the demo branch they are already there, and Godot will refuse the duplicate names. Finished version: tag W2R1a.)
Open blitz.gd. The entire if/elif ladder from week one:
Becomes:
get_vector reads four actions as one Vector2. Last week’s ladder could not even do diagonals; this gets them for free!(0.707, 0.707), not (1, 1). No diagonal speed cheat. This is a bug most first games ship.match casualtyOne thing breaks blitz.gd: _update_facing pattern-matched exact values (Vector2.LEFT), and a diagonal is neither left nor up (it is both). A continuum needs logic. Replace the whole function with:
Dominant axis wins: mostly-horizontal means flip, mostly-vertical means tilt. match is for shapes of values; thresholds are for ranges.
CharacterBody2D has been waiting since day one. It owns a velocity, and it knows how to move. Back in blitz.gd, _process becomes:
velocity is a built-in property of the CharacterBody2D class that we extended.move_and_slide() consumes it: moves the body, detects collisions, resolves them. The physics engine now owns motion. So the code lives in _physics_process.clamp line is gone. Something better is about to hold Blitz in.* delta go?Nowhere: it moved inside. velocity is still pixels per second; move_and_slide() multiplies by the physics tick’s delta for you.
This means: position += velocity * delta and move_and_slide() together is double movement. You need one owner of motion per body. The moment the physics engine takes over, your arithmetic goes away.
Bodies so far: CharacterBody2D (moves, collides) and Area2D (senses, never blocks). The third archetype is StaticBody2D: never moves, never senses. It just is. Perfect for walls.
StaticBody2D, renamed Walls.CollisionShape2D child, Shape → New RectangleShape2D, size it into the top border (about 1152 x 24, centered at the top).ColorRect child, sized to match, so you can see it.walls.tscn, instance it into main.tscn (chain-link icon), save, run.Blitz hits the wall and stops. That is our first real collision: the physics engine noticed two shapes, refused the overlap, and resolved it.
Now hold a diagonal into a wall: Blitz slides along it instead of sticking. That is the “slide” in move_and_slide. the engine cancels the into-wall part of your velocity and keeps the rest. Every 2D game that feels good does exactly this; you get it for free.
One problem left. Run through the berry row… and watch the ghost do the same. It still eats everything.
The berry’s whole brain (unchanged):
body_entered fires for any physics body. Bearcat, ghost, falling piano: the berry cannot tell.if body.name == "Blitz" (identity checks in code). It works… until there are four rivals, figs, and a maze of moving parts, and every object interrogates every other.Every collision object carries two bitmasks. collision_layer answers “what am I?” (which groups I belong to). collision_mask answers “what do I notice?” (which groups I am scanning for). Contact only exists when one object’s mask meets another’s layer.
First, name the groups so the checkboxes mean something:
Project → Project Settings → General, search “layer names” → Layer Names → 2D Physics: name layer 1 world, 2 player, 3 rivals, 4 snacks.
Select each scene’s root node, and in the Inspector find Collision:
| Node | Layer (“I am…”) | Mask (“I notice…”) | In English |
|---|---|---|---|
Blitz |
player | world | Walls stop me. Berries are not my problem; I am theirs. |
Ghost |
rivals | world | I am a rival. (I float through walls for now. I am a ghost.) |
Berry |
snacks | player | I am a snack, and I only notice the player. |
Walls |
world | - | I am the world. I notice nothing. I outlast you all. |
That single player-only mask on the berry is the entire fix. (W2R1c)
Run it. The ghost drifts straight through the berry row, and the berries survive. The rules moved into data: checkboxes the physics engine reads before your script ever runs.
Remember Blitz’s four class years? First-Year fits through crawlspaces the big kids cannot. You now hold both halves of that mechanic: a crawlspace is a gap in the world layer, and “fits” is nothing more than a smaller CollisionShape2D. Growth stages swap shape data (same masks, same code).
Today’s work lives on the W2R1-demo branch, and each checkpoint is a git tag you can jump to by name:
| Tag | State |
|---|---|
| W2R1a | Input actions + get_vector; Tuesday’s probes retired |
| W2R1b | velocity + move_and_slide(); walls up, clamp gone |
| W2R1c | Layers and masks: the ghost goes hungry (the branch tip) |
Two git lessons ride along: checking out a tag leaves you “detached” from any branch (sightseeing, not building); git checkout W2R1-demo brings you home. And git show W2R1b prints exactly what that step changed.
Switching branches with Godot open? Scenes and scripts reload from disk, but Project Settings do not: the editor keeps its cached Input Map and layer names, and will happily save them back over your checkout. After any checkout that touches project.godot: Project → Reload Current Project (and do not save before you do).
Your Lab 1 keeps its arithmetic on purpose (nothing physical has taken over there). But two of today’s tools transfer:
Input.get_vector a one-line replacement for the ladder, with normalized diagonals free.velocity + move_and_slide() put movement on the physics clock; the body applies delta itself, and one owner of motion per body.StaticBody2D completes the archetype set: moves-and-collides, senses, just-is.Lab 1: Paddle Game is due Thursday, September 10, 9:59 PM: game pushed to main, Your Submission section filled: screenshot, your custom feature, your reflections.
Reading for next week (highly recommended): Godot: Scene organization. It is the best-practices version of the thing you have been doing by instinct.
Next week: scenes as reusable objects: composition, instancing, and how big games stay sane. Then the marionberry count finally reaches the screen: signals.
W2R1-demo: checkpoint tags W2R1a, W2R1b, W2R1c.