Building scenes
These four modules load baked scenes, attach properties to tiles, create simple bitmap art in code, and reuse a fixed set of sprites. See /reference/ for the signatures.
picogame_scene
Section titled “picogame_scene”The loader turns a baked SCENE dictionary into a pg.Scene and a set of named handles. Use it for a level produced by the editor or scene_build.py. The same data and loader run on hardware and in the simulator. See /scene-format/ for the input format.
load(pg, scene, display=None, strip_h=None, font=None, bank=None) returns a View. It resolves the display in the same way as picogame_game.setup(), constructs the scene, and adds each tilemap, sprite, group, particle system, and HUD label. On an SPI display it allocates two width * strip_h * 2 render buffers, available as view.bufA and view.bufB. On a framebuffer target both are None. Pass a font such as terminalio.FONT if the data contains a HUD label. See /hardware/ for display backends and /memory/ for buffer costs.
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.
From game.json
Section titled “From game.json”A whole game is one game.json (see scene format); Game opens it the way the
device does, streaming and baking every level at start, and load(name, at) builds one level’s View:
import picogame_scene as pgs, terminaliogame = pgs.Game(pg, "game.json", font=terminalio.FONT) # or Game(pg, "game_bank") after build --mpyview = game.load(game.start)# later, on a goto: drop the old view, gc.collect(), thenview = game.load("cave", "entry")Zone stories and per-level effects from the file run through picogame_story.Story on top of the
Director — no code is generated from the JSON.
Tile properties come with the scene. Do not reach for picogame_tiles after loading a scene — the View already answers per-tile questions: view.is_solid(tx, ty), and view.tile_has(tx, ty, "name") for any other flag. The name is whatever the editor painted, so it is not limited to the four the editor offers by default: add a glass flag in the editor and the game reads it as view.tile_has(tx, ty, "glass"). Falling back to a bitfield here means re-deriving data the loader already holds.
view.camera is data, not behaviour. The loader hands you (mode, target, axis, x, y, w, h) and the game applies it — nothing follows the player on its own. axis is "x", "y" or "xy"; honour it when you call scene.set_view(), or a level authored to scroll vertically silently will not.
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.set_tile_prop(tile, prop, on=True)- flip a flag for a tile TYPE at runtime: every cell holding that tile changes meaning at once (a lever makes all gate tiles walkable, ice melts). One cell instead: swap its tile with the nativetilemap.set_tile. Changes last until the nextload()- loading a level resets its tile meanings, even with a shared bank.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()picogame_tiles
Section titled “picogame_tiles”TileFlags stores one metadata bitfield for each tile index, so every cell using that tile shares the same properties. Use it with a hand-built pg.Tilemap. A scene loaded through picogame_scene already exposes the higher-level view.is_solid() and view.tile_has() methods.
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()picogame_shapes
Section titled “picogame_shapes”These functions build single-colour PAL8 Bitmap objects in code. They suit prototypes and geometric art such as balls, bricks, or ships. Unlike Canvas, they return a reusable bitmap for a Sprite or Tilemap. Palette index 0 is transparent and index 1 contains the requested 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 also rotates at runtime (Sprite.angle); baked frames trade a little RAM for a cheaper per-frame blit and pixel-stable art, so pick them for many always-rotating objects (asteroids), andanglefor one-off or continuous rotation. 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)picogame_pool
Section titled “picogame_pool”Pool pre-allocates a fixed number of sprites for short-lived objects such as bullets, enemies, or pickups. the pool tracks which slots are taken (a spawned sprite is made visible) and sprite.data can hold entity state. Spawning and freeing slots allocate no new sprites. See /memory/ for why stable allocation matters on the 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. The slot comes back in its baseline look - blit effect (flash/tint/dither/shadow),scale/angle,frameand flips restored to how the sprites looked at the firstspawn()- so a hit-flash or a death scale-up never leaks into the next life. Set-up right after construction (for e in enemies.items: e.flip_y = True) is therefore kept; only.dataand the position are left to you.pool.baseline()- re-snapshot the baseline after reconfiguring the sprites later in the game (bigger rocks on level 3).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 slots, O(1). A slot stays live even if you hide its sprite yourself, so this is not a count of what’s on screen; 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)