Animation & sequencing
These three Python helpers cover sprite animation, timed sequences, and full-screen still images. picogame_anim advances animation from elapsed time, picogame_seq expresses timelines as generators, and picogame_cutscene streams an image without keeping the full frame in the Python heap. See /reference/ for their signatures.
picogame_anim
Section titled “picogame_anim”Give FrameAnim a sequence and an fps, then call tick(dt) once per game frame with the elapsed time in seconds. Sequence entries can be frame indices for a sprite sheet or separate Bitmap objects. The animation follows elapsed time instead of loop count.
There are two classes:
FrameAnim(sprite, frames, *, fps=8, loop=True)- plays one sequence.framesis a list or tuple of frame indices, or ofBitmapobjects. The sequence is referenced rather than copied, so treat it as read-only.fpssets the animation speed andloopis keyword-only. Construction displaysframes[0]when the sequence is not empty..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 dictionary{name: (frames, fps, loop)}. It keeps one reusableFrameAnim, so switching an animation does not allocate another driver..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 one sequence, such as a spinning coin, use FrameAnim directly: spin = picogame_anim.FrameAnim(sprite, list(range(COIN_FRAMES)), fps=15).
picogame_seq
Section titled “picogame_seq”picogame_seq expresses timed logic as generators. Each yield pauses until the next game frame, and each tick() advances one generator to its next yield. Use it for intros, staged AI, and other ordered actions. Compose smaller 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 runningpicogame_cutscene
Section titled “picogame_cutscene”picogame_cutscene displays a full-screen still without loading the complete image into the Python heap. A 320x240 source occupies 153,600 bytes in RGB565 or 76,800 bytes in PAL8. The module instead reads a raw, row-major file from flash one band at a time and renders each band to the display backend.
The temporary source band costs w * band bytes for PAL8 or w * band * 2 bytes for RGB565. With the default w=320 and band=24, that is 7,680 or 15,360 bytes in addition to the render buffer passed to show(). Reduce band if the allocation does not fit. Once rendered, the still image needs no additional Python object containing the full frame. See /memory/ for the other memory costs.
Bake the raw file first with tools/bake_cutscene.py (PNG to PAL8 + a palette module, or wire-order RGB565 rows). See /scene-format/ for engine bitmap formats and /hardware/ for the display and buttons.
palette(pg, rgb)- build the device palette (array('H')of wire colours) from abake_cutscene.pypalette module, a list of(r, g, b)triplets, or wire ints. Build it ONCE at setup and reuse it -show()would otherwise rebuild it per call.show(pg, display, buffer, path, pal=None, w=320, h=240, scale=None, band=24, bg=0)- stream the image atpathone band at a time and render each band with the supplied engine stripbuffer.palselects PAL8 input (1 B/px);Noneselects RGB565 (2 B/px).scale=Nonederives an integer scale from the display (width // w) and rejects a source size that does not fill both axes. A short final band is cleared withbg. Returns the scale used.play(pg, display, buffer, btn, path, pal=None, w=320, h=240, scale=None, band=24, caption=None, caption_lines=None, auto_hold=0, clock=None, bg=0)- show the image, add optional caption lines in a dark bar, then block until A or B is pressed. Withauto_hold > 0, it advances after that many loop ticks andbtnmay beNone. Pass apicogame_clockinstance asclockto pace the wait loop.
import picogame_cutscene as cutimport board
PAL = cut.palette(pg, intro_pal) # once, at setupcut.play(pg, board.DISPLAY, bufA, btn, "intro.raw", pal=PAL, caption="Chapter 1", clock=clock)scene.invalidate() # the image clobbered the LCD