Building scenes
These four modules cover the static “world” side of a game: loading a baked scene, asking what a tile means, making art with no PNG assets, and recycling a fixed set of sprites. For bare signatures see /reference/; the snippets below explain behaviour and the traps.
picogame_scene
Section titled “picogame_scene”A one-time loader that turns a baked SCENE dict (see /scene-format/) into a live pg.Scene plus named handles. Reach for it when your level is built by the editor/scene_build.py rather than hand-coded, and you want the same loader to run on device and in the simulator. Loading is not a hot path, so it lives in Python.
load(pg, scene, display=None, strip_h=24, font=None, bank=None) builds and returns a View. It sets display.auto_refresh = False and clears root_group for you, allocates two strip buffers (bufA/bufB, sized width * strip_h * 2), constructs the pg.Scene, then adds every layer (tilemap, sprite, group, particles, hudlabel). display defaults to board.DISPLAY. Pass font (e.g. terminalio.FONT) if any layer is a HUD label. See /hardware/ for display details and /memory/ on the strip-buffer cost.
load_bank(pg, bank) builds shared bitmaps/sounds/anims ONCE; pass the result as load(..., bank=...) for each level so unchanged art is not rebuilt per level.
The returned View is your handle to everything:
view.scene- the livepg.Scene. Callview.scene.refresh()each frame andview.scene.set_view(ox, oy)to scroll.view.named[name]- dict of name -> sprite / particles / HUD label for any layer given aname.view.group(tag)- list of sprites for a group layer (returns[]if the tag is unknown, so it is safe to iterate).view.tick(dt)- advance all auto-animated sprites; call once per frame withdtin seconds.view.tile_xy(px, py)- world pixel ->(tx, ty)cell of the primary (first) tilemap.view.is_solid(tx, ty)- shorthand fortile_has(tx, ty, "solid").view.tile_has(tx, ty, prop)- True if the primary tilemap’s tile at that cell has the named property (from the bakedtileprops).view.point(name)-(x, y)for a named point, or None.view.in_zone(x, y, tag=None)- first zone(tag, x, y, w, h)containing the point (optionally filtered by tag), else None.view.play(sound_id)- play a baked sfx by id (no-op if audio/sample is missing).view.tilemap/view.camera/view.zones/view.points/view.anims- the primary tilemap object, the camera tuple, and the raw collections.
import board, terminalioimport picogame as pgimport picogame_scene as pgsimport world1_scene
view = pgs.load(pg, world1_scene.SCENE, font=terminalio.FONT)player = view.named["player"]enemies = view.group("enemies")while True: view.tick(1 / 30) # advance auto-animations tx, ty = view.tile_xy(player.x, player.y) if not view.is_solid(tx, ty): player.move(player.x + 2, player.y) view.scene.refresh()Gotchas: the FIRST tilemap layer is the “primary” one - tile_xy, is_solid, and tile_has only query that map. view.named only holds layers that were given a name; an un-named layer is added to the scene but unreachable. Sounds load best-effort: on the simulator (no wavs) view.sounds entries are None and play() silently does nothing.
picogame_tiles
Section titled “picogame_tiles”A per-tile metadata bitfield keyed by tile INDEX. The flags belong to the tile graphic, so every cell drawn with that tile shares them. Reach for it when you hand-build a pg.Tilemap (not via picogame_scene) and want collision/coin/exit logic as one-liners instead of a side table. If you load scenes with picogame_scene, use view.is_solid / view.tile_has instead - this module is the lower-level alternative.
Eight named bits and their masks: B_SOLID, B_HAZARD, B_LADDER, B_PLATFORM, B_WATER, B_COIN, B_EXIT, B_CUSTOM are bit indices 0..7; SOLID, HAZARD, LADDER, PLATFORM, WATER, COIN, EXIT, CUSTOM are the matching 1 << bit masks. Use the masks when building the table, the B_* indices when querying.
TileFlags(flags=None, tile_px=8) builds the table. flags is either a {tile_index: bitfield} dict or a list/bytes indexed by tile index; tile_px is the tile size used by the pixel helper.
tf.get(tile, bit=None)- the full bitfield of a tile, or one bool flag ifbit(aB_*index) is given.tf.set(tile, bit, value=True)- flag (or clear) a bit on a tile at runtime.tf.at(tilemap, tx, ty, bit)- flagbitof the tile at cell(tx, ty).tf.at_px(tilemap, px, py, bit)- flagbitof the tile under MAP-LOCAL pixel(px, py); the common collision probe.
import picogame as pgimport picogame_tiles as tiles
TILE = 8tf = tiles.TileFlags({1: tiles.SOLID, 2: tiles.COIN, 3: tiles.EXIT}, tile_px=TILE)
def blocked(level, tx, ty): # level is a pg.Tilemap return tf.at(level, tx, ty, tiles.B_SOLID)
if tf.at_px(level, px, py, tiles.B_SOLID): # is the tile under pixel (px, py) solid? stop()Gotchas: at_px assumes the map is at screen (0, 0) - if it is moved, subtract the map origin from px/py yourself before calling. Tiles missing from the table read as 0 (no flags). Build with the MASK constants (tiles.SOLID) but query with the BIT indices (tiles.B_SOLID); mixing them up silently checks the wrong bit.
picogame_shapes
Section titled “picogame_shapes”Generators that BAKE single-colour PAL8 pg.Bitmaps in code - placeholder sprites and tilesets with no PNG assets and no firmware changes. Reach for it for prototypes, geometric art (balls, bricks, ships), or anything you would otherwise hand-roll a packing loop for. Note: this is NOT the C Canvas (which draws into a live surface) - shapes returns a reusable Bitmap you hand to a Sprite or Tilemap. Index 0 is transparent, 1 is the colour.
rect(w, h, color)- a filledw x hrectangle.circle(d, color)- a filled disc of diameterd.ring(d, color, thickness=2)- a circle outline of diameterd.from_mask(mask, color)- a bitmap from a list of strings;#,X, or1sets a pixel. Sized to the mask.atlas(frames_data, w, h, color)- pack a list ofw*h0/1 buffers into one horizontal multi-frame bitmap (one colour). The general “frame sheet” builder.color_frames(w, h, colors)- a multi-frame bitmap where frameiis a solid fill ofcolors[i]; frame 0 is already a colour. Index 0 transparent.tileset_colors(w, h, colors)- a tileset where frame 0 is EMPTY (transparent) and frameiis a solid fill ofcolors[i-1]. So a Tilemap reads tile value 0 as empty and 1..N as coloured tiles.poly_frames(size, points, nframes, color, fill=True)- bakenframesrotations of a polygon (points around centre, +y down) into asize x sizeatlas. The engine has no runtime rotation, so this is the pre-rotated-frames pattern for ships/asteroids. Setfill=Falsefor an outline.
import picogame as pgimport picogame_shapes as shp
ball = shp.circle(4, pg.rgb565(255, 255, 120))bricks = shp.tileset_colors(16, 8, [pg.rgb565(220, 70, 70), pg.rgb565(80, 140, 240)]) # value 0 empty, 1..2 colouredship = shp.poly_frames(16, [(0, -8), (6, 7), (0, 4), (-6, 7)], 16, pg.rgb565(200, 220, 255)) # 16 pre-rotated framessprite = pg.Sprite(ball, 100, 60)Gotchas: color_frames frame 0 is a visible colour, but tileset_colors frame 0 is transparent (empty) - pick the one matching how your Tilemap treats value 0. circle fills its bitmap edge-to-edge, so it looks flat-topped when scaled up. poly_frames with nframes=1 bakes a single un-rotated frame (use it for a fixed-direction shape).
picogame_pool
Section titled “picogame_pool”A reusable fixed-size sprite pool - the pre-allocate / spawn / free / iterate pattern every spawner game (bullets, enemies, orbs) hand-rolled. It uses sprite.visible AS the alive flag (no parallel data["on"]) and sprite.data for per-entity state. Reach for it whenever you have a bounded number of transient sprites and want zero per-frame allocation. See /memory/ on why pre-allocation matters on device.
Pool(scene, bitmap, capacity, anchor=None, fixed=False) pre-allocates capacity hidden sprites sharing bitmap, sets each anchor (if given) and data = None, and adds them all to scene (fixed= passes through to scene.add).
pool.items- the underlying list of sprites; iterate it directly for zero-alloc updates.pool.spawn()- make the first free (hidden) sprite visible and return it, or None if the pool is full.pool.free(s)- hide sprites(return it to the pool).pool.free_all()- hide every sprite (use on level reset).pool.count()- count of live (visible) sprites; cheap, but iterateitemsfor the sprites themselves.
import picogame as pgimport picogame_pool
bullets = picogame_pool.Pool(scene, bullet_bm, 6, anchor=(0.5, 0.5))
b = bullets.spawn() # a now-visible sprite, or None if fullif b: b.data = {"vx": 6} b.move(x, y)
for s in bullets.items: # zero-alloc iteration if not s.visible: continue s.move(s.x + s.data["vx"], s.y) if off_screen(s): bullets.free(s)Gotchas: always skip hidden slots when iterating (if not s.visible: continue) - items holds every slot, alive or not. spawn() reuses the most-recently-freed slot, so if you free(s) and spawn() in the same step, read any state off s.data BEFORE freeing - the new spawn will overwrite it. A full pool returns None from spawn(); check for it. All sprites share one bitmap, so per-entity frame/animation must be set on each sprite after spawn().