Learning Goals
3 minBy the end of this lesson you can:
- Place an Actor by
midbottomso its feet meet a surface exactly. - Store your own attributes on an Actor, and say why
homeis worth keeping. - Write a one-point ground probe and use it to turn a walker round at a ledge.
- Explain why the turn happens once per frame, after the move, and not inside each check.
Warm-Up · How Does a Skeleton Know Where the Ledge Is?
5 minA skeleton walks right along a three-tile ledge. It must turn round before stepping off. Suggest a way it could know the edge is coming — then check yours against these:
- Remember the ledge. Store where the platform starts and ends, and turn at those numbers. Works, but every skeleton now needs to be told about its own platform.
- Fall and notice. Let it walk off, see that it is falling, and put it back. Works, but now skeletons need gravity, a
vy, and a landing loop — all the machinery the player needed. - Look before you step. Ask one question about one point: is there floor just past my toes? No means turn.
A monster that never leaves the ground never needs to fall — so it never needs gravity or collision resolution. One point test replaces the whole two-pass system the player needs. Cheap enemies are normal: most of the walking enemies in the platformers you grew up with are about this simple.
New Concept · Probes, Own Attributes, and Turning Once
12 minA point test, not a rectangle test. overlaps() asks whether two boxes touch. ground_at() asks something smaller: is this one pixel inside any platform?
def ground_at(x, y): """True if some platform covers the single point (x, y).""" for p in platforms: if p.left <= x < p.right and p.top <= y < p.bottom: return True return False
It earns its own function because "is there floor here?" is a question you will ask again the moment you add anything else that walks.
Where to put the probe. Just past the toes, and a little below the feet:
toes = m.right + 1 if m.vx > 0 else m.left - 1 turn = not ground_at(toes, m.bottom + 2)
Past the toes, because you are asking about the next step, not this one. Below the feet, because m.bottom is level with the platform's top surface — a pixel or two lower is safely inside the tile.
Your own attributes. m.home is not a Pygame Zero feature. An Actor is an ordinary Python object, so you can hang your own names on it — you have done this already with player.vx and player.on_ground. Here home remembers where the monster started so respawn() can put it back.
midbottom, not pos. pos is the middle of the sprite, so placing a monster by pos means working out half its height first. midbottom is the point between its feet — exactly what has to line up with a platform's surface. So the y in your monster list is simply the y of the platform it stands on, copied straight out of LEVEL.
A skeleton can have several reasons to turn in the same frame: a ledge ahead, a platform in the way, the window edge. Set a single turn flag, let each check push it to True, and reverse vx once at the end.
Reverse it inside each check instead and a skeleton wedged in a corner flips twice in one frame — which leaves it walking back into the wall, shivering on the spot forever.
Build Log · Stage 11
14 minThree passes — standing, walking, dangerous — and you run the game after each one.
Put them on the platforms
skeleton.png is 599 × 333, and as with the player the drawing does not fill it: the skeleton sits in a 240 × 278 box whose top-left corner is at (211, 27). Same problem as PLT-01, same fix.
platformer.py · add to the tuning block
MONSTER_SPEED = 2 # skeletons walk slower than the player
platformer.py · add after the platforms loop
# --- monsters: (x, y) = where the feet stand, y is a platform's top --- MONSTERS = [ (250, 570), # left half of the ground (620, 570), # right half of the ground (300, 470), # the three-tile ledge (500, 390), # the platform above it ] monsters = [] for x, y in MONSTERS: # skeleton.png is 599x333; the drawing sits in a 240x278 box inside it m = Actor('skeleton', subrect=(211, 27, 240, 278)) m.scale = 0.22 m.midbottom = (x, y) m.home = (x, y) # where respawn() puts it back m.vx = MONSTER_SPEED monsters.append(m)
platformer.py · update draw()
for p in platforms: p.draw() for m in monsters: m.draw() player.draw()
Monsters are drawn after the platforms so they stand in front of the tiles, and before the player so that when you collide, you are the one on top.
Run it — checkpoint 11a
- Four skeletons, each standing on a platform with its feet on the surface — not sunk into it, not hovering above it.
- They do not move yet.
- If one floats, the
yyou gave it is not a real platform top.
Make them walk, and stop at the edge
platformer.py · add after overlaps()
def ground_at(x, y): """True if some platform covers the single point (x, y).""" for p in platforms: if p.left <= x < p.right and p.top <= y < p.bottom: return True return False def update_monsters(): for m in monsters: m.x += m.vx # look one pixel past the toes and a little below the feet. # nothing there means the next step walks off the ledge. toes = m.right + 1 if m.vx > 0 else m.left - 1 turn = not ground_at(toes, m.bottom + 2) # a platform in the way is a wall: stop against it and turn for p in platforms: if overlaps(m, p): if m.vx > 0: m.right = p.left else: m.left = p.right turn = True # the window edges count as walls too if m.left < 0: m.left = 0 turn = True elif m.right > WIDTH: m.right = WIDTH turn = True if turn: m.vx = -m.vx m.flip_x = m.vx < 0
platformer.py · add near the end of update()
# ---- monsters ---- update_monsters()
Three details, all of them the same lesson as earlier stages:
- Move first, then look. The probe reads
m.rightandm.left, which only describe the position after the step. Probe before moving and the monster turns a frame early, creeping backwards from the edge over time. - Collect the reasons, turn once — as in §03.
flip_xonly on the turn. Same reasoning as PLT-04: assigning to it rebuilds the sprite, and a monster changes direction about once a second.
Run it — checkpoint 11b
- Each skeleton paces its own platform and turns at both ends, facing the way it walks.
- None of them steps off an edge; the two on the ground turn at the pit and at the window edge.
- Walking into one still does nothing.
Make them cost something
You already have overlaps and you already have respawn, so this is four lines.
platformer.py · add after update_monsters(), still in update()
for m in monsters: if overlaps(player, m): respawn() break
The break matters. respawn() moves the player away, so carrying on round the loop would be testing a player who is no longer where the loop thinks he is.
platformer.py · update respawn()
def respawn(): player.pos = START_POS player.vx = 0 player.vy = 0 for m in monsters: m.midbottom = m.home m.vx = MONSTER_SPEED m.flip_x = False
Sending the monsters home is not decoration; it is what keeps the game playable. Leave it out and you reappear while each skeleton is wherever it happened to have wandered to — and if one has wandered onto your landing spot, you die again on the next frame, and the next, forever.
Run it — checkpoint 11
- Four skeletons pace their platforms and turn at every edge.
- Each one faces the direction it is walking.
- Touching one puts you back at the start — from the side, from above, from below. (Harsh. Next lesson softens it into a health bar.)
- After a respawn every skeleton is back where it began, and you do not immediately die again.
- R resets the skeletons too.
MONSTER_SPEED is 2 against your MOVE_SPEED of 5, so you can always outrun one. A skeleton is about 61 pixels tall against a jump of roughly 163, so you can always clear one. Push MONSTER_SPEED up and watch how quickly the level stops feeling fair.
Try It Yourself
13 minAdd one more skeleton, on a platform of your choice. One tuple in MONSTERS — and the y must be a real platform top, copied from LEVEL.
Then deliberately give it a y that is 10 pixels too high, run it, and describe what happens. That shiver is worth seeing once, because you will meet it again in your own levels.
Draw the probe. In draw(), loop the monsters and put a small dot exactly where update_monsters() is looking, then walk one to a ledge and watch the dot leave the platform a moment before it turns.
Hint
for m in monsters: toes = m.right + 1 if m.vx > 0 else m.left - 1 screen.draw.filled_circle((toes, m.bottom + 2), 3, (255, 0, 0))
Drawing what a check is testing is the fastest way to debug anything positional. Delete it once you have seen it.
Mini-Challenge 🔥 · Debug: The Sky-Walking Skeleton
8 minNia's skeletons walk right to the end of a ledge and keep going, straight out over the pit, until they hit the window edge and come back. Here is her probe:
toes = m.left - 1 if m.vx > 0 else m.right + 1
turn = not ground_at(toes, m.bottom + 2)Say what her code is actually asking about, and why it almost always answers "yes, there is floor".
Answer
She has swapped the two sides. When vx is positive the skeleton is walking right, so the next step lands past m.right — but her code probes m.left - 1, which is the floor it has just walked over. That is nearly always solid, so turn stays False and it strolls off the ledge.
Fix: toes = m.right + 1 if m.vx > 0 else m.left - 1. The probe must sit on the side you are heading towards. It is worth checking with the red dot from Try-It 02 rather than reasoning about it twice.
Recap
3 minMonsters are placed by midbottom so their feet meet a platform top copied from LEVEL, and they carry your own attributes — vx and home. They need no gravity, because they never leave the ground: a single ground_at() probe past the toes tells them a ledge is coming. Walls and window edges add reasons to turn, all collected into one flag so vx is reversed exactly once per frame. Touching one calls the same respawn() as the pit, which now also sends every skeleton home.
Vocabulary Card
- midbottom
- The point between an Actor's feet. Use it to stand something on a surface.
- probe
- A single point tested against the world — cheaper than a rectangle, and enough for "is there floor here?".
- patrol AI
- Walk, look ahead, turn at a ledge or wall. The simplest enemy behaviour there is, and still the most common.
- custom attribute
- A name you add to an Actor yourself, such as
homeorhp. Python objects let you do this freely.
Homework
4 minPopulate your own level from PLT-03/04. Place three to five skeletons so that the route to the highest platform is possible but tense: at least one ledge you must time, and at least one you can simply outrun.
Then write two sentences: which skeleton is the hardest, and what you would change if a younger player found it unfair.
Hint: a skeleton on a two-tile ledge patrols 120 pixels, so it is back within a second. On a seven-tile ground row it is away for much longer — placement changes difficulty far more than speed does.
Sample · a route with one tense step
MONSTERS = [ (120, 570), # ground: easy, you can walk round it (640, 570), # ground, far side: guards the way back (240, 480), # 2-tile ledge: patrols 120px, so it is always near (450, 320), # 2-tile ledge just below the top: the tense one ]
The tense skeleton is the one on a short ledge you must land on. The fair version gives you somewhere to wait — a wider ledge below, where you can watch it turn before committing. If a younger player struggles, widen that ledge from two tiles to three before you touch MONSTER_SPEED: more room to react is kinder than a slower enemy.