Boot & game loop
These three modules are the spine of every PicoPad game: picogame_game takes over the display and hands you a Scene, picogame_clock paces the frame and gives you dt, and picogame_input turns the board’s buttons into a bitmask with edge detection. A typical code.py calls picogame_game.setup() once, makes a Buttons() and a Clock(), then loops: poll, update, refresh, tick. For the bare signatures see /reference/.
picogame_game
Section titled “picogame_game”The boot scaffold. Call it once at the top of your game to disable displayio auto-refresh, clear the root group, allocate the two strip buffers, and build a Scene for you - so you skip the rendering boilerplate. Reach for it whenever you want a game-style takeover of the screen rather than a normal displayio UI.
setup(display=None, strip_h=24, background=0, fast=True, top=0, bottom=0, left=0, right=0, rgb444=False)- takes over the display and returns the tuple(scene, buffer_a, buffer_b). It setsdisplay.auto_refresh = False, clearsroot_group, allocates two full-width xstrip_hstrip buffers (eachwidth * strip_h * 2bytes), and builds theScene. Keep all three returned values alive for the whole game - the buffers must not be garbage-collected.display- the displayio display; defaults toboard.DISPLAY.strip_h- height in pixels of each render strip. Smaller uses less RAM, larger can mean fewer SPI transactions. See /memory/ for the trade-off.background- fill colour behind the scene, an RGB565 int. Build one withpg.rgb565(r, g, b).top/bottom/left/right- reserve a border (px) the scene won’t render into, so it paints only the inner play rect. You draw that border yourself (a HUD bar, side panels) once, and it is never recomputed per frame.fast-Trueuses the platform fastpg.Display(async DMA where available);Falsedrives the plain busdisplay via the portable renderer (correct everywhere, slower). On a port without the DMA backend,setupfalls back automatically.rgb444-Truesends 12-bit RGB444 instead of 16-bit RGB565 (~25% less SPI traffic, 4096 colours), fastDisplayonly and needs a 12-bit-capable controller (ST7789/ST7735, not ILI9341).rgb444="auto"enables it only where the board reportspg.RGB444_SUPPORTED, so one codebase runs optimally on ST7789 and safely on ILI9341.
import picogame as pgimport picogame_game
BG = pg.rgb565(20, 24, 30)scene, bufA, bufB = picogame_game.setup(background=BG, strip_h=16, top=12)# scene is a pg.Scene; add sprites and refresh it each framescene.add(sprite)scene.refresh()Gotchas: hold onto scene, bufA and bufB for the life of the game - if the buffers are collected, rendering breaks. The border you reserve with top/bottom/left/right is yours to paint; setup only stops the scene drawing there. See /scene-format/ for what Scene expects and /hardware/ for per-board display notes.
picogame_clock
Section titled “picogame_clock”Frame timing. Clock caps your loop to a target FPS and returns the real dt (seconds elapsed), so movement can be frame-rate independent and a quick D-pad tap doesn’t fling a sprite across the screen at high FPS. FixedStep is the alternative when you want deterministic logic: it runs your update in equal-sized steps regardless of render time. Use Clock for most games; reach for FixedStep when physics or collision must be reproducible.
Clock:
Clock(fps=30, max_dt=0.1)- cap the loop tofps(use0for uncapped) and clamp the returneddtto at mostmax_dtseconds, so a pause or stall can’t produce a giantdtthat teleports everything.tick()- sleeps until the frame boundary, then returns the realdtin seconds since the lasttick(). Anchors to the ideal schedule so a small oversleep can’t accumulate into drift; if you ran over budget it anchors to real time instead, keepingdtaccurate. Call once per frame.tick_async()- awaitable variant that yields to otherasynciotasks during the idle wait instead of blocking. Needs theasynciolibrary available (raisesRuntimeErrorotherwise). Note rendering itself is blocking, so async only helps in the cap-sleep gap.set_fps(fps)- change the target FPS on the fly (e.g. menus at 30, action at 60).0uncaps.
FixedStep:
FixedStep(step_fps=60, max_steps=5)- fixed timestep of1/step_fpsseconds, running at mostmax_stepslogic steps per frame (the cap avoids a “spiral of death” when rendering can’t keep up - backlog is dropped).step_count()- returns how many fixed steps to run this frame (0..max_steps). Loopfor _ in range(step_count())and use the constantself.dt; this form allocates nothing, good for hot loops.dt- the constant step duration in seconds. Pass it to your update.steps()- generator form yieldingself.dtper step. Convenient, but allocates a generator each call; preferstep_count()in the main loop.
import picogame_clock
clock = picogame_clock.Clock(30) # cap to 30 FPSwhile True: dt = clock.tick() # sleeps to the frame boundary, returns real dt player.x += player.vx * dt # frame-rate independent movement scene.refresh()Gotchas: call tick() exactly once per frame, at the bottom of the loop. If you ignore the return value you still get the FPS cap, but lose frame-rate independence - fine for a fixed-feel grid game, not for smooth motion. tick_async() only buys you anything if rendering can overlap other tasks; the render call still blocks.
picogame_input
Section titled “picogame_input”Buttons. Buttons reads the board’s physical buttons into a logical bitmask with edge detection (just-pressed / just-released) and auto-repeat, picking a backend per board automatically: the CircuitPython keypad module (hardware debounce, background scan, event queue, so quick taps are never missed) where present, else digitalio polling - same API either way. Timer is a small decaying frame counter for input-leniency tricks (coyote time, jump buffering). Reach for Buttons in every game; add Timer when an action game needs to feel forgiving.
Logical buttons are exposed both as module constants and as attributes on the instance: UP, DOWN, LEFT, RIGHT, A, B, X, Y, L1, L2, R1, R2, START, SELECT, plus ALL. The PicoPad maps the eight face buttons; absent buttons (no shoulders) simply never fire. They are bit flags, so you can OR them: btn.A | btn.B.
Buttons:
Buttons(profile=None, pull=None, prefer_keypad=True, debounce_s=0.02)- build the reader. Withprofile=Nonethe pin map is resolved highest-wins: an explicitprofile, thensettings.tomlPICOGAME_BUTTONS = "UP=GP2 A=GP12 ..."(remap a custom Pico with no reflash), then a built-in profile byboard.board_id, then thePICOPADfallback.pulldefaults toPull.UP(orPICOGAME_PULLinsettings.toml).debounce_sis the keypad scan window;prefer_keypad=Falseforces polling.poll()- sample all buttons once and return the current pressed bitmask. Call once per frame, before any query. Drains the keypad event queue (catching sub-frame taps) or reads the pins directly, and updates held-frame counts forrepeat().is_pressed(mask=ALL)-Trueif any button inmaskis currently down (level).just_pressed(mask=ALL)-Trueon the rising edge (the frame a button went down). On the keypad backend this comes from the event queue, so a tap shorter than a frame still registers.just_released(mask=ALL)-Trueon the falling edge (the frame a button came up).has(mask=ALL)-Trueif this board physically wires the given button(s). Use it to adapt controls/UI to boards without shoulders or START/SELECT.repeat(button, delay=15, interval=4)- auto-repeat for a SINGLE button:Truethe frame it’s pressed, then everyintervalframes once helddelayframes. Ideal for menu and grid movement.clear()- reset state and flush pending input. Call on scene or menu transitions so a held button doesn’t leak across.
Timer:
Timer(frames)- a counter that decays one frame at a time overframesframes.feed(condition)- recharge to full whenconditionis true, else count down one frame; returns whether still active. Use for coyote time (feed(on_ground)).charge()- force the timer to full.is_active(property) -Truewhile the counter is above zero.consume()-Trueonce if active, then clears it, so a buffered press fires exactly once (jump buffering).
import picogame_input
btn = picogame_input.Buttons() # auto profile by boardwhile True: btn.poll() dx = btn.is_pressed(btn.RIGHT) - btn.is_pressed(btn.LEFT) # -1, 0 or +1 if btn.just_pressed(btn.A): # rising edge: fire once per tap jump() scene.refresh()Coyote time and jump buffering, straight from the platformer example:
coyote = picogame_input.Timer(5) # still jump a few frames after a ledgejbuf = picogame_input.Timer(6) # honour a jump pressed just before landing# each frame:coyote.feed(on_ground)jbuf.feed(btn.just_pressed(btn.A))if coyote.is_active and jbuf.consume(): jump()Gotchas: always poll() once per frame before querying, or is_pressed / just_pressed read stale state. repeat() takes a single button, not an OR-mask. The digitalio polling backend is undebounced (per-frame sampling already filters sub-frame bounce); for a noisy switch on a keypad-less build, debounce upstream. See /hardware/ for board profiles and the settings.toml remap keys.