Tutoriál 2 — Starship
V tomto tutoriálu si postavíš malou střílečku ve stylu Asteroids. Předpokládáme, že už máš za sebou 01-bounce (vykreslovací smyčku, ovládání, pohyb s desetinnou přesností, kolize, HUD a částice). Tentokrát přidáš rotaci, vektorový tah, fondy objektů, dělení nepřátel a stavový automat.
Celý zdrojový kód najdeš na GitHubu.
Spusť libovolný krok:
python3 sim/run.py tutorials/02-starship/stepN_name.py --hold UP,B --shot /tmp/out.pngKrok 1 — step1_ship.py · sprite ve tvaru lodi
Sekce “Krok 1 — step1_ship.py · sprite ve tvaru lodi”
picogame_game.setup připraví displej a vrátí
(scene, buffer_a, buffer_b). U SPI displeje jsou poslední dvě hodnoty znovu použitelné
řádkové buffery, u framebufferu mají hodnotu None. Tento krok potřebuje jen
Scene, a tak zbývající hodnoty zahodí (scene, _, _).
picogame_shapes (importovaný jako shp) vytváří bitmapy: shp.from_mask převede
ASCII obrázek na jednobarevný Bitmap. Zabalíme ho do Sprite a
anchor=(0.5, 0.5) umístí referenční bod spritu do jeho středu. To se hodí pro objekt,
který se otáčí a přechází přes okraje. Uvidíš: malou loď uprostřed. Zkus: překreslit masku.
18 collapsed lines
# Starship -- step 1: a ship on screen (recap, with a shaped sprite).## This second tutorial assumes you've done Bounce (01-bounce). It builds a top-down# space shooter and covers what Bounce couldn't: rotation, vector thrust, object# pools (bullets/enemies), circular collision, explosions, and game states.## What you learn here (recap): a Sprite can be any shape. shp.from_mask turns an# ASCII picture into a one-colour bitmap. anchor=(0.5, 0.5) puts the sprite's# reference point at its CENTRE -- the natural choice for something that rotates.## New: shp.from_mask, centre anchor.## Run: python3 sim/run.py tutorials/02-starship/step1_ship.py --shot /tmp/p1.png
import picogame as pgimport picogame_gameimport picogame_clockimport picogame_shapes as shp
W, H = 320, 240BACKGROUND = pg.rgb565(0, 0, 8)
scene, _, _ = picogame_game.setup(background=BACKGROUND)clock = picogame_clock.Clock(30)
SHIP_MASK = [ " # ", " # ", " ### ", " ### ", "#####", "## ##",]ship = pg.Sprite(shp.from_mask(SHIP_MASK, pg.rgb565(200, 220, 255)), W // 2, H // 2)ship.anchor = (0.5, 0.5) # rotate/position about the centrescene.add(ship)
while True: scene.refresh() clock.tick()Krok 2 — step2_fly.py · rotace, tah a přechod přes okraj
Sekce “Krok 2 — step2_fly.py · rotace, tah a přechod přes okraj”
Pro loď, která se pořád otáčí, připravíme všechny úhly předem. Výsledek je ostřejší a
levnější než změna sprite.angle za běhu: shp.poly_frames(size, points, N, colour)
vykreslí polygon v N úhlech do jedné bitmapy s více snímky a ship.frame = angle vybere
správný snímek. Tabulka DIRS obsahuje jednotkový vektor každého úhlu; UP podle něj přidává
tah k rychlosti lodi. Maximální rychlost omezuje strop a loď postupně zpomaluje odporem.
wrap() ji po přeletu okraje přesune na opačnou stranu. Uvidíš: loď, kterou pilotuješ
ve stylu Asteroids. Zkus: změnit
tah 0.25 nebo odpor 0.99.
22 collapsed lines
# Starship -- step 2: rotate, thrust, and wrap around the screen.## What you learn: pre-baked rotation. For a ship that spins constantly, baking the# rotations into frames is crisper and cheaper than rotating at runtime (sprite.angle).# shp.poly_frames(size, points, N, colour) renders a polygon at N angles into# one multi-frame bitmap; setting ship.frame = angle_index shows that rotation. A# DIRS table holds the unit vector for each angle. UP thrusts along the facing# vector into the sub-pixel velocity (velocity_x, velocity_y); we cap top speed and apply a little# drag so it drifts like a spaceship. wrap() teleports across screen edges.## New vs step 1: shp.poly_frames (pre-rotated frames), ship.frame, vector thrust# into fx/fy, speed cap + drag, screen wrap.## Run: python3 sim/run.py tutorials/02-starship/step2_fly.py --hold UP --shot /tmp/p2.png
import mathimport picogame as pgimport picogame_gameimport picogame_inputimport picogame_clockimport picogame_shapes as shp
W, H = 320, 240BACKGROUND = pg.rgb565(0, 0, 8)FRAMES = 16 # number of baked rotation frames
scene, _, _ = picogame_game.setup(background=BACKGROUND)btn = picogame_input.Buttons()clock = picogame_clock.Clock(30)
# a ship polygon (points around the centre, +y is down), baked at FRAMES anglesship_bitmap = shp.poly_frames(18, [(0, -8), (6, 7), (0, 4), (-6, 7)], FRAMES, pg.rgb565(200, 220, 255))# facing unit vector for each frame: frame 0 points up (-y)DIRS = [(math.sin(frame * 2 * math.pi / FRAMES), -math.cos(frame * 2 * math.pi / FRAMES)) for frame in range(FRAMES)]
ship = pg.Sprite(ship_bitmap, W // 2, H // 2)ship.anchor = (0.5, 0.5)scene.add(ship)
angle = 0 # current rotation framevelocity_x = velocity_y = 0.0 # velocity
def wrap(x, y): return x % W, y % H
while True: btn.poll() if btn.is_pressed(btn.LEFT): angle = (angle - 1) % FRAMES if btn.is_pressed(btn.RIGHT): angle = (angle + 1) % FRAMES delta_x, delta_y = DIRS[angle] if btn.is_pressed(btn.UP): velocity_x += delta_x * 0.25 # accelerate along the facing vector velocity_y += delta_y * 0.25
speed = math.sqrt(velocity_x * velocity_x + velocity_y * velocity_y) # cap top speed if speed > 5: velocity_x *= 5 / speed velocity_y *= 5 / speed velocity_x *= 0.99 # gentle drag velocity_y *= 0.99
ship.fx, ship.fy = wrap(ship.fx + velocity_x, ship.fy + velocity_y) # sub-pixel position + wrap ship.frame = angle # show the matching rotation
scene.refresh() clock.tick()Krok 3 — step3_shoot.py · fond objektů
Sekce “Krok 3 — step3_shoot.py · fond objektů”
Opakované vytváření a zahazování spritů může tříštit haldu. Pool(scene, bitmap, N) z
modulu picogame_pool proto jednou připraví N skrytých spritů. spawn()
zviditelní volný sprite, free() ho zase skryje a sprite.visible zároveň slouží jako
příznak aktivní střely. Každá střela má rychlost a zbývající životnost v sprite.data,
pozici pak v fx a fy. Proměnná fire_cooldown omezuje kadenci a just_pressed(B) z
picogame_input vystřelí jednou při stisku B. Uvidíš: proud střel,
které po chvíli zaniknou. Zkus: změnit velikost fondu nebo fire_cooldown.
22 collapsed lines
# Starship -- step 3: fire bullets from an object pool.## What you learn: pooling. Spawning objects (bullets, enemies, sparks) by creating# Sprites at runtime causes memory churn. Instead, pre-allocate a fixed pool ONCE:# picogame_pool.Pool makes N hidden sprites in the scene, spawn() reveals the first# free one, free() hides it, and sprite.visible IS the alive flag. We keep each# bullet's velocity + remaining life in sprite.data, and its position in fx/fy.# A cooldown limits the fire rate.## New vs step 2: picogame_pool.Pool, spawn/free, btn.just_pressed (a fresh press),# per-bullet state in sprite.data, a fire cooldown + bullet lifetime.## Run: python3 sim/run.py tutorials/02-starship/step3_shoot.py --hold B --shot /tmp/p3.png
import mathimport picogame as pgimport picogame_gameimport picogame_inputimport picogame_clockimport picogame_shapes as shpimport picogame_pool
W, H = 320, 240BACKGROUND = pg.rgb565(0, 0, 8)FRAMES = 16
scene, _, _ = picogame_game.setup(background=BACKGROUND)btn = picogame_input.Buttons()clock = picogame_clock.Clock(30)
ship_bitmap = shp.poly_frames(18, [(0, -8), (6, 7), (0, 4), (-6, 7)], FRAMES, pg.rgb565(200, 220, 255))DIRS = [(math.sin(frame * 2 * math.pi / FRAMES), -math.cos(frame * 2 * math.pi / FRAMES)) for frame in range(FRAMES)]bullet_bitmap = shp.circle(4, pg.rgb565(255, 255, 120))
ship = pg.Sprite(ship_bitmap, W // 2, H // 2)ship.anchor = (0.5, 0.5)bullets = picogame_pool.Pool(scene, bullet_bitmap, 6, anchor=(0.5, 0.5)) # NEW: 6-bullet poolscene.add(ship) # add ship AFTER the pool so it draws on top
angle = 0velocity_x = velocity_y = 0.0fire_cooldown = 0
def wrap(x, y): return x % W, y % H
while True: btn.poll() fire_cooldown -= 1 if btn.is_pressed(btn.LEFT): angle = (angle - 1) % FRAMES if btn.is_pressed(btn.RIGHT): angle = (angle + 1) % FRAMES delta_x, delta_y = DIRS[angle] if btn.is_pressed(btn.UP): velocity_x += delta_x * 0.25 velocity_y += delta_y * 0.25 speed = math.sqrt(velocity_x * velocity_x + velocity_y * velocity_y) if speed > 5: velocity_x *= 5 / speed; velocity_y *= 5 / speed velocity_x *= 0.99; velocity_y *= 0.99 ship.fx, ship.fy = wrap(ship.fx + velocity_x, ship.fy + velocity_y) ship.frame = angle
# fire: a fresh B press, if the cooldown has elapsed and a slot is free if btn.just_pressed(btn.B) and fire_cooldown <= 0: bullet = bullets.spawn() if bullet: bullet.data = {"velocity_x": delta_x * 7, "velocity_y": delta_y * 7, "life": 30} bullet.move(ship.x, ship.y) fire_cooldown = 6
# advance live bullets; retire them when their life runs out for bullet in bullets.items: if not bullet.visible: continue bullet.data["life"] -= 1 if bullet.data["life"] <= 0: bullets.free(bullet) continue bullet.fx, bullet.fy = wrap(bullet.fx + bullet.data["velocity_x"], bullet.fy + bullet.data["velocity_y"])
scene.refresh() clock.tick()Krok 4 — step4_rocks.py · druhý fond a vlny
Sekce “Krok 4 — step4_rocks.py · druhý fond a vlny”
Stejný fond použijeme i pro nepřátele. Kameny mají tři velikosti uložené v sprite.data;
odpovídající kruhovou bitmapu vybíráme přes sprite.bitmap. new_wave(n) rozmístí po
obrazovce n kamenů a každému přidělí směr pohybu. Uvidíš: pohybující se prstence kamenů. Zkus:
změnit počet ve vlně nebo rychlost kamenů.
20 collapsed lines
# Starship -- step 4: asteroids to dodge (a second pool + waves).## What you learn: reuse the pool pattern for enemies, and spawn a "wave". Rocks come# in 3 sizes (we keep the size in sprite.data and pick a matching ring bitmap with# sprite.bitmap). A wave spreads N rocks around the screen, each drifting with its# own velocity. shp.ring draws a hollow circle.## New vs step 3: a rocks Pool with per-rock size/velocity, choosing a bitmap per# rock (sprite.bitmap), spawning a wave.## Run: python3 sim/run.py tutorials/02-starship/step4_rocks.py --shot /tmp/p4.png
import mathimport picogame as pgimport picogame_gameimport picogame_inputimport picogame_clockimport picogame_shapes as shpimport picogame_pool
W, H = 320, 240BACKGROUND = pg.rgb565(0, 0, 8)FRAMES = 16
scene, _, _ = picogame_game.setup(background=BACKGROUND)btn = picogame_input.Buttons()clock = picogame_clock.Clock(30)
ship_bitmap = shp.poly_frames(18, [(0, -8), (6, 7), (0, 4), (-6, 7)], FRAMES, pg.rgb565(200, 220, 255))DIRS = [(math.sin(frame * 2 * math.pi / FRAMES), -math.cos(frame * 2 * math.pi / FRAMES)) for frame in range(FRAMES)]bullet_bitmap = shp.circle(4, pg.rgb565(255, 255, 120))ROCK_BITMAP = [shp.ring(40, pg.rgb565(170, 140, 100), 3), # size 0 = big shp.ring(24, pg.rgb565(170, 140, 100), 3), # size 1 = medium shp.ring(13, pg.rgb565(170, 140, 100), 2)] # size 2 = small
ship = pg.Sprite(ship_bitmap, W // 2, H // 2)ship.anchor = (0.5, 0.5)rocks = picogame_pool.Pool(scene, ROCK_BITMAP[0], 16, anchor=(0.5, 0.5)) # NEWbullets = picogame_pool.Pool(scene, bullet_bitmap, 6, anchor=(0.5, 0.5))scene.add(ship)
angle = 0velocity_x = velocity_y = 0.0fire_cooldown = 0wave = 3
def wrap(x, y): return x % W, y % H
def spawn_rock(size, x, y, velocity_x, velocity_y): rock = rocks.spawn() if rock is None: return rock.data = {"size": size, "velocity_x": velocity_x, "velocity_y": velocity_y} rock.bitmap = ROCK_BITMAP[size] # pick the bitmap for this size rock.fx, rock.fy = float(x), float(y)
def new_wave(count): for i in range(count): angle_rad = i * 2 * math.pi / count spawn_rock(0, (W // 2 + int(140 * math.cos(angle_rad))) % W, (H // 2 + int(110 * math.sin(angle_rad))) % H, math.cos(angle_rad) * 1.2, math.sin(angle_rad) * 1.2)
new_wave(wave)32 collapsed lines
while True: btn.poll() fire_cooldown -= 1 if btn.is_pressed(btn.LEFT): angle = (angle - 1) % FRAMES if btn.is_pressed(btn.RIGHT): angle = (angle + 1) % FRAMES delta_x, delta_y = DIRS[angle] if btn.is_pressed(btn.UP): velocity_x += delta_x * 0.25; velocity_y += delta_y * 0.25 speed = math.sqrt(velocity_x * velocity_x + velocity_y * velocity_y) if speed > 5: velocity_x *= 5 / speed; velocity_y *= 5 / speed velocity_x *= 0.99; velocity_y *= 0.99 ship.fx, ship.fy = wrap(ship.fx + velocity_x, ship.fy + velocity_y) ship.frame = angle
if btn.just_pressed(btn.B) and fire_cooldown <= 0: bullet = bullets.spawn() if bullet: bullet.data = {"velocity_x": delta_x * 7, "velocity_y": delta_y * 7, "life": 30} bullet.move(ship.x, ship.y) fire_cooldown = 6 for bullet in bullets.items: if not bullet.visible: continue bullet.data["life"] -= 1 if bullet.data["life"] <= 0: bullets.free(bullet) continue bullet.fx, bullet.fy = wrap(bullet.fx + bullet.data["velocity_x"], bullet.fy + bullet.data["velocity_y"])
# drift the rocks for rock in rocks.items: if not rock.visible: continue rock.fx, rock.fy = wrap(rock.fx + rock.data["velocity_x"], rock.fy + rock.data["velocity_y"])
scene.refresh() clock.tick()Krok 5 — step5_collide.py · kruhové zásahy a dělení
Sekce “Krok 5 — step5_collide.py · kruhové zásahy a dělení”
a.near(b, r) je rychlý test vzdálenosti bez odmocniny na středech spritů,
ideální pro kulaté věci. Střela, která zasáhne kámen, uvolní oba; velký/střední kámen se rozdělí
na dva menší, které odletí od sebe. Když kámen zasáhne loď, přijdeš o život. Loď se znovu
objeví s krátkou nezranitelností, kterou znázorňuje blikání ship.visible. Uvidíš: že
můžeš kameny rozstřílet a sám dostat zásah. Zkus: změnit ROCK_RADIUS nebo rychlosti dělení.
21 collapsed lines
# Starship -- step 5: shooting rocks (and getting hit).## What you learn: circular collision + spawning on destruction. sprite.near(other, r)# (a, b, r) is a fast no-sqrt distance test reading sprite positions -- ideal for# round things. A bullet that hits a rock frees both; a big/medium rock SPLITS into# two smaller rocks flying apart. A rock that reaches the ship costs a life and# triggers a brief invulnerability (i-frames) + respawn so you don't die instantly.## New vs step 4: sprite.near (circular hit), splitting rocks, lives + i-frames + respawn,# blinking the ship while invulnerable (ship.visible toggled).## Run: python3 sim/run.py tutorials/02-starship/step5_collide.py --hold B --shot /tmp/p5.png
import mathimport picogame as pgimport picogame_gameimport picogame_inputimport picogame_clockimport picogame_shapes as shpimport picogame_pool
W, H = 320, 240BACKGROUND = pg.rgb565(0, 0, 8)FRAMES = 16
scene, _, _ = picogame_game.setup(background=BACKGROUND)btn = picogame_input.Buttons()clock = picogame_clock.Clock(30)
ship_bitmap = shp.poly_frames(18, [(0, -8), (6, 7), (0, 4), (-6, 7)], FRAMES, pg.rgb565(200, 220, 255))DIRS = [(math.sin(frame * 2 * math.pi / FRAMES), -math.cos(frame * 2 * math.pi / FRAMES)) for frame in range(FRAMES)]bullet_bitmap = shp.circle(4, pg.rgb565(255, 255, 120))ROCK_BITMAP = [shp.ring(40, pg.rgb565(170, 140, 100), 3), shp.ring(24, pg.rgb565(170, 140, 100), 3), shp.ring(13, pg.rgb565(170, 140, 100), 2)]ROCK_RADIUS = [20, 12, 6] # collision radius per size
ship = pg.Sprite(ship_bitmap, W // 2, H // 2)ship.anchor = (0.5, 0.5)rocks = picogame_pool.Pool(scene, ROCK_BITMAP[0], 16, anchor=(0.5, 0.5))bullets = picogame_pool.Pool(scene, bullet_bitmap, 6, anchor=(0.5, 0.5))scene.add(ship)
angle = 0velocity_x = velocity_y = 0.0fire_cooldown = 0lives = 3invincible = 60 # invincibility frameswave = 3frame = 0
def wrap(x, y): return x % W, y % H
def spawn_rock(size, x, y, velocity_x, velocity_y): rock = rocks.spawn() if rock is None: return rock.data = {"size": size, "velocity_x": velocity_x, "velocity_y": velocity_y} rock.bitmap = ROCK_BITMAP[size] rock.fx, rock.fy = float(x), float(y)
def new_wave(count): for i in range(count): angle_rad = i * 2 * math.pi / count spawn_rock(0, (W // 2 + int(140 * math.cos(angle_rad))) % W, (H // 2 + int(110 * math.sin(angle_rad))) % H, math.cos(angle_rad) * 1.2, math.sin(angle_rad) * 1.2)
def respawn(): global velocity_x, velocity_y, invincible ship.fx, ship.fy = float(W // 2), float(H // 2) velocity_x = velocity_y = 0.0 invincible = 90
new_wave(wave)while True: btn.poll() frame += 1 fire_cooldown -= 1 if invincible > 0: invincible -= 1
if btn.is_pressed(btn.LEFT):33 collapsed lines
angle = (angle - 1) % FRAMES if btn.is_pressed(btn.RIGHT): angle = (angle + 1) % FRAMES delta_x, delta_y = DIRS[angle] if btn.is_pressed(btn.UP): velocity_x += delta_x * 0.25; velocity_y += delta_y * 0.25 speed = math.sqrt(velocity_x * velocity_x + velocity_y * velocity_y) if speed > 5: velocity_x *= 5 / speed; velocity_y *= 5 / speed velocity_x *= 0.99; velocity_y *= 0.99 ship.fx, ship.fy = wrap(ship.fx + velocity_x, ship.fy + velocity_y) ship.frame = angle ship.visible = (invincible <= 0) or (frame & 1) # blink while invincible
if btn.just_pressed(btn.B) and fire_cooldown <= 0: bullet = bullets.spawn() if bullet: bullet.data = {"velocity_x": delta_x * 7, "velocity_y": delta_y * 7, "life": 30} bullet.move(ship.x, ship.y) fire_cooldown = 6 for bullet in bullets.items: if not bullet.visible: continue bullet.data["life"] -= 1 if bullet.data["life"] <= 0: bullets.free(bullet) continue bullet.fx, bullet.fy = wrap(bullet.fx + bullet.data["velocity_x"], bullet.fy + bullet.data["velocity_y"])
for rock in rocks.items: if not rock.visible: continue rock.fx, rock.fy = wrap(rock.fx + rock.data["velocity_x"], rock.fy + rock.data["velocity_y"]) size = rock.data["size"] radius = ROCK_RADIUS[size] # bullet hits this rock? for bullet in bullets.items: if not bullet.visible: continue if bullet.near(rock, radius): bullets.free(bullet) rocks.free(rock) if size < 2: # split into two smaller, flying apart for sign in (-1, 1): spawn_rock(size + 1, rock.fx, rock.fy, rock.data["velocity_x"] + sign * 0.8, rock.data["velocity_y"] - sign * 0.8) break # rock reaches the ship? if invincible <= 0 and rock.visible and ship.near(rock, radius + 6): lives -= 1 respawn() if lives < 0: lives = 3 rocks.free_all() new_wave(wave) break
scene.refresh() clock.tick()Krok 6 — step6_waves.py · skóre a postup
Sekce “Krok 6 — step6_waves.py · skóre a postup”
Menší kameny dávají víc bodů. SceneLabel z picogame_ui ukazuje
skóre a počet lodí. Když je rocks.count() == 0,
na ploše nezůstal žádný kámen, takže spustíme další, větší vlnu. Uvidíš: rostoucí
skóre a vlny, které se zvětšují. Zkus: změnit bodování nebo růst vln.
21 collapsed lines
# Starship -- step 6: score, lives, and escalating waves.## What you learn: a scoring/progression loop. Smaller rocks are worth more. A HUD# shows score + ships. When the field is clear (rocks.count() == 0) we start the# next, bigger wave -- the game keeps going and ramps up.## New vs step 5: picogame_ui.SceneLabel, scoring, rocks.count() to detect a cleared# field, growing waves.## Run: python3 sim/run.py tutorials/02-starship/step6_waves.py --hold B --shot /tmp/p6.png
import mathimport terminalioimport picogame as pgimport picogame_gameimport picogame_inputimport picogame_clockimport picogame_shapes as shpimport picogame_poolimport picogame_ui as ui
W, H = 320, 240BACKGROUND = pg.rgb565(0, 0, 8)FRAMES = 16
scene, _, _ = picogame_game.setup(background=BACKGROUND)btn = picogame_input.Buttons()clock = picogame_clock.Clock(30)
ship_bitmap = shp.poly_frames(18, [(0, -8), (6, 7), (0, 4), (-6, 7)], FRAMES, pg.rgb565(200, 220, 255))DIRS = [(math.sin(frame * 2 * math.pi / FRAMES), -math.cos(frame * 2 * math.pi / FRAMES)) for frame in range(FRAMES)]bullet_bitmap = shp.circle(4, pg.rgb565(255, 255, 120))ROCK_BITMAP = [shp.ring(40, pg.rgb565(170, 140, 100), 3), shp.ring(24, pg.rgb565(170, 140, 100), 3), shp.ring(13, pg.rgb565(170, 140, 100), 2)]ROCK_RADIUS = [20, 12, 6]
ship = pg.Sprite(ship_bitmap, W // 2, H // 2)ship.anchor = (0.5, 0.5)rocks = picogame_pool.Pool(scene, ROCK_BITMAP[0], 16, anchor=(0.5, 0.5))bullets = picogame_pool.Pool(scene, bullet_bitmap, 6, anchor=(0.5, 0.5))scene.add(ship)hud = ui.SceneLabel(scene, pg, terminalio.FONT, 4, 4, pg.rgb565(255, 255, 255), BACKGROUND)
angle = 0velocity_x = velocity_y = 0.0fire_cooldown = 0lives = 3invincible = 60wave = 3score = 0frame = 0
79 collapsed lines
def wrap(x, y): return x % W, y % H
def spawn_rock(size, x, y, velocity_x, velocity_y): rock = rocks.spawn() if rock is None: return rock.data = {"size": size, "velocity_x": velocity_x, "velocity_y": velocity_y} rock.bitmap = ROCK_BITMAP[size] rock.fx, rock.fy = float(x), float(y)
def new_wave(count): for i in range(count): angle_rad = i * 2 * math.pi / count spawn_rock(0, (W // 2 + int(140 * math.cos(angle_rad))) % W, (H // 2 + int(110 * math.sin(angle_rad))) % H, math.cos(angle_rad) * 1.2, math.sin(angle_rad) * 1.2)
def respawn(): global velocity_x, velocity_y, invincible ship.fx, ship.fy = float(W // 2), float(H // 2) velocity_x = velocity_y = 0.0 invincible = 90
new_wave(wave)while True: btn.poll() frame += 1 fire_cooldown -= 1 if invincible > 0: invincible -= 1
if btn.is_pressed(btn.LEFT): angle = (angle - 1) % FRAMES if btn.is_pressed(btn.RIGHT): angle = (angle + 1) % FRAMES delta_x, delta_y = DIRS[angle] if btn.is_pressed(btn.UP): velocity_x += delta_x * 0.25; velocity_y += delta_y * 0.25 speed = math.sqrt(velocity_x * velocity_x + velocity_y * velocity_y) if speed > 5: velocity_x *= 5 / speed; velocity_y *= 5 / speed velocity_x *= 0.99; velocity_y *= 0.99 ship.fx, ship.fy = wrap(ship.fx + velocity_x, ship.fy + velocity_y) ship.frame = angle ship.visible = (invincible <= 0) or (frame & 1)
if btn.just_pressed(btn.B) and fire_cooldown <= 0: bullet = bullets.spawn() if bullet: bullet.data = {"velocity_x": delta_x * 7, "velocity_y": delta_y * 7, "life": 30} bullet.move(ship.x, ship.y) fire_cooldown = 6 for bullet in bullets.items: if not bullet.visible: continue bullet.data["life"] -= 1 if bullet.data["life"] <= 0: bullets.free(bullet) continue bullet.fx, bullet.fy = wrap(bullet.fx + bullet.data["velocity_x"], bullet.fy + bullet.data["velocity_y"])
for rock in rocks.items: if not rock.visible: continue rock.fx, rock.fy = wrap(rock.fx + rock.data["velocity_x"], rock.fy + rock.data["velocity_y"]) size = rock.data["size"] radius = ROCK_RADIUS[size] for bullet in bullets.items: if not bullet.visible: continue if bullet.near(rock, radius): bullets.free(bullet) rocks.free(rock) score += (3 - size) * 20 # smaller rock -> more points if size < 2: for sign in (-1, 1): spawn_rock(size + 1, rock.fx, rock.fy, rock.data["velocity_x"] + sign * 0.8, rock.data["velocity_y"] - sign * 0.8) break if invincible <= 0 and rock.visible and ship.near(rock, radius + 6): lives -= 1 respawn() if lives < 0: lives = 3 score = 0 rocks.free_all() new_wave(wave) break
if rocks.count() == 0: # field cleared -> next, bigger wave wave += 1 new_wave(wave)
hud.set("SCORE %05d SHIPS %d" % (score, max(0, lives))) scene.refresh() clock.tick()Krok 7 — step7_particles.py · exploze, výfuk a zvuk
Sekce “Krok 7 — step7_particles.py · exploze, výfuk a zvuk”
Jeden systém Particles(fade=True) obslouží dva efekty: výbuch při zničení kamene
(spousta rychlých, mizejících jisker) a plamen motoru (pár krátkých jisker za lodí
v každém snímku, kdy zrychluješ). fade=True ztmavuje částice, jak stárnou. tone() přidá
pípnutí výstřelu a hlubší dunění. Uvidíš: exploze a stopu motoru. Zkus: změnit
count/life exploze nebo frekvence pípnutí.
24 collapsed lines
# Starship -- step 7: explosions, thrust flame, and sound.## What you learn: particles for two different effects, plus audio. One Particles# system gives us BOTH a burst on a rock's destruction (many fast, fading sparks)# and a thrust flame (a few short-lived sparks behind the ship each frame while# thrusting). fade=True dims particles as they age. tone() beeps for firing and a# lower boom for explosions. Audio is optional (try/except) so it's silent but safe# where there's no audio output.## New vs step 6: pg.Particles(fade=True), emit for explosions AND exhaust,# picogame_audio for fire/boom beeps.## Run: python3 sim/run.py tutorials/02-starship/step7_particles.py --hold UP,B --shot /tmp/p7.png
import mathimport terminalioimport picogame as pgimport picogame_gameimport picogame_inputimport picogame_clockimport picogame_shapes as shpimport picogame_poolimport picogame_ui as ui
W, H = 320, 240BACKGROUND = pg.rgb565(0, 0, 8)FRAMES = 16
scene, _, _ = picogame_game.setup(background=BACKGROUND)btn = picogame_input.Buttons()clock = picogame_clock.Clock(30)
try: import picogame_audio audio = picogame_audio.Audio() snd_fire = picogame_audio.tone(880, 25) snd_boom = picogame_audio.tone(160, 90)except Exception: audio = None
ship_bitmap = shp.poly_frames(18, [(0, -8), (6, 7), (0, 4), (-6, 7)], FRAMES, pg.rgb565(200, 220, 255))DIRS = [(math.sin(frame * 2 * math.pi / FRAMES), -math.cos(frame * 2 * math.pi / FRAMES)) for frame in range(FRAMES)]bullet_bitmap = shp.circle(4, pg.rgb565(255, 255, 120))ROCK_BITMAP = [shp.ring(40, pg.rgb565(170, 140, 100), 3), shp.ring(24, pg.rgb565(170, 140, 100), 3), shp.ring(13, pg.rgb565(170, 140, 100), 2)]ROCK_RADIUS = [20, 12, 6]
ship = pg.Sprite(ship_bitmap, W // 2, H // 2)ship.anchor = (0.5, 0.5)rocks = picogame_pool.Pool(scene, ROCK_BITMAP[0], 16, anchor=(0.5, 0.5))bullets = picogame_pool.Pool(scene, bullet_bitmap, 6, anchor=(0.5, 0.5))sparks = pg.Particles(160, size=2, fade=True) # NEWscene.add(sparks)scene.add(ship)55 collapsed lines
hud = ui.SceneLabel(scene, pg, terminalio.FONT, 4, 4, pg.rgb565(255, 255, 255), BACKGROUND)
angle = 0velocity_x = velocity_y = 0.0fire_cooldown = 0lives = 3invincible = 60wave = 3score = 0frame = 0
def wrap(x, y): return x % W, y % H
def spawn_rock(size, x, y, velocity_x, velocity_y): rock = rocks.spawn() if rock is None: return rock.data = {"size": size, "velocity_x": velocity_x, "velocity_y": velocity_y} rock.bitmap = ROCK_BITMAP[size] rock.fx, rock.fy = float(x), float(y)
def new_wave(count): for i in range(count): angle_rad = i * 2 * math.pi / count spawn_rock(0, (W // 2 + int(140 * math.cos(angle_rad))) % W, (H // 2 + int(110 * math.sin(angle_rad))) % H, math.cos(angle_rad) * 1.2, math.sin(angle_rad) * 1.2)
def respawn(): global velocity_x, velocity_y, invincible ship.fx, ship.fy = float(W // 2), float(H // 2) velocity_x = velocity_y = 0.0 invincible = 90
new_wave(wave)while True: btn.poll() frame += 1 fire_cooldown -= 1 if invincible > 0: invincible -= 1
if btn.is_pressed(btn.LEFT): angle = (angle - 1) % FRAMES if btn.is_pressed(btn.RIGHT): angle = (angle + 1) % FRAMES delta_x, delta_y = DIRS[angle] if btn.is_pressed(btn.UP): velocity_x += delta_x * 0.25; velocity_y += delta_y * 0.25 # thrust flame: a couple of orange sparks shot out the back sparks.emit(ship.x - int(delta_x * 8), ship.y - int(delta_y * 8), 2, 2, 10, pg.rgb565(255, 150, 40)) speed = math.sqrt(velocity_x * velocity_x + velocity_y * velocity_y) if speed > 5: velocity_x *= 5 / speed; velocity_y *= 5 / speed velocity_x *= 0.99; velocity_y *= 0.99 ship.fx, ship.fy = wrap(ship.fx + velocity_x, ship.fy + velocity_y) ship.frame = angle ship.visible = (invincible <= 0) or (frame & 1)
if btn.just_pressed(btn.B) and fire_cooldown <= 0: bullet = bullets.spawn() if bullet: bullet.data = {"velocity_x": delta_x * 7, "velocity_y": delta_y * 7, "life": 30} bullet.move(ship.x, ship.y) fire_cooldown = 6 if audio: audio.sfx(snd_fire) for bullet in bullets.items: if not bullet.visible: continue bullet.data["life"] -= 1 if bullet.data["life"] <= 0: bullets.free(bullet) continue bullet.fx, bullet.fy = wrap(bullet.fx + bullet.data["velocity_x"], bullet.fy + bullet.data["velocity_y"])
for rock in rocks.items: if not rock.visible: continue rock.fx, rock.fy = wrap(rock.fx + rock.data["velocity_x"], rock.fy + rock.data["velocity_y"]) size = rock.data["size"] radius = ROCK_RADIUS[size] for bullet in bullets.items: if not bullet.visible: continue if bullet.near(rock, radius): bullets.free(bullet) rocks.free(rock) score += (3 - size) * 20 sparks.emit(rock.x, rock.y, 18, 3, 26, pg.rgb565(255, 200, 120)) # explosion if audio: audio.sfx(snd_boom) if size < 2: for sign in (-1, 1): spawn_rock(size + 1, rock.fx, rock.fy, rock.data["velocity_x"] + sign * 0.8, rock.data["velocity_y"] - sign * 0.8) break if invincible <= 0 and rock.visible and ship.near(rock, radius + 6): lives -= 1 sparks.emit(ship.x, ship.y, 24, 4, 30, pg.rgb565(120, 200, 255)) respawn() if lives < 0: lives = 3 score = 0 rocks.free_all() new_wave(wave) break
if rocks.count() == 0: wave += 1 new_wave(wave)
sparks.tick() # advance all particles hud.set("SCORE %05d SHIPS %d" % (score, max(0, lives))) scene.refresh() clock.tick()Krok 8 — step8_states.py · stavový automat
Sekce “Krok 8 — step8_states.py · stavový automat”
Hotová hra potřebuje víc než jeden režim — a jak kód roste, vyplatí se ho uklidit. Všechny měnitelné
hodnoty pokusu se přesunou do jednoho objektu State (st), re-inicializovaného na místě metodou
reset(), a celá smyčka se přesune do funkce main() (uvnitř funkce se její jména vyhledávají jako
rychlé lokály — na zařízení malý měřitelný zisk). st.mode běží TITLE → PLAY → GAMEOVER; new_game()
volá st.reset() pro restart; vycentrovaný SceneLabel zobrazuje zprávy mimo samotnou hru; smrt
ukončí pokus místo tichého restartu. Tohle je tvar State + main(), do kterého každá větší hra
doroste — viz Herní vzory a hotová kostra. Uvidíš:
titulní obrazovka → hra → konec hry → titulní obrazovka. Zkus: přidat nejlepší skóre, které zůstane
uložené i po další hře (nech ho jako modulový globál, ne ve State; viz picogame_save
a ukládání do NVM).
27 collapsed lines
# Starship -- step 8: game states (title / playing / game over) + restart, tidied into State + main().## What you learn: a state machine -- the backbone of a finished game -- built the way picogame# recommends. All the run's mutable values live in ONE `class State` (st), re-initialised IN PLACE by# reset() (st is created once and never reassigned). The per-frame loop lives in a main() function --# inside a function its names resolve as fast locals instead of globals-dict lookups, a measured win on# device. st.mode runs TITLE -> PLAY -> GAMEOVER; new_game() calls st.reset() to restart. This is the# difference between a mechanic and a game, in the shape every bigger game grows into# (see /concepts/patterns/).## New vs step 7: a State object + a mode machine (TITLE/PLAY/GAMEOVER), the loop moved into main(),# new_game() reset, a centred message label, ending the run on death instead of silently restarting,# plus a confirm blip on start/menu and a low boom on game over.## Run: python3 sim/run.py tutorials/02-starship/step8_states.py --hold B --shot /tmp/p8.png
import mathimport terminalioimport picogame as pgimport picogame_gameimport picogame_inputimport picogame_clockimport picogame_shapes as shpimport picogame_poolimport picogame_ui as ui
W, H = 320, 240BACKGROUND = pg.rgb565(0, 0, 8)FRAMES = 16TITLE, PLAY, GAMEOVER = 0, 1, 2
scene, _, _ = picogame_game.setup(background=BACKGROUND)6 collapsed lines
btn = picogame_input.Buttons()clock = picogame_clock.Clock(30)
try: import picogame_audio audio = picogame_audio.Audio() snd_fire = picogame_audio.tone(880, 25) snd_boom = picogame_audio.tone(160, 90) snd_start = picogame_audio.tone(660, 40) # bright blip: start / menu confirm snd_die = picogame_audio.tone(120, 260) # low boom: game over (descending = lose)except Exception: audio = None11 collapsed lines
ship_bitmap = shp.poly_frames(18, [(0, -8), (6, 7), (0, 4), (-6, 7)], FRAMES, pg.rgb565(200, 220, 255))DIRS = [(math.sin(frame * 2 * math.pi / FRAMES), -math.cos(frame * 2 * math.pi / FRAMES)) for frame in range(FRAMES)]bullet_bitmap = shp.circle(4, pg.rgb565(255, 255, 120))ROCK_BITMAP = [shp.ring(40, pg.rgb565(170, 140, 100), 3), shp.ring(24, pg.rgb565(170, 140, 100), 3), shp.ring(13, pg.rgb565(170, 140, 100), 2)]ROCK_RADIUS = [20, 12, 6]
ship = pg.Sprite(ship_bitmap, W // 2, H // 2)ship.anchor = (0.5, 0.5)rocks = picogame_pool.Pool(scene, ROCK_BITMAP[0], 16, anchor=(0.5, 0.5))bullets = picogame_pool.Pool(scene, bullet_bitmap, 6, anchor=(0.5, 0.5))sparks = pg.Particles(160, size=2, fade=True)scene.add(sparks)scene.add(ship)hud = ui.SceneLabel(scene, pg, terminalio.FONT, 4, 4, pg.rgb565(255, 255, 255), BACKGROUND)msg = ui.SceneLabel(scene, pg, terminalio.FONT, 96, 112, pg.rgb565(255, 255, 255), BACKGROUND)
# ALL the run's mutable values in ONE object. reset() lists every field's default in one place, so a# restart is st.reset() -- and st is created ONCE below and never reassigned (a never-rebound singleton),# which is what lets main() and new_game() just mutate st.* with no `global`. Engine objects (ship, the# pools, sparks, clock, the labels) stay module-level names, NOT in State.class State: def __init__(self): self.reset()
def reset(self): self.mode = TITLE # TITLE -> PLAY -> GAMEOVER self.angle = 0 self.vx = self.vy = 0.0 self.fire_cooldown = 0 self.lives = 3 self.invincible = 0 self.wave = 3 self.score = 0
st = State()
def wrap(x, y): return x % W, y % H17 collapsed lines
def spawn_rock(size, x, y, velocity_x, velocity_y): rock = rocks.spawn() if rock is None: return rock.data = {"size": size, "velocity_x": velocity_x, "velocity_y": velocity_y} rock.bitmap = ROCK_BITMAP[size] rock.fx, rock.fy = float(x), float(y)
def new_wave(count): for i in range(count): angle_rad = i * 2 * math.pi / count spawn_rock(0, (W // 2 + int(140 * math.cos(angle_rad))) % W, (H // 2 + int(110 * math.sin(angle_rad))) % H, math.cos(angle_rad) * 1.2, math.sin(angle_rad) * 1.2)
def new_game(): st.reset() # every field back to its default, IN PLACE st.mode = PLAY st.invincible = 60 # a mercy window on the fresh run ship.fx, ship.fy = float(W // 2), float(H // 2) rocks.free_all() bullets.free_all() sparks.clear() new_wave(st.wave)
def main(): # The per-frame loop lives in a FUNCTION: its names (st, the helpers, the loop's own frame counter) # resolve as fast array-indexed locals instead of globals-dict lookups. frame = 0 while True: btn.poll() frame += 1
if st.mode == TITLE: ship.visible = False msg.set("STARSHIP PRESS A") if btn.just_pressed(btn.A): if audio: audio.sfx(snd_start) # confirm blip on start new_game() scene.refresh() clock.tick() continue
if st.mode == GAMEOVER: msg.set("GAME OVER %05d A=MENU" % st.score) if btn.just_pressed(btn.A): if audio: audio.sfx(snd_start) # confirm blip back to menu st.mode = TITLE sparks.tick() scene.refresh() clock.tick() continue
if st.mode == PLAY: msg.set(" ") st.fire_cooldown -= 1 if st.invincible > 0: st.invincible -= 1 if btn.is_pressed(btn.LEFT): st.angle = (st.angle - 1) % FRAMES if btn.is_pressed(btn.RIGHT): st.angle = (st.angle + 1) % FRAMES delta_x, delta_y = DIRS[st.angle] if btn.is_pressed(btn.UP): st.vx += delta_x * 0.25; st.vy += delta_y * 0.25 sparks.emit(ship.x - int(delta_x * 8), ship.y - int(delta_y * 8), 2, 2, 10, pg.rgb565(255, 150, 40)) speed = math.sqrt(st.vx * st.vx + st.vy * st.vy) if speed > 5: st.vx *= 5 / speed; st.vy *= 5 / speed st.vx *= 0.99; st.vy *= 0.99 ship.fx, ship.fy = wrap(ship.fx + st.vx, ship.fy + st.vy) ship.frame = st.angle ship.visible = (st.invincible <= 0) or (frame & 1)
if btn.just_pressed(btn.B) and st.fire_cooldown <= 0: bullet = bullets.spawn() if bullet: bullet.data = {"velocity_x": delta_x * 7, "velocity_y": delta_y * 7, "life": 30} bullet.move(ship.x, ship.y) st.fire_cooldown = 6 if audio: audio.sfx(snd_fire) for bullet in bullets.items: if not bullet.visible: continue bullet.data["life"] -= 1 if bullet.data["life"] <= 0: bullets.free(bullet) continue bullet.fx, bullet.fy = wrap(bullet.fx + bullet.data["velocity_x"], bullet.fy + bullet.data["velocity_y"])
for rock in rocks.items: if not rock.visible: continue rock.fx, rock.fy = wrap(rock.fx + rock.data["velocity_x"], rock.fy + rock.data["velocity_y"]) size = rock.data["size"] radius = ROCK_RADIUS[size] for bullet in bullets.items: if not bullet.visible: continue if bullet.near(rock, radius): bullets.free(bullet) rocks.free(rock) st.score += (3 - size) * 20 sparks.emit(rock.x, rock.y, 18, 3, 26, pg.rgb565(255, 200, 120)) if audio: audio.sfx(snd_boom) if size < 2: for sign in (-1, 1): spawn_rock(size + 1, rock.fx, rock.fy, rock.data["velocity_x"] + sign * 0.8, rock.data["velocity_y"] - sign * 0.8) break if st.invincible <= 0 and rock.visible and ship.near(rock, radius + 6): st.lives -= 1 sparks.emit(ship.x, ship.y, 24, 4, 30, pg.rgb565(120, 200, 255)) ship.fx, ship.fy = float(W // 2), float(H // 2) st.vx = st.vy = 0.0 st.invincible = 90 if st.lives < 0: if audio: audio.sfx(snd_die) # low boom on game over st.mode = GAMEOVER break
if rocks.count() == 0: st.wave += 1 new_wave(st.wave)
sparks.tick() hud.set("SCORE %05d SHIPS %d" % (st.score, max(0, st.lives))) scene.refresh() clock.tick()
main()Kam dál: ve webovém editoru můžeš navrhovat úrovně jako data a
načítat je pomocí picogame_scene místo ručního zadávání pozic. Strukturu dat popisuje
formát scény. Můžeš se také podívat na další tutoriály:
Tutoriál 1 — Bounce a Tutoriál 3 — Quest.