Animation & sequencing
These three modules move animation and timed logic off hand-rolled frame counters. picogame_anim drives a sprite’s frame on a time basis, picogame_seq lets you write timelines as generators, and picogame_cutscene paints a full-screen image with almost no RAM. They are pure-Python helpers over the engine - see /reference/ for the bare signatures.
picogame_anim
Section titled “picogame_anim”What it is: a tiny frame-animation driver. You give it a list of frame indices and an fps, call tick(dt) once per frame with the real elapsed seconds, and it sets sprite.frame for you. Reach for it whenever you would otherwise write sprite.frame = (f // 4) % n - it makes the animation frame-rate independent (driven by real dt, not loop count) and keeps the math out of your update loop.
There are two classes:
FrameAnim(sprite, frames, *, fps=8, loop=True)- plays one sequence.framesis a list/tuple of frame indices (referenced, not copied - treat it read-only).fpsis animation speed;loopkeyword-only. On construction it sets the sprite toframes[0]..tick(dt)- advance bydtreal seconds. Accumulates time, steps the frame when enough has passed. No-op once a non-looping anim is finished or ifframesis empty. No return value..configure(frames, fps=8, loop=True)- re-point this same instance at a new sequence and reset it. Returnsself. Lets you reuse oneFrameAniminstead of allocating a new one on every switch..reset()- back to frame 0, clears thedoneflag and time accumulator.- Attributes you can read:
.done(True when a non-looping anim has reached its last frame),.i(current index intoframes),.frames,.fps,.loop.
AnimatedSprite(sprite, anims)- a sprite with named states.animsis a dict{name: (frames, fps, loop)}. Internally holds one reusableFrameAnim(no per-switch allocation)..play(name)- switch to that named animation. Looks upanims[name]and reconfigures. Callingplaywith the name already playing is a no-op, so it is safe to call every frame..tick(dt)- advance the current animation.
import picogame_anim
hero = picogame_anim.AnimatedSprite(self.spr, { "run": (DINO_RUN, 12, True), "jump": ((DINO_JUMP,), 1, False),})hero.play("run")# each frame, with dt = real seconds since last frame:hero.play("jump" if self.jumping else "run") # cheap to call every framehero.tick(dt)For a single sequence (a spinning coin, a walk cycle) skip the dict and use FrameAnim directly: spin = picogame_anim.FrameAnim(sprite, list(range(COIN_FRAMES)), fps=15).
Gotchas: you must pass real dt (seconds per frame, e.g. from picogame_clock), not a frame count - passing 1 makes it run at fps frames per call. The frames list is held by reference, so do not mutate it while it is in use. .done only ever becomes True for loop=False.
picogame_seq
Section titled “picogame_seq”What it is: a way to write timed and staged logic as generators (a coroutine pattern). Each yield means “resume me next frame”; a Seq advances one generator one step per tick(). Reach for it for cutscenes, intros, staged AI, and any “do X over N frames” timeline that would otherwise become a tangle of frame counters and if-ladders. Compose sub-sequences with yield from.
Generator helpers (call them with yield from):
wait(frames)- pause forframesframes (yields that many times, doing nothing).over(frames, fn)- generic tween: callsfn(t)each frame withtramping0..1(specificallyi/framesforiin1..frames) overframesframes.move_over(sprite, x, y, frames)- glide a sprite from its current position to(x, y)overframesframes, linearly, viasprite.move(...).
The driver:
Seq(gen=None)- wraps one generator. IfgenisNoneit starts already.done..start(gen)- point it at a (new) generator and cleardone. Returnsself, so it is reusable..tick()- advance to the nextyield. CatchesStopIterationand sets.done. Returns thedoneflag (True once finished), so you can branch on it..done- True when the sequence has finished.
import picogame_seq as seq
def intro(hero, label): yield from seq.wait(30) label.set("GO!") yield from seq.move_over(hero, 120, hero.y, 20) # glide over 20 frames
s = seq.Seq(intro(player, hud))# each frame:if not s.tick(): # advances one step; True once the intro is over ... # still runningGotchas: tick() advances exactly one step (to the next yield) per call, so drive it once per game frame. Anything between yields runs in a single frame - keep heavy work behind a yield. A Seq runs one generator; to run several timelines at once, give each its own Seq, or merge them with yield from inside one generator.
picogame_cutscene
Section titled “picogame_cutscene”What it is: a full-screen image player that uses almost no RAM. A 320x240 frame is 150 KB in RGB565 (75 KB in PAL8), which does not fit the ~138 KB heap - see /memory/. Instead the image lives on flash as a raw, row-major file, and the module streams it one ~24-row band at a time through a small reused buffer, blitting each band straight to the LCD. Only one band is ever in RAM; once drawn, the LCD holds the picture, so a static cutscene costs nothing afterwards. Reach for it for title screens, story panels, maps, and chapter cards.
Bake the raw file first with tools/bake_image.py (PNG to wire-order RGB565 rows, or PAL8 plus a palette). See /scene-format/ for engine bitmap formats and /hardware/ for the display and buttons.
show(pg, display, buffer, path, w=320, h=240, band=24, palette=None)- render the image atpathtodisplay, streaming band by band.bufferis your strip buffer. Pass apaletteto read a 1-byte-per-pixel PAL8 file; omit it for 2-byte RGB565. Returns nothing.play(pg, display, buffer, btn, path, w=320, h=240, band=24, palette=None)- callsshow(), then blocks (pollingbtn) untilAorBis pressed, then returns. Use this for “press to continue” screens; useshow()when you want to keep running your own loop.
import picogame_cutscene as cutimport board
cut.play(pg, board.DISPLAY, bufA, btn, "intro.dat") # RGB565cut.play(pg, board.DISPLAY, bufA, btn, "map.dat", palette=PAL) # PAL8 + paletteGotchas: band MUST divide h evenly (240 % 24 == 0) or you will miss rows. buffer must be at least w * band * 2 bytes (RGB565) - or w * band for PAL8. The file must be raw, row-major, in wire order from bake_image.py; a normal PNG will not work. play() blocks your game loop entirely while waiting for the button.