A game loop
Apps wait for the user; games do not. A game updates the world many times per second, whether or not you press a key. This tutorial builds Dodge: blocks fall, you slide left and right, you collect stars and the game speeds up. About 130 lines, and every piece is reusable.
| Goal | Time | Level | You need |
|---|---|---|---|
| A playable arcade game | 40 minutes | Advanced · Python | A multi-view app |
What you’ll learn
Section titled “What you’ll learn”- Run code every frame with
pz.frames(callback)and move things by speed × dt. - Read the keys held down (
game.axis) instead of single key presses. - Detect collisions with
game.Rect. - Draw on a
pz.canvasand add glow, particles and screen shake. - Stack a HUD and a Game over card, made of normal widgets, over the canvas.

-
The loop. Return
pz.frames(self.tick)fromsubscriptions()and Pyzed callstick(dt)before every frame (60, 120 or 144 times a second, in sync with the screen).dtis the time since the last frame, in seconds. When the game is over, return nothing and the loop stops, so a paused game uses no CPU:def subscriptions(self):return [] if self.over else [pz.frames(self.tick)] -
Move by speed × dt. Speeds are in pixels per second. Multiplying by
dtmakes the game run at the same speed on any screen:self.x += game.axis(("left", "a"), ("right", "d")) * 380 * dt # -1, 0 or 1 × speed × timeself.x = game.clamp(self.x, 0, W - PLAYER_W)game.axisreads the keys held right now: hold the arrow and the paddle keeps moving. -
Spawn things on a timer. A countdown in seconds, reset when it reaches zero. It shrinks over time, so the game gets harder:
self.spawn -= dtif self.spawn <= 0:self.spawn = max(0.25, 0.8 - self.time * 0.01)size = self.random.uniform(24, 60)self.blocks.append(game.Rect(self.random.uniform(0, W - size), -size, size, size)) -
Collisions.
game.Rectis a box withoverlaps,containsandcenter:player = game.Rect(self.x, PLAYER_Y, PLAYER_W, PLAYER_H)if any(block.overlaps(player) for block in self.blocks):self.over = Trueself.shake.kick(10) # screen shakeself.sparks.burst(*player.center, count=40, color=["#f87171", "#fca5a5"]) -
Draw the world. A list of shapes from
pyzed.drawon apz.canvas.game.glowadds soft light,sparks.shapes()draws the particles andshake.shapes()moves everything a little after a hit:shapes = [draw.background("#0b0e14"), *self.shake.shapes()]shapes += [block.shape(fill="#ef4444", radius=6) for block in self.blocks]for x, y in self.stars:shapes += game.glow(x, y, 6, "#fbbf24", layers=3)shapes.append(draw.rect(self.x, PLAYER_Y, PLAYER_W, PLAYER_H, radius=7, fill="#22d3ee"))shapes += self.sparks.shapes(glow=True)return pz.canvas(shapes, width=W, height=H) -
HUD and Game over. These are not drawn on the canvas: they are widgets.
pz.stacklayers the Game over card (a glass panel with a button) over the canvas, and the score badges sit above it:play = pz.stack([self.board(), self.overlay()], width=W, height=H) -
Restart.
on_keygets single key presses; Enter resets the state and, sinceoveris false again,subscriptions()returns the loop and the game starts over.
Full code
Section titled “Full code”"""Dodge: a tiny real-time game built with the game loop of ``pyzed.game``.
python examples/tutorial/dodge.py
Move with the arrows or A/D, dodge the falling blocks, collect the stars. Enter starts again.
- ``pz.frames(self.tick)`` in ``subscriptions()`` calls ``tick(dt)`` once per displayed frame.- ``game.axis("left", "right")`` reads the keys held down: -1, 0 or 1.- ``game.Rect.overlaps`` checks collisions; ``game.glow``, ``game.Particles`` and ``game.Shake`` add light, sparks and screen shake.- The score and the "Game over" card are ordinary widgets stacked over the canvas.
Walkthrough: https://albakore.github.io/pyzed/tutorials/advanced/game-loop/"""
import random
import pyzed as pzfrom pyzed import draw, game, ui
W, H = 480, 560PLAYER_W, PLAYER_H, PLAYER_Y = 56, 14, H - 48
class Dodge(pz.App): title = "Dodge" size = (W + 32, H + 88) theme = "One Dark"
def __init__(self, seed=None): self.random = random.Random(seed) self.sparks = game.Particles(gravity=300, seed=seed) self.shake = game.Shake(seed=seed) self.best = 0 self.reset()
def reset(self): self.x = W / 2 - PLAYER_W / 2 self.blocks = [] # game.Rect objects falling down self.stars = [] # (x, y) of the stars to collect self.spawn = 0.0 # seconds until the next block self.time = 0.0 # seconds survived self.bonus = 0 # points from stars self.over = False
@property def score(self): return int(self.time * 10) + self.bonus
# -- the loop ------------------------------------------------------------------------- def subscriptions(self): return [] if self.over else [pz.frames(self.tick)]
def tick(self, dt): dt = min(dt, 0.05) # never jump too far after a pause self.time += dt speed = 220 + self.time * 8 # blocks fall faster over time
# 1. input: move the player by speed x dt, so it goes as fast at 60 Hz as at 144 Hz self.x = game.clamp(self.x + game.axis(("left", "a"), ("right", "d")) * 380 * dt, 0, W - PLAYER_W) player = game.Rect(self.x, PLAYER_Y, PLAYER_W, PLAYER_H)
# 2. spawn new things now and then self.spawn -= dt if self.spawn <= 0: self.spawn = max(0.25, 0.8 - self.time * 0.01) size = self.random.uniform(24, 60) self.blocks.append(game.Rect(self.random.uniform(0, W - size), -size, size, size)) if self.random.random() < 0.3: self.stars.append((self.random.uniform(16, W - 16), -16.0))
# 3. move everything and check collisions for block in self.blocks: block.y += speed * dt self.blocks = [b for b in self.blocks if b.y < H] self.stars = [(x, y + speed * 0.8 * dt) for x, y in self.stars if y < H] for x, y in list(self.stars): if player.overlaps(game.Rect(x - 12, y - 12, 24, 24)): # a star is a 24 px box self.stars.remove((x, y)) self.bonus += 50 self.sparks.burst(x, y, count=18, color=["#fde68a", "#fbbf24"]) if any(block.overlaps(player) for block in self.blocks): self.over = True self.best = max(self.best, self.score) self.shake.kick(10) self.sparks.burst(*player.center, count=40, color=["#f87171", "#fca5a5"])
# 4. effects self.sparks.update(dt) self.shake.update(dt)
def on_key(self, combo): if combo == "enter" and self.over: self.reset()
# -- drawing -------------------------------------------------------------------------- def board(self): shapes = [draw.background("#0b0e14"), *self.shake.shapes()] for block in self.blocks: shapes.append(block.shape(fill="#ef4444", radius=6)) for x, y in self.stars: shapes += game.glow(x, y, 6, "#fbbf24", layers=3) shapes += game.glow(self.x + PLAYER_W / 2, PLAYER_Y + PLAYER_H / 2, 4, "#22d3ee", layers=4, core=False) shapes.append(draw.rect(self.x, PLAYER_Y, PLAYER_W, PLAYER_H, radius=7, fill="#22d3ee")) shapes += self.sparks.shapes(glow=True) return pz.canvas(shapes, width=W, height=H)
def overlay(self): if not self.over: return None card = pz.container(pz.column([ ui.heading("Game over", level=2), ui.muted(f"Score {self.score} · Best {self.best}"), ui.button("Play again", self.reset, variant="solid", icon="rotate-ccw"), ], spacing=10, align="center"), padding=24).style(glass=True, radius=14) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center")
def view(self): t = self.resolved_theme() hud = pz.row([ui.label("Dodge", weight="semibold"), pz.hspace(), ui.badge(f"Score {self.score}", icon="star"), ui.badge(f"Best {self.best}", tint="warning", icon="trophy")], spacing=8, align="center") play = pz.stack([self.board(), self.overlay()], width=W, height=H) return pz.container( pz.column([hud, pz.container(play, radius=10, clip=True), ui.muted("Arrows / A-D move · Enter restarts", size="sm")], spacing=10), padding=16, width="fill", height="fill", bg=t.background, )
if __name__ == "__main__": pz.run(Dodge)uv run --with pyzed-ui python examples/tutorial/dodge.pypython3 -m venv .venvsource .venv/bin/activatepython -m pip install pyzed-uipython examples/tutorial/dodge.pypy -m venv .venv.venv\Scripts\Activate.ps1python -m pip install pyzed-uipython examples\tutorial\dodge.pypython3 examples/tutorial/dodge.pypy examples\tutorial\dodge.pyEvents in this tutorial
Section titled “Events in this tutorial”| Source | Event | Handler |
|---|---|---|
subscriptions() |
every displayed frame | pz.frames(self.tick): tick(dt) gets the seconds since the last frame. |
| The keyboard | keys held | game.axis(("left", "a"), ("right", "d")) reads them inside tick. |
| The app | on_key |
Enter starts again after a crash. |
A game reads the keys that are held in every frame instead of reacting to each press; on_key is for one-off
actions.
Try more events
Section titled “Try more events”def on_key(self, combo): if combo == "enter" and self.over: self.reset() elif combo == "p": # pause: stop returning the frame subscription self.paused = not self.paused
def subscriptions(self): return [] if self.over or self.paused else [pz.frames(self.tick)]
def board(self): ... return pz.canvas(shapes, width=W, height=H, on_press=lambda x, y: self.reset() if self.over else None) # click to restart- Making games: Snake, Breakout and Neon Invaders, step by step.
- A custom shader graph for a GPU background.