Přeskočit na obsah

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.

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álem bytes (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í .mpy pipeline (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; nebo data pro inline grafiku kreslenou v editoru; nebo blob pro předpečenou.
  • layer kinds: tilemap, sprite, group (mnoho jedné bitmapy), particles, canvas, hudlabel/hud (implikuje fixed: 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, terminalio
view = pgs.load(pg, level1_scene.SCENE, font=terminalio.FONT)
view.scene # picogame.Scene (už naplněná + uspořádaná do vrstev)
view.named["player"] # Sprite
view.group("enemies") # seznam Sprite
view.named["fx"] # vrstva částic
view.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()
  • 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, .mpy pipeline, fixed HUD 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, bytearray z .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 (src PNG + frame/tile, frames, transparent), rect, tileset_color; volitelné per-tile props (solid/coin/goal/hazard) a animations: {name:{frames,fps,loop}}.
  • sounds (sdílená banka): {id:{src}} wav reference.
  • layers na úroveň: více tilemap vrstev (vrstva s fg:true kreslí přes sprite); sprite (s name/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).