Snippets — patterns ready to adapt
These are paste-ready fragments — the code for a specific task. For the why and the higher-level structure behind them, see Game patterns. The skeleton below is a whole game in miniature; the fragments after it fill in the parts. Copy the relevant piece, adapt the names, and adjust the limits to your game. They use the same conventions as the bundled examples.
The whole shape: a ready-to-run skeleton
Section titled “The whole shape: a ready-to-run skeleton”Start here. All mutable game state lives in one State object (st), and the per-frame
loop lives in a main() function — inside a function its names resolve as fast locals
instead of globals-dict lookups, a measured win on device. Every fragment further down slots
into this shape. Copy it, rename the State fields, and fill in the play branch.
# Ready-to-use picogame skeleton: a TITLE -> PLAY -> GAME-OVER state machine, with ALL game state# in one State object and the per-frame loop in a function (main). Copy it, rename the State fields,# and fill in the PLAY branch. Runs as-is here in the browser and on a device.import boardimport terminalioimport picogame as pgimport picogame_gameimport picogame_inputimport picogame_clockimport picogame_shapes as shapesimport picogame_ui as ui
W, H = board.DISPLAY.width, board.DISPLAY.height # size-independent: never hardcode 320x240BAR = 16 # reserved top HUD strip
scene, bufA, _ = picogame_game.setup(background=pg.rgb565(16, 20, 34), top=BAR)btn = picogame_input.Buttons()clock = picogame_clock.Clock(30)
hud = ui.HudBar(pg, board.DISPLAY, bufA, 0, 0, W, BAR, pg.rgb565(10, 12, 22))label = hud.label(terminalio.FONT, 4, 3, pg.rgb565(235, 240, 255), "")
# The player: a generated shape now; swap in a Bitmap (png2picogame) later.player = pg.Sprite(shapes.circle(14, pg.rgb565(240, 90, 90)), W // 2, H // 2)player.anchor = (0.5, 0.5)scene.add(player)
class State: """ALL mutable game state in ONE object, created once and never reassigned. reset() re-inits it in place, so main() can hold `st` and every helper sees the same state. Add your fields here; keep engine objects (sprites, pools, clock) as module globals, not in State.""" def __init__(self): self.reset()
def reset(self): self.mode = "title" # "title" -> "play" -> "over" self.score = 0
st = State()
def show(text): # set the HUD text and repaint the bar label.set(text) hud.draw()
def new_game(): st.reset() st.mode = "play" player.move(W // 2, H // 2) show("SCORE 0")
def main(): # The per-frame loop lives in a FUNCTION, not at module scope: here its names resolve as fast # array-indexed locals instead of globals-dict lookups -- a measured win on device. show("PRESS A TO START") while True: btn.poll() if st.mode == "play": # --- your game goes here: move, collide, score --- player.x += (btn.is_pressed(btn.RIGHT) - btn.is_pressed(btn.LEFT)) * 3 if btn.just_pressed(btn.A): st.score += 1 show("SCORE %d" % st.score) # set text where it CHANGES, not every frame if btn.just_pressed(btn.B): st.mode = "over" show("GAME OVER - press A") elif btn.just_pressed(btn.A): # title or game-over -> (re)start, no reload new_game() scene.refresh() clock.tick()
main()The Game patterns page explains the shape; the fragments below are the individual moves.
Read the screen size instead of hardcoding it
Section titled “Read the screen size instead of hardcoding it”Derive positions from W and H when the layout should adapt to different supported
screen sizes (for example 320×240 and 240×240).
W, H = board.DISPLAY.width, board.DISPLAY.heightplayer = pg.Sprite(bm, W // 2, H - 40)HUD text: format on change, not per frame
Section titled “HUD text: format on change, not per frame”label.set() skips the redraw on unchanged text, but the "%"-format itself allocates a new
string on every call. Doing that each frame creates avoidable garbage. Compare the integers
first and format only when the displayed value changes:
hud = picogame_font.Label(...) # or a picogame_ui.HudBar; built ONCE at load
if score != shown_score: # shadow int of what the label shows shown_score = score hud.set("SCORE %d" % score)Full HUD/label options: Text & UI.
When possible, set the label where its value changes (in the hit handler or at the level transition) and keep the frame loop free of HUD code. Quantize continuous values to the displayed unit first:
secs = int(time.monotonic() - t0) # whole seconds = what the label showsif secs != shown_secs: shown_secs = secs timer_lbl.set("TIME %d" % secs)Bullets & spawns come from a Pool
Section titled “Bullets & spawns come from a Pool”For frequently created objects such as bullets, avoid constructing a new pg.Sprite(...)
each time. A Pool pre-allocates them once; spawn() and free() toggle visibility:
bullets = Pool(scene, bullet_bm, 8, anchor=(0.5, 0.5)) # ONE allocation, at load
b = bullets.spawn() # None when all 8 fly -> fire rate self-limitsif b: b.move(ship.x + 14, ship.y)
for b in bullets.items: # fixed list -> the loop allocates nothing if not b.visible: continue b.x += 6 if b.x > W + 8: bullets.free(b) # back to the pool for reuseFixed-count HUD icons (lives, ammo pips) don’t need a Pool — a plain list of sprites with
visible toggles is right there.
Instant restart: one reset(), one never-rebound st
Section titled “Instant restart: one reset(), one never-rebound st”Keep all run-scoped values in one State class and re-init them in place with reset().
st is created once and never reassigned — a never-rebound singleton — so main() (and any
helper) can hold it as a fast local with no risk of pointing at a stale copy after a restart.
Engine objects and cross-run values (best score) stay module globals.
class State: def __init__(self): self.reset() # __init__ just calls reset -> one place for defaults def reset(self): self.score = 0 self.lives = 3 self.px = 60.0
st = State() # created ONCE, at load
def new_game(): st.reset() # every field back to its documented default, IN PLACE bullets.free_all() player.move(60, GROUND)Pause / menu over a live scene
Section titled “Pause / menu over a live scene”pg.render paints behind the scene’s back — without invalidate() the next refresh()
leaves stale overlay fragments. overlay() does both; draw once, freeze, resume clean:
if btn.just_pressed(btn.START): paused = not paused if paused: picogame_game.overlay(scene, board.DISPLAY, [pause_sd], bufA, 0, 0, W, H)if paused: clock.tick() # no updates, no refresh -> overlay stays put continuescene.refresh() # first refresh after unpause repaints fullyShuffle: there is no random.shuffle on the device
Section titled “Shuffle: there is no random.shuffle on the device”CircuitPython’s random lacks shuffle()/sample() (the desktop simulator has them — it
runs CPython). Fisher-Yates over randint is the same algorithm:
for i in range(len(deck) - 1, 0, -1): j = random.randint(0, i) deck[i], deck[j] = deck[j], deck[i]Hit flash: 1–3 frames, then clear
Section titled “Hit flash: 1–3 frames, then clear”The cheapest visual punch. Set flash on the impact, count it down in the loop:
enemy.flash = WHITE # at the hitst.flash_t = 3
if st.flash_t > 0: # in the frame loop st.flash_t -= 1 if st.flash_t == 0: enemy.flash = None(On the device the blit effects flash/tint/dither/shadow share one slot — last set
wins; the sim treats them as independent, so it won’t reproduce this. Re-assert a permanent
dither after a flash ends. Fuller toolkit: Effects.)
Beep on an event
Section titled “Beep on an event”Pair the flash with a sound. Build the tone once at load, then fire it on the event. Guard
everything behind if audio: so an audio-less board (or a failed init) just runs silent:
audio = Nonetry: import picogame_audio audio = picogame_audio.Audio() # PWM out + mixer; may fail on audio-less boards boom = picogame_audio.tone(140, 200) # low thud, built ONCE (a RawSample in RAM)except Exception: # tight heap, no audio pin, ... audio = None # -> run silent
if audio: # at the hit / pickup: audio.sfx(boom) # fire-and-forget on a free voicetone() beeps like this play in the browser playground (WebAudio). For where each audio path makes
sound (playground / simulator / device) and a ready-made named-effect kit (hit(), coin(),
boom()), see Audio.
Buttons: one virtual pad, mapped in settings.toml
Section titled “Buttons: one virtual pad, mapped in settings.toml”Code against the engine’s logical buttons (UP DOWN LEFT RIGHT A B, optional X Y L1 L2 R1 R2 START SELECT) — never against pins. The same code.py then runs on any board; only the mapping in
settings.toml changes (no reflash):
btn = picogame_input.Buttons() # reads GPIO + key matrix + USB, all OR'd
btn.poll() # once per frame, before any queryif btn.is_pressed(btn.LEFT): px -= 2if btn.just_pressed(btn.A): jump()if btn.has(btn.X): show_x_hint() # only if this board mapped X# direct GPIO buttons (a Pico you wired yourself)PICOGAME_BUTTONS = "UP=GP2 DOWN=GP3 LEFT=GP4 RIGHT=GP5 A=GP6 B=GP7"
# …or a scanned key matrix — NAME=row,colPICOGAME_MATRIX_ROWS = "GP0 GP1 GP2 GP3"PICOGAME_MATRIX_COLS = "GP4 GP5 GP6 GP7"PICOGAME_MATRIX_MAP = "UP=0,1 DOWN=2,1 LEFT=1,0 RIGHT=1,2 A=3,3 B=3,2"USB gamepad/keyboard (USB-host boards) attach automatically. Full guide + USB remap keys: Input & controls.
Input forgiveness: coyote time + jump buffer
Section titled “Input forgiveness: coyote time + jump buffer”Both are picogame_input.Timer one-liners; together they make platforming feel fair:
coyote = picogame_input.Timer(6) # jump up to 6 frames after leaving a ledgejbuf = picogame_input.Timer(5) # honour a jump pressed 5 frames before landing
coyote.feed(on_ground) # in the frame loopjbuf.feed(btn.just_pressed(btn.A))if jbuf.consume() and coyote.is_active: jump()A panel that repaints only when it changes
Section titled “A panel that repaints only when it changes”A StripDraw with always_dirty=False does not retain a pixel surface for the panel. It
stays idle until you call invalidate():
panel = pg.StripDraw(draw_panel, x, y, w, h, always_dirty=False)scene.add(panel, fixed=True)
lines[1] = "HP %d" % hp # when the content changes:panel.invalidate() # repaints once on the next refreshSub-pixel movement without float drift
Section titled “Sub-pixel movement without float drift”Sprite x/y are ints; for smooth slow motion use the engine’s fixed-point accessors
fx/fy (24.8) instead of keeping your own float position:
ball.fx = ball.fx + 0.4 # fractional-pixel movement without integer jitterball.fy = ball.fy + vy * dt