deklarativní formát scény/mapy picogame — návrh designu
Cíl: popsat scénu/úroveň/mapu jako data, aby jeden zdroj pravdy napájel hru na zařízení, desktopový simulátor i budoucí editor — a aby zdlouhavé, editovatelné části (zapojení assetů, umisťování sprite, malování map, sémantika tile, pořadí vrstev/z-order, HUD, camera) přestaly být ručně psaný Python.
Rozhodnutí (a proč)
Sekce “Rozhodnutí (a proč)”1. Dvě úrovně: autorské JSON → pečený runtime modul
Sekce “1. Dvě úrovně: autorské JSON → pečený runtime modul”Jeden formát nemůže být zároveň editor-friendly A levný pro zařízení. Takže:
- Autorství = JSON (
*.scene.json): neutrální, diff-ovatelné, round-trippovatelné editorem nebo člověkem. Barvy jako[r,g,b], mapy jako 2-D mřížky — čitelné. - Runtime = pečený Python modul (
level1_scene.py→.mpy): kompaktní a levný na import. Mřížka tilemapy se stane literálembytes(1 bajt/tile, JEDNA alokace), barvy jsou předem převedené na wire RGB565, PNG jsou předem převedené na PAL8/RGB565 atlasy. Znovu používá existující.mpypipeline (tools/build_mpy.sh).
Proč ne JSON na zařízení? json.load tilemapy jako int pole alokuje
Python list N boxovaných intů (~28 B každý) — mapa 28×18 = ~14 KB jen na list,
plus JSON text. bytes mřížka v .mpy je ~500 B, jedna alokace. Dvouúrovňové
rozdělení je pro RAM na RP2040 nesmlouvavé.
tools/scene_build.py dělá JSON → runtime modul, vyvolává existující asset
konvertory (png2picogame, tinyjoypad_extract, cavern_pack) na referencovanou grafiku.
(Pro velmi velké mapy může baker místo toho emitovat binární blob načítaný přes readinto
— stejný loader, jiné úložiště — ale .mpy modul je výchozí.)
2. Formát popisuje DATA, ne CHOVÁNÍ
Sekce “2. Formát popisuje DATA, ne CHOVÁNÍ”Nese: assety, umístění sprite/entit, sémantiku tile, pořadí vrstev + fixed/HUD příznak, nastavení camera. NENESE herní logiku (pohyb, AI, podmínky výhry) — ta zůstává v prostém Pythonu. To odpovídá zjištění o ergonomii: gameplay-v-Pythonu je pohodlný; boilerplate je nastavení scény/assetů/mapy.
3. Vlastnosti tile jsou prvotřídní (velká editorová výhra)
Sekce “3. Vlastnosti tile jsou prvotřídní (velká editorová výhra)”Každý tileset připojuje sémantické příznaky k hodnotám tile (solid, coin, hazard,
spawn, trigger…). Editor maluje tile A značí význam; loader staví
rychlé vyhledávací tabulky, takže se hra ptá level.is_solid(tx, ty) místo natvrdo
tile == 1. Tohle zobecňuje kolizní logiku pacman / digdug / mario / cavern.
4. Jeden sdílený loader (zařízení == simulátor)
Sekce “4. Jeden sdílený loader (zařízení == simulátor)”picogame_scene.load(...) staví Scene z runtime dat pomocí pouze
veřejného picogame API, takže STEJNÝ loader běží na hardwaru i v simulátoru —
formát je oběma automaticky validován.
Autorské schéma (JSON)
Sekce “Autorské schéma (JSON)”{ "format": "picogame-scene", "version": 1, "size": [320, 240], "background": [8, 10, 24], // -> wire rgb565 při pečení
"assets": { // znovupoužitelné, referencované přes id "hero": { "type": "bitmap", "src": "hero.png", "format": "pal8", "frames": 6, "transparent": 0 }, "tiles": { "type": "tileset", "src": "tiles.png", "tile": [16, 16], "frames": 5, "props": { "1": {"solid": true}, "2": {"coin": true}, "3": {"goal": true} } }, "goomba":{ "type": "bitmap", "src": "goomba.png", "frames": 2 } },
"layers": [ // seřazeno zdola -> nahoru { "kind": "tilemap", "asset": "tiles", "cols": 80, "rows": 15, "pos": [0, 0], "map": "rle:..." }, // 2-D mřížka; RLE/base64 při autorství { "kind": "sprite", "asset": "hero", "name": "player", "pos": [40, 208], "anchor": [0.5, 1.0], "frame": 0, "data": { "lives": 3 } }, { "kind": "group", "asset": "goomba", "anchor": [0.5, 1.0], "instances": [[224, 208], [480, 208], [704, 208]], "tag": "enemies" }, { "kind": "particles", "capacity": 64, "size": 2, "gravity": 0.5, "fade": true, "name": "fx" }, { "kind": "hudlabel", "name": "score", "pos": [4, 4], "fg": [255,255,255], "bg": [0,0,0] } // fixed implikováno pro hud kinds ],
"camera": { "mode": "follow", "target": "player", "axis": "x", "bounds": [0, 0, 1280, 240] },
"meta": { "editor": { "grid": 16, "name": "World 1-1" } } // runtime ignoruje}Poznámky:
- assets klíčované přes id; vrstvy referencují přes id (žádná duplikace).
src= zdrojová grafika, kterou baker konvertuje; nebodatapro inline grafiku kreslenou v editoru; neboblobpro předpečenou. - layer kinds:
tilemap,sprite,group(mnoho jedné bitmapy),particles,canvas,hudlabel/hud(implikujefixed: true). Jakákoli vrstva může nastavit"fixed": true. - name → adresovatelná entita; tag/group → seznam. Barvy jako
[r,g,b]. - camera je poradní data, která hra aplikuje přes
set_view(cíl/osa následování, hranice světa); hry stále mohou camera řídit samy. - meta je volné pro editor; runtime loader ignoruje neznámé klíče (forward-kompatibilita).
Pečený runtime modul (co zařízení importuje)
Sekce “Pečený runtime modul (co zařízení importuje)”# level1_scene.py (pak -> level1_scene.mpy)SCENE = { "bg": 0x2001, # předem převedené wire rgb565 "assets": { "hero": ("pal8", b"...", 12, 16, 6, 0, (0x0000, 0xF80F, ...)), # data,w,h,frames,transp,palette "tiles": ("pal8", b"...", 16, 16, 5, None, (...)), }, "tileprops": { "tiles": { "solid": b"\x00\x01\x00\x00\x00", # indexováno hodnotou tile "coin": b"\x00\x00\x01\x00\x00" } }, "layers": [ ("tilemap", "tiles", 80, 15, 0, 0, b"\x01\x01..."), # cols,rows,ox,oy,grid bytes ("sprite", "hero", "player", 40, 208, 128, 256, 0, {"lives":3}), # anchor v 1/256 ("group", "goomba", "enemies", 128, 256, ((224,208),(480,208),(704,208))), ("particles", "fx", 64, 2, 0.5, True), ("hudlabel", "score", 4, 4, 0xFFFF, 0x0000), ], "camera": ("follow", "player", "x", 0, 0, 1280, 240),}Tuple (ne dicty) pro vrstvy/assety drží .mpy malý a bez parsování; loader
je rozbaluje pozičně. Grid + tileprops jsou bytes (jedna alokace každý).
API runtime loaderu (sdílené zařízení + sim)
Sekce “API runtime loaderu (sdílené zařízení + sim)”import picogame_scene as pgs, terminalioview = pgs.load(pg, level1_scene.SCENE, font=terminalio.FONT)view.scene # picogame.Scene (už naplněná + uspořádaná do vrstev)view.named["player"] # Spriteview.group("enemies") # seznam Spriteview.named["fx"] # vrstva částicview.is_solid(tx, ty) # dotaz na vlastnost tile (z tileprops)view.tile_has(tx, ty, "coin")view.camera # (mode, target, axis, bounds) pro aplikaci hrou# hra pak běží svou vlastní smyčku: čte vstup, hýbe view.named[...], view.scene.refresh()Co tím získáme
Sekce “Co tím získáme”- Editor čte/zapisuje JSON; baker produkuje
.mpy; zařízení + sim načítají stejná data jedním loaderem. Round-trip i live preview oboje fungují. - Malování map, umisťování entit, sémantika tile, vrstvení, HUD, nastavení camera opouští kód hry — zbývá jen herní logika v Pythonu.
- Znovu používá vše už postavené: asset konvertory,
.mpypipeline,fixedHUD vrstvu, anchory, simulátor.
Rozhodnuto: jeden pečený modul, Python loader (žádný multi-file / žádný frozen)
Sekce “Rozhodnuto: jeden pečený modul, Python loader (žádný multi-file / žádný frozen)”Zvažovali jsme multi-file formát (Python drží reference, samostatný .bin /
C-vlastněný buffer drží „ostrá” data) kvůli RAM. Zamítnuto po analýze:
- Na filesystému (jediná realistická cesta nasazení — drag-drop na SD, žádný
reflash) landují asset bajty v RAM tak jako tak (inline
bytes,bytearrayz.bin, nebo C buffer jsou stejný GC heap, stejná velikost). RP2040 nemá memory-mapped filesystém, takže „čtení na místě” není možné. Multi-file / C-vlastní- data zde nedávají žádnou výhru v RAM. - Jediné, co posune data MIMO RAM, je zmrazení do flash — a v praxi nikdo nekompiluje firmware na hru, takže ta páka je mimo stůl.
- Proto je multi-file rozdělení komplikace, která se nevyplatí. Zahozeno.
Rozhodnutí: scéna = JEDEN pečený modul (data inline) + čistě Python loader.
Jednoduché, jeden soubor, drag-drop, sim-kompatibilní. RAM je v praxi v pohodě (hry sedí na
~10–54 KB assetů + ~20–30 KB strip bufferů v ~140–190 KB heapu). JEDINÝ důvod
někdy vyčlenit .bin je vyhnout se obřímu Python source literálu pro velký asset
(lekce squest 84 KB) + jediná čistá alokace — drž to jako taktiku podle potřeby
pro velké jednotlivé assety (cavern to dělá), NE jako pravidlo formátu.
Zbývající drobné volby: autorská mřížka jako rows+legend (současné) vs. 2-D int pole; nakolik loader vynucuje camera vs. ponechání jako poradní (současné: poradní).
Implementovaná sada polí (v2)
Sekce “Implementovaná sada polí (v2)”Autorství (editor je exportuje; tools/scene_build.py je peče):
- assets (sdílená banka):
sprite/tileset/bitmap(srcPNG +frame/tile,frames,transparent),rect,tileset_color; volitelné per-tileprops(solid/coin/goal/hazard) aanimations: {name:{frames,fps,loop}}. - sounds (sdílená banka):
{id:{src}}wav reference. - layers na úroveň: více
tilemapvrstev (vrstva sfg:truekreslí přes sprite);sprite(sname/anchor/frame/anim/data);group(tagované instance);hudlabel(nezávislé na camera);particles. - na úroveň také:
zones:[{tag,x,y,w,h}],points:[{name,x,y}],camera,music.
Dva top-level tvary: jednotlivá picogame-scene (assety inline) pečená do jednoho _scene
modulu, nebo picogame-project (asset banka + levels[]) pečený do jednoho _bank modulu +
jeden _level modul na úroveň.
Runtime loader picogame_scene vystavuje: view.scene, view.named[...], view.group(tag),
view.tick(dt) (animace), view.is_solid(tx,ty)/view.tile_has(...), view.in_zone(x,y,tag),
view.point(name), view.sounds/view.play(id), view.camera; plus load_bank(pg,BANK)
load(...,bank=)pro načítání úroveň-po-úrovni ze sdílené banky. Vše validováno headless v simulátoru (editor core.js → bake → load → render).