Boot & game loop
These three modules set up the display, pace the game loop, and read buttons. A typical code.py calls picogame_game.setup() once, creates Buttons() and Clock(), then repeats four steps: poll input, update state, refresh the scene, and tick the clock. See /reference/ for the signatures.
picogame_game
Section titled “picogame_game”Call setup() once before creating the game objects. It resolves the display backend, stops displayio from refreshing independently where applicable, and returns a new Scene with the memory it needs.
setup(display=None, strip_h=None, background=0, fast=True, top=0, bottom=0, left=0, right=0, rgb444=False)- returns(scene, buffer_a, buffer_b). On an SPI display it disables automatic refresh, clearsroot_group, and allocates two full-width render strips. On a framebuffer target such as Fruit Jam DVI or the browser playground, the scene composites into the framebuffer and both returned buffers areNone. TheScenekeeps any allocated strip buffers alive. A game that only callsscene.refresh()can ignore the returned buffers:scene, _, _ = picogame_game.setup(...).display- explicit display object. If omitted, setup triesboard.DISPLAYand thensupervisor.runtime.display.strip_h- height of each render strip on the SPI path. It defaults to the board’s compiledpicogame.STRIP_Hvalue. The two buffers occupy2 * width * strip_h * 2bytes: about 10 KiB at 320×8 or 30 KiB at 320×24. On the measured RP2040 DMA path, a smaller strip also improved overlap between rendering and transfer; without DMA, larger strips reduce the number of blocking transfers. Override the value per call or at firmware build time with-DPICOGAME_STRIP_H=N. Framebuffer targets ignore it. 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- on an SPI display,Trueselectspg.Displaywhen available;Falseuses the portable busdisplay renderer. Setup falls back automatically when the fast backend is absent.rgb444-Truerequests 12-bit colour on a compatible fast SPI backend. Use"auto"to enable it only when the board reports support. Framebuffer targets ignore this setting. See /hardware/.
import picogame as pgimport picogame_game
BG = pg.rgb565(20, 24, 30)scene, buffer_a, buffer_b = picogame_game.setup(background=BG, strip_h=16, top=12)# scene is a pg.Scene; add sprites and refresh it each frame# a simple game that never draws in immediate mode can skip the buffers: scene, _, _ = ...scene.add(sprite)scene.refresh()picogame_clock
Section titled “picogame_clock”Clock caps the loop to a target FPS and returns elapsed time as dt, so movement can be independent of frame rate. FixedStep runs game logic in equal time steps 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()picogame_input
Section titled “picogame_input”Buttons maps physical buttons to a logical bitmask with pressed and released edges plus auto-repeat. It uses CircuitPython’s background-scanned keypad event queue when available and falls back to digitalio polling. The Timer class provides frame-based windows for coyote time and jump buffering.
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, matrix=None, usb=None, sources=None)- 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.matrix=adds a scanned key matrix andusb=adds USB HID sources — see below.- More than one input source:
ButtonsORs several sources into one mask — on-board GPIO buttons, a scanned key matrix (matrix=, or thePICOGAME_MATRIX_*keys), and USB gamepads/keyboards on USB-host boards (usb=, auto-attached). A game reads them all with no code change. Full guide: Input & controls.
- More than one input source:
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()