Game dev tips
Short, practical advice gathered from the games of this section. Each tip says why, with the few lines of code that do it in Pyzed.
Always multiply by dt
Section titled “Always multiply by dt”pz.frames(self.tick) calls tick(dt) once per displayed frame, and dt is the seconds since the last one.
Speeds are per second and every change is speed × dt, so the game runs the same on a 60 Hz laptop and a
144 Hz monitor. Timers count down by dt too:
self.x += self.vx * dtself.cooldown = max(0.0, self.cooldown - dt)Cap dt
Section titled “Cap dt”After a pause (a dragged window, a breakpoint, a slow frame) dt can be large, and a big step lets things pass
through walls. Pyzed already caps it at 0.25 s; physics-heavy games cap it lower:
def tick(self, dt): dt = min(dt, 1 / 30)A fixed timestep, when you need one
Section titled “A fixed timestep, when you need one”Some games need every step to be the same size: physics that must not depend on the frame rate, replays, networking, or a simulation that must give exactly the same result twice. Accumulate time and step in fixed slices:
STEP = 1 / 120
def tick(self, dt): self.acc += dt while self.acc >= STEP: self.simulate(STEP) # the game's logic, always with the same dt self.acc -= STEP self.blend = self.acc / STEP # 0..1: how far between two steps, to interpolate drawingFor turn-based or grid games, use pz.every(seconds, callback) instead of a frame loop, as
Snake does.
Stop the loop when nothing moves
Section titled “Stop the loop when nothing moves”subscriptions() is evaluated after every update. Return [] while paused, on a menu with nothing animated,
or between moves of a puzzle, and the game uses no CPU:
def subscriptions(self): return [pz.frames(self.tick)] if self.animating else []Performance
Section titled “Performance”- Keep
ticksmall. Plain numbers, lists and small classes are fast; avoid building big structures every frame, and avoid reading files or the network in the loop. - Batch shapes.
draw.rects([...], fill=color)draws many rectangles of one color as one shape; that is howgame.spriteand the platformer’s tiles stay cheap. - Draw only what is on screen, and collide only with what is near (see the platformer).
- Cap particles with
game.Particles(limit=...), and remove bullets and enemies that leave the screen. - Images are cached.
draw.image("ship.png", ...)decodes a file once, so drawing it hundreds of times per frame is fine. - The HUD is cheap, but
pz.memocan cache a widget that is expensive to build and rarely changes. - Measure. Show an FPS counter (see Orb Rush) and read the Performance guide.
-
Held vs pressed. Use
game.axis,game.vectorandpz.is_pressedintickfor things that happen while a key is down (moving, auto-fire), andon_keyfor things that happen once per press (jump, pause). -
Buffer inputs. Remember a press for a short time so it is not lost: a jump pressed just before landing, or two quick turns in Snake. A small timer or queue is enough:
def on_key(self, combo):if combo == "space":self.jump_buffer = 0.12 # seconds; tick counts it down and jumps when possible -
Be forgiving. Coyote time, slightly smaller hitboxes for the player than its sprite, and slightly bigger ones for pickups make a game feel fair.
-
Let people choose. Accept arrows and WASD (
game.vectordoes by default), and the mouse where it makes sense.
“Juice” is the feedback that makes an action feel good: the same hit, with and without it:

| Effect | How |
|---|---|
| Screen shake | shake = game.Shake(), shake.kick(8) on a hit, shake.update(dt) every frame, and start the canvas with *shake.shapes() |
| Particles | game.Particles(...), burst(x, y, ...) on events, update(dt) and shapes(glow=True) |
| Hit-stop | skip the world’s update for 50–100 ms after a big hit (keep particles moving) |
| Flash | draw the hit thing white for one or two frames |
| Squash and stretch | draw.scale(sx, sy) around the feet after landing, or along the direction of motion |
| Easing | tweens with ease_out curves instead of linear moves (see Sprites and animation) |
| Glow | game.glow(x, y, r, color) on canvas; bloom and glow on widgets |
Organizing a game
Section titled “Organizing a game”- State in one place. The app’s attributes are the game state;
reset()ornew_game()sets all of them, so starting again is one call. - Small classes for things.
Enemy,Rock,Tile: a few attributes each. Lists of them are the world. - Rules as functions. Code that decides what happens (
slide_line,can_move, collisions) should not draw; code that draws should not change the state. Then both are easy to change and to test. - Scenes. One
sceneattribute ("title","play","pause"…) decides whattickruns and whatviewshows (see Menus, HUD and settings). - Constants at the top. Speeds, sizes and timings as named constants are the game’s tuning knobs.
- Randomness with a seed.
self.random = random.Random(seed)makes a game reproducible for tests and bug reports;seed=Noneis random as usual.
Debugging
Section titled “Debugging”- Draw what the code sees: hitboxes, velocities, paths, the tile under the mouse. Toggle it with a key, as Jump Lab does with H.
- Print numbers on the canvas with
draw.text(..., font="mono")instead of to the console, so you see them in context. - Slow motion: multiply
dtby 0.25 while a key is held to watch a fast moment.
Testing games headlessly
Section titled “Testing games headlessly”A Pyzed game is a normal Python object: you can create it, call tick in a loop and check the result, with no
window. The games of this section let tests set the keys they read through a held attribute:
from coin_chase import CoinChase, SPEED
def test_moves_right(): app = CoinChase(seed=1) app.held = {"right"} # instead of the real keyboard x = app.x for _ in range(30): app.tick(1 / 60) # half a second, frame by frame assert app.x == pytest.approx(x + SPEED * 0.5)
def test_draws(tmp_path): app = CoinChase(seed=1) app.screenshot(tmp_path / "frame.png", scale=1) # renders off-screen, no window neededgame.axis, game.vector and game.is_pressed all take held= for this. App.screenshot (or
pz.screenshot(app, path)) renders any moment of the game to a PNG, which is also how the animations on these
pages were recorded. The tests of these games are in
tests/test_game_tutorials.py.