Learning Goals 3 min
By the end of this lesson you will be able to:
- Replace big change x by (5) hops with a tunable move-speed variable so the player glides instead of jumps.
- Combine four if <> then arrow-key checks inside one forever so diagonal movement works when two keys are held.
- Use last-good-x / last-good-y snapshots to roll back into open space and stop the "stuck in the corner" bug.
Warm-Up — the sprite that jumps through walls 7 min
Last lesson you built a maze with black walls and added the basic wall-bounce. Here's the script you probably ended with — and three problems hiding in five lines.
when flag clicked
forever
if <key [right arrow v] pressed?> then
change x by (5)
if <touching color [#000000] ?> then
change x by (-5)
end
end
end
Predict: the player walks right and hits a wall. What looks wrong about the motion?
Reveal bug #1
The cat jumps across the screen in chunks of 5 pixels. At change x by (5) per frame, movement is jerky like a jumping kangaroo. Real maze games glide. The fix is smaller steps — but if you just type 1 everywhere, you have to edit four blocks every time you want to tune it. We'll use a variable instead.
Predict: the player holds the right arrow and the up arrow at the same time. What happens?
Reveal bug #2
If your forever loop only has one if-then (just the right-arrow), the player can only move in one direction at a time. To allow diagonal movement, the loop needs four if-thens in a row — one per arrow — all checked every frame.
Predict: the player walks right, hits a wall, then presses up. What happens?
Reveal bug #3 — the "stuck in corner" bug
The rollback (change x by (-5)) puts the player back where they were last frame. But what if last frame's spot is also touching the wall, because the player has been pushing into the corner for a while? The player gets wedged — every direction touches black, every rollback puts them right back into black. Fix: remember the last-known-good position in two variables and snap back to that instead.
Today we fix all three. The result: a player sprite that glides smoothly, accepts diagonals, and never passes through (or wedges into) a wall.
New Concept — speed variable + last-good snapshot 15 min
Two ideas combine to fix everything in one stack: a tunable speed and a safe-position memory.
The move-speed variable
Make a variable called move-speed (Variables → Make a Variable → "For all sprites"). At the top of your script, set it once:
when flag clicked
set [move-speed v] to (3)
Now every change x by () uses the variable instead of a hard-coded number. Want faster? Change 3 to 5. Want slow-motion mode? Set it to 1. The game tunes from one block, not eight.
Four checks, one loop
For diagonal movement, all four arrow keys must be checked every frame. Sit four if <> then blocks in a row inside the same forever:
forever
if <key [right arrow v] pressed?> then
change x by (move-speed)
end
if <key [left arrow v] pressed?> then
change x by ((0) - (move-speed))
end
if <key [up arrow v] pressed?> then
change y by (move-speed)
end
if <key [down arrow v] pressed?> then
change y by ((0) - (move-speed))
end
end
Note the (0) - (move-speed) trick to make a negative version of the variable. There is no "change x by negative variable" block in Scratch — you build it with subtraction.
Last-good-x and last-good-y
This is the heart of bounded movement. Before moving each frame, remember where you are. After moving, if you ended up in a wall, snap back to the remembered spot.
forever
set [last-good-x v] to (x position)
set [last-good-y v] to (y position)
if <key [right arrow v] pressed?> then
change x by (move-speed)
end
if <touching color [#000000] ?> then
set x to (last-good-x)
set y to (last-good-y)
end
end
Why does this beat the old change x by (-5) rollback? Because last-good-x is only ever overwritten after a frame the player was safely in open space. The wedge can never happen, because we never save a wedged position.
Worked Example — the glider 12 min
Open the maze project from L03-33. We'll upgrade the player sprite from jerky to glassy-smooth in eight steps.
Step 1 — Make the variable
Variables palette → Make a Variable → name it move-speed → "For all sprites" → OK. Uncheck the box next to it on the Stage if you don't want it shown.
Step 2 — Make two more variables
Same way, make last-good-x and last-good-y. Uncheck both on the Stage — these are internal bookkeeping, not score.
Step 3 — Drop the hat and initialise
From Events: when ⚑ clicked. From Variables: set [move-speed v] to (3). From Motion: go to x: (-200) y: (-150) (the start corner of your maze).
Step 4 — Snapshot at the top of the loop
Drop a forever. Inside, first two blocks: set [last-good-x v] to (x position) and set [last-good-y v] to (y position).
Step 5 — Add the four arrow checks
Four if <> then blocks. Each diamond gets one key [right arrow v] pressed? (left, up, down). Inside each, the matching change x by () or change y by (), using move-speed or (0) - (move-speed).
Step 6 — Add the wall-bump rollback
After all four key checks, one more if <> then with touching color [#000000] ? in the diamond. Inside: set x to (last-good-x) and set y to (last-good-y).
Step 7 — Click the flag and try to glitch it
Walk into a wall. Hold two keys and push into a corner. Try every nasty move you can think of. The player should always either move freely or snap to the last open spot.
Step 8 — Tune
Change move-speed's initial value. Try 1 (sluggish), 3 (just right), 5 (twitchy), 8 (passes through thin walls — too fast!). 3 or 4 is the sweet spot for most mazes.
The full assembled stack
when flag clicked
set [move-speed v] to (3)
go to x: (-200) y: (-150)
forever
set [last-good-x v] to (x position)
set [last-good-y v] to (y position)
if <key [right arrow v] pressed?> then
change x by (move-speed)
end
if <key [left arrow v] pressed?> then
change x by ((0) - (move-speed))
end
if <key [up arrow v] pressed?> then
change y by (move-speed)
end
if <key [down arrow v] pressed?> then
change y by ((0) - (move-speed))
end
if <touching color [#000000] ?> then
set x to (last-good-x)
set y to (last-good-y)
end
end
What you just built: the same player controller used in Pac-Man, Zelda, Pokémon, and every roguelike. Snapshot, attempt, validate, rollback. Bigger games add animation and inertia, but the bones are these.
Try It Yourself — three movement drills 15 min
Goal: Add a "slow-mo" mode. While the shift key is held, set move-speed to 1. When it's released, set it back to 3. Drop this above the four arrow checks.
forever
if <key [shift v] pressed?> then
set [move-speed v] to (1)
else
set [move-speed v] to (3)
end
end
Think: Why does this feel right with two separate forever loops instead of one big one? Because the speed-tuning loop and the movement loop have nothing to do with each other — splitting them keeps each one short and readable.
Goal: Add a "boost" cooldown. Pressing space sets move-speed to 6 for one second, then back to 3. While boosting, the player can plow through narrow corridors faster.
when [space v] key pressed
set [move-speed v] to (6)
wait (1) seconds
set [move-speed v] to (3)
Think: Hold space down — what happens? Scratch's when [space v] key pressed hat fires once per press, not per frame, so holding doesn't stack boosts. Tampering with this is your gateway into power-ups in L03-37.
Goal: Some maze games slide the player along the wall instead of stopping dead. If the player tries to move diagonally and the diagonal move hits a wall, try the horizontal-only version, then the vertical-only version, before giving up. Split the rollback into x-rollback and y-rollback.
forever
set [last-good-x v] to (x position)
set [last-good-y v] to (y position)
if <key [right arrow v] pressed?> then
change x by (move-speed)
end
if <touching color [#000000] ?> then
set x to (last-good-x)
end
if <key [up arrow v] pressed?> then
change y by (move-speed)
end
if <touching color [#000000] ?> then
set y to (last-good-y)
end
end
Think: This is the trick behind Mario-style movement. Hitting a wall on the side shouldn't kill upward momentum. Independent axis rollback gives players that "the controls feel good" feeling.
Mini-Challenge — the cat in the kopitiam 5 min
"Mei Ling's wedged kucing"
Mei Ling is building a kopitiam maze. The player kucing has to deliver a kopi-o from the kitchen to a table without hitting any tables (drawn in black). She wrote this:
when flag clicked
set [move-speed v] to (4)
forever
if <key [right arrow v] pressed?> then
change x by (move-speed)
if <touching color [#000000] ?> then
change x by ((0) - (move-speed))
end
end
end
Mei Ling complains: "When I push into the corner where two tables meet, my kucing freezes and I can't reverse out!" Why does her stack wedge, and how do you fix it without rewriting from scratch?
Reveal one valid solution
The bug: when the player is already touching the wall (say, after a previous frame's nudge left them grazing it), Mei Ling's rollback uses change x by ((0) - (move-speed)). That moves them further back — sometimes into another wall. Now the touching-colour check is still true, but the next frame's rollback moves them in yet another direction. They're trapped bouncing between two walls.
The fix is the snapshot pattern. Save the position before moving; snap back to that saved spot if the move ends in a wall:
when flag clicked
set [move-speed v] to (4)
forever
set [last-good-x v] to (x position)
set [last-good-y v] to (y position)
if <key [right arrow v] pressed?> then
change x by (move-speed)
end
if <touching color [#000000] ?> then
set x to (last-good-x)
set y to (last-good-y)
end
end
Same idea, different mechanism. The snapshot is taken before the move attempt, so it's always a known-safe spot. Mei Ling's kucing can always reverse out of any corner, because the last-good position is always reachable. Memory beats recalculation, every time.
Recap 3 min
You replaced the jerky change x by (5) jumps with a tunable move-speed variable, added four arrow-key if-thens for diagonal movement, and used last-good-x / last-good-y snapshots to roll back into safe positions instead of out into more walls. The player now glides, accepts two keys at once, and can never get wedged. This is the standard top-down player controller — every maze game on Scratch (and most professional ones) uses some variant of it.
- Tunable variable
- A variable used in place of a hard-coded number so you can change the game's feel from one spot. move-speed is tunable — edit one set-block, the whole game speeds up.
- Snapshot pattern
- Saving a value before trying something risky so you can restore it if the try fails. Used here for player position; also used in databases and "undo" buttons.
- Rollback
- Returning to a previous safe state after detecting a problem. set x to (last-good-x) is a rollback to the pre-move position.
- Wall-bump / wedge
- The bug where reversing a move pushes the player into another wall, causing them to oscillate or freeze. Fixed by rolling back to a remembered safe spot, not by reversing the most recent move.
- Diagonal movement
- Moving in both x and y the same frame because two keys are held. Requires four independent if-thens (not one if-else chain) in the same forever loop.
Homework 2 min
The Smooth-Mover Drill. Take last lesson's maze project and upgrade it with the new pattern.
- Open your
HW-L3-33-Maze.sb3(or start fresh — draw a simple black-walled maze on the backdrop). - Make three variables: move-speed, last-good-x, last-good-y. All "For all sprites".
- Replace the player's movement script with the worked-example stack. Set move-speed to 3.
- Test: hold right + down. Does the player move diagonally?
- Test: push into a corner from any direction. Can you always reverse out?
- Tune: try move-speed at 2, 4, and 6. Which feels best in your maze?
Save as HW-L3-34-Glider.sb3.
Bring back next class:
- The
.sb3file. - One sentence answering: "What's the smallest move-speed that still feels playable, and the largest before the player phases through a wall?"
- One sentence answering: "Why did we put the snapshot blocks at the top of the forever instead of the bottom?"
Heads up for next class: SCR-L03-35 adds win conditions — a green goal square the player has to reach, plus a multi-key collect-em-up version using lists. The maze is about to become a real game.