Přeskočit na obsah

Tutoriál 2 — Starship

Postavíme si malou střílečku ve stylu Asteroids. Předpokládáme, že už máš za sebou 01-bounce (render smyčka, vstup, sub-pixelový pohyb, kolize, HUD, částice). Tady přidáme věci, které tě hra s pálkou nenaučí: rotaci, vektorový tah, object pooly, dělící se nepřátele a pořádný stavový automat hry.

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.png

step 1 — step1_ship.py · tvarovaný sprite (opáčko)

Sekce “step 1 — step1_ship.py · tvarovaný sprite (opáčko)”

Starship – krok 1 shp.from_mask převede ASCII obrázek na jednobarevný bitmap. anchor=(0.5, 0.5) umístí referenční bod spritu do jeho středu — což je správná volba pro něco, co rotuje 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 pg
import picogame_game
import picogame_clock
import picogame_shapes as shp
W, H = 320, 240
BG = pg.rgb565(0, 0, 8)
scene, bufA, bufB = picogame_game.setup(background=BG)
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 centre
scene.add(ship)
while True:
scene.refresh()
clock.tick()
▶ Vyzkoušet v prohlížeči

step 2 — step2_fly.py · rotace, tah, wrap

Sekce “step 2 — step2_fly.py · rotace, tah, wrap”

Starship – krok 2 Pro loď, která se pořád otáčí, předpečeme rotaci do snímků — ostřejší a levnější než rotace za běhu (sprite.angle): shp.poly_frames(size, points, N, colour) vykreslí polygon v N úhlech do jednoho multi-frame bitmapu a ship.frame = angle zobrazí ten správný. Tabulka DIRS drží jednotkový vektor každého úhlu; UP zrychluje podél vektoru natočení do rychlosti, s omezením maximální rychlosti a jemným odporem. wrap() teleportuje přes okraje. Uvidíš: loď, kterou pilotuješ ve stylu Asteroids. Zkus: změnit tah 0.25 nebo odpor 0.99.

21 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 (vx, vy); 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 math
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_shapes as shp
W, H = 320, 240
BG = pg.rgb565(0, 0, 8)
NF = 16 # number of baked rotation frames
scene, bufA, bufB = picogame_game.setup(background=BG)
btn = picogame_input.Buttons()
clock = picogame_clock.Clock(30)
# a ship polygon (points around the centre, +y is down), baked at NF angles
ship_bm = shp.poly_frames(18, [(0, -8), (6, 7), (0, 4), (-6, 7)], NF, pg.rgb565(200, 220, 255))
# facing unit vector for each frame: frame 0 points up (-y)
DIRS = [(math.sin(f * 2 * math.pi / NF), -math.cos(f * 2 * math.pi / NF)) for f in range(NF)]
ship = pg.Sprite(ship_bm, W // 2, H // 2)
ship.anchor = (0.5, 0.5)
scene.add(ship)
ang = 0 # current rotation frame
vx = vy = 0.0 # velocity
def wrap(x, y):
return x % W, y % H
while True:
btn.poll()
if btn.is_pressed(btn.LEFT):
ang = (ang - 1) % NF
if btn.is_pressed(btn.RIGHT):
ang = (ang + 1) % NF
dx, dy = DIRS[ang]
if btn.is_pressed(btn.UP):
vx += dx * 0.25 # accelerate along the facing vector
vy += dy * 0.25
speed = math.sqrt(vx * vx + vy * vy) # cap top speed
if speed > 5:
vx *= 5 / speed
vy *= 5 / speed
vx *= 0.99 # gentle drag
vy *= 0.99
ship.fx, ship.fy = wrap(ship.fx + vx, ship.fy + vy) # sub-pixel position + wrap
ship.frame = ang # show the matching rotation
scene.refresh()
clock.tick()
▶ Vyzkoušet v prohlížeči

step 3 — step3_shoot.py · object pooly

Sekce “step 3 — step3_shoot.py · object pooly”

Starship – krok 3 Vytváření spritů za běhu žere paměť. Místo toho picogame_pool.Pool(scene, bitmap, N) jednou předalokuje N skrytých spritů; spawn() odhalí volný, free() ho schová a sprite.visible je příznak živosti. Každá střela si drží svou rychlost + zbývající život v sprite.data, pozici v fx/fy. Cooldown omezuje kadenci střelby; just_pressed(B) vystřelí při čerstvém stisku. Uvidíš: proud střel, které zaniknou. Zkus: změnit velikost poolu nebo fire_cd.

21 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 math
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_shapes as shp
import picogame_pool
W, H = 320, 240
BG = pg.rgb565(0, 0, 8)
NF = 16
scene, bufA, bufB = picogame_game.setup(background=BG)
btn = picogame_input.Buttons()
clock = picogame_clock.Clock(30)
ship_bm = shp.poly_frames(18, [(0, -8), (6, 7), (0, 4), (-6, 7)], NF, pg.rgb565(200, 220, 255))
DIRS = [(math.sin(f * 2 * math.pi / NF), -math.cos(f * 2 * math.pi / NF)) for f in range(NF)]
bullet_bm = shp.circle(4, pg.rgb565(255, 255, 120))
ship = pg.Sprite(ship_bm, W // 2, H // 2)
ship.anchor = (0.5, 0.5)
bullets = picogame_pool.Pool(scene, bullet_bm, 6, anchor=(0.5, 0.5)) # NEW: 6-bullet pool
scene.add(ship) # add ship AFTER the pool so it draws on top
ang = 0
vx = vy = 0.0
fire_cd = 0
def wrap(x, y):
return x % W, y % H
while True:
btn.poll()
fire_cd -= 1
if btn.is_pressed(btn.LEFT):
ang = (ang - 1) % NF
if btn.is_pressed(btn.RIGHT):
ang = (ang + 1) % NF
dx, dy = DIRS[ang]
if btn.is_pressed(btn.UP):
vx += dx * 0.25
vy += dy * 0.25
speed = math.sqrt(vx * vx + vy * vy)
if speed > 5:
vx *= 5 / speed; vy *= 5 / speed
vx *= 0.99; vy *= 0.99
ship.fx, ship.fy = wrap(ship.fx + vx, ship.fy + vy)
ship.frame = ang
# fire: a fresh B press, if the cooldown has elapsed and a slot is free
if btn.just_pressed(btn.B) and fire_cd <= 0:
b = bullets.spawn()
if b:
b.data = {"vx": dx * 7, "vy": dy * 7, "life": 30}
b.move(ship.x, ship.y)
fire_cd = 6
# advance live bullets; retire them when their life runs out
for b in bullets.items:
if not b.visible:
continue
b.data["life"] -= 1
if b.data["life"] <= 0:
bullets.free(b)
continue
b.fx, b.fy = wrap(b.fx + b.data["vx"], b.fy + b.data["vy"])
scene.refresh()
clock.tick()
▶ Vyzkoušet v prohlížeči

step 4 — step4_rocks.py · druhý pool + vlny

Sekce “step 4 — step4_rocks.py · druhý pool + vlny”

Starship – krok 4 Stejný vzor poolu použijeme i pro nepřátele. Kameny jsou ve 3 velikostech; velikost držíme v sprite.data a odpovídající kruhový bitmap vybíráme přes sprite.bitmap. new_wave(n) rozprostře n kamenů kolem obrazovky, každý se posunuje. Uvidíš: plovoucí prstence kamenů. Zkus: změnit počet ve vlně nebo rychlost kamenů.

19 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 math
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_shapes as shp
import picogame_pool
W, H = 320, 240
BG = pg.rgb565(0, 0, 8)
NF = 16
scene, bufA, bufB = picogame_game.setup(background=BG)
btn = picogame_input.Buttons()
clock = picogame_clock.Clock(30)
ship_bm = shp.poly_frames(18, [(0, -8), (6, 7), (0, 4), (-6, 7)], NF, pg.rgb565(200, 220, 255))
DIRS = [(math.sin(f * 2 * math.pi / NF), -math.cos(f * 2 * math.pi / NF)) for f in range(NF)]
bullet_bm = shp.circle(4, pg.rgb565(255, 255, 120))
ROCK_BM = [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_bm, W // 2, H // 2)
ship.anchor = (0.5, 0.5)
rocks = picogame_pool.Pool(scene, ROCK_BM[0], 16, anchor=(0.5, 0.5)) # NEW
bullets = picogame_pool.Pool(scene, bullet_bm, 6, anchor=(0.5, 0.5))
scene.add(ship)
ang = 0
vx = vy = 0.0
fire_cd = 0
wave = 3
def wrap(x, y):
return x % W, y % H
def spawn_rock(size, x, y, rvx, rvy):
r = rocks.spawn()
if r is None:
return
r.data = {"size": size, "vx": rvx, "vy": rvy}
r.bitmap = ROCK_BM[size] # pick the bitmap for this size
r.fx, r.fy = float(x), float(y)
def new_wave(n):
for i in range(n):
a = i * 2 * math.pi / n
spawn_rock(0, (W // 2 + int(140 * math.cos(a))) % W,
(H // 2 + int(110 * math.sin(a))) % H,
math.cos(a) * 1.2, math.sin(a) * 1.2)
new_wave(wave)
while True:
btn.poll()
fire_cd -= 1
if btn.is_pressed(btn.LEFT):
ang = (ang - 1) % NF
if btn.is_pressed(btn.RIGHT):
ang = (ang + 1) % NF
dx, dy = DIRS[ang]
if btn.is_pressed(btn.UP):
vx += dx * 0.25; vy += dy * 0.25
speed = math.sqrt(vx * vx + vy * vy)
if speed > 5:
vx *= 5 / speed; vy *= 5 / speed
vx *= 0.99; vy *= 0.99
ship.fx, ship.fy = wrap(ship.fx + vx, ship.fy + vy)
ship.frame = ang
if btn.just_pressed(btn.B) and fire_cd <= 0:
b = bullets.spawn()
if b:
b.data = {"vx": dx * 7, "vy": dy * 7, "life": 30}
b.move(ship.x, ship.y)
fire_cd = 6
for b in bullets.items:
if not b.visible:
continue
b.data["life"] -= 1
if b.data["life"] <= 0:
bullets.free(b)
continue
b.fx, b.fy = wrap(b.fx + b.data["vx"], b.fy + b.data["vy"])
# drift the rocks
for r in rocks.items:
if not r.visible:
continue
r.fx, r.fy = wrap(r.fx + r.data["vx"], r.fy + r.data["vy"])
scene.refresh()
clock.tick()
▶ Vyzkoušet v prohlížeči

step 5 — step5_collide.py · kruhové zásahy + dělení

Sekce “step 5 — step5_collide.py · kruhové zásahy + dělení”

Starship – krok 5 picogame_collide.is_within(a, b, r) je rychlý test vzdálenosti bez odmocniny, který čte pozice 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. Kámen, který doletí k lodi, stojí život, udělí krátkou nezranitelnost (i-frames, znázorněné blikáním ship.visible) a respawne. Uvidíš: že můžeš kameny rozstřílet a dostat zásah. Zkus: změnit ROCK_R nebo rychlosti dělení.

21 collapsed lines
# Starship -- step 5: shooting rocks (and getting hit).
#
# What you learn: circular collision + spawning on destruction. picogame_collide.is_within
# (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: picogame_collide.is_within, 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 math
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_shapes as shp
import picogame_pool
import picogame_collide as collide
W, H = 320, 240
BG = pg.rgb565(0, 0, 8)
NF = 16
scene, bufA, bufB = picogame_game.setup(background=BG)
btn = picogame_input.Buttons()
clock = picogame_clock.Clock(30)
ship_bm = shp.poly_frames(18, [(0, -8), (6, 7), (0, 4), (-6, 7)], NF, pg.rgb565(200, 220, 255))
DIRS = [(math.sin(f * 2 * math.pi / NF), -math.cos(f * 2 * math.pi / NF)) for f in range(NF)]
bullet_bm = shp.circle(4, pg.rgb565(255, 255, 120))
ROCK_BM = [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_R = [20, 12, 6] # collision radius per size
ship = pg.Sprite(ship_bm, W // 2, H // 2)
ship.anchor = (0.5, 0.5)
rocks = picogame_pool.Pool(scene, ROCK_BM[0], 16, anchor=(0.5, 0.5))
bullets = picogame_pool.Pool(scene, bullet_bm, 6, anchor=(0.5, 0.5))
scene.add(ship)
ang = 0
vx = vy = 0.0
fire_cd = 0
lives = 3
inv = 60 # invulnerability frames
wave = 3
frame = 0
def wrap(x, y):
return x % W, y % H
def spawn_rock(size, x, y, rvx, rvy):
r = rocks.spawn()
if r is None:
return
r.data = {"size": size, "vx": rvx, "vy": rvy}
r.bitmap = ROCK_BM[size]
r.fx, r.fy = float(x), float(y)
def new_wave(n):
for i in range(n):
a = i * 2 * math.pi / n
spawn_rock(0, (W // 2 + int(140 * math.cos(a))) % W,
(H // 2 + int(110 * math.sin(a))) % H,
math.cos(a) * 1.2, math.sin(a) * 1.2)
def respawn():
global vx, vy, inv
ship.fx, ship.fy = float(W // 2), float(H // 2)
vx = vy = 0.0
inv = 90
new_wave(wave)
while True:
btn.poll()
frame += 1
fire_cd -= 1
if inv > 0:
inv -= 1
if btn.is_pressed(btn.LEFT):
ang = (ang - 1) % NF
if btn.is_pressed(btn.RIGHT):
ang = (ang + 1) % NF
dx, dy = DIRS[ang]
if btn.is_pressed(btn.UP):
vx += dx * 0.25; vy += dy * 0.25
speed = math.sqrt(vx * vx + vy * vy)
if speed > 5:
vx *= 5 / speed; vy *= 5 / speed
vx *= 0.99; vy *= 0.99
ship.fx, ship.fy = wrap(ship.fx + vx, ship.fy + vy)
ship.frame = ang
ship.visible = (inv <= 0) or (frame & 1) # blink while invulnerable
if btn.just_pressed(btn.B) and fire_cd <= 0:
b = bullets.spawn()
if b:
b.data = {"vx": dx * 7, "vy": dy * 7, "life": 30}
b.move(ship.x, ship.y)
fire_cd = 6
for b in bullets.items:
if not b.visible:
continue
b.data["life"] -= 1
if b.data["life"] <= 0:
bullets.free(b)
continue
b.fx, b.fy = wrap(b.fx + b.data["vx"], b.fy + b.data["vy"])
for r in rocks.items:
if not r.visible:
continue
r.fx, r.fy = wrap(r.fx + r.data["vx"], r.fy + r.data["vy"])
size = r.data["size"]
rr = ROCK_R[size]
# bullet hits this rock?
for b in bullets.items:
if not b.visible:
continue
if collide.is_within(b, r, rr):
bullets.free(b)
rocks.free(r)
if size < 2: # split into two smaller, flying apart
for s in (-1, 1):
spawn_rock(size + 1, r.fx, r.fy,
r.data["vx"] + s * 0.8, r.data["vy"] - s * 0.8)
break
# rock reaches the ship?
if inv <= 0 and r.visible and collide.is_within(ship, r, rr + 6):
lives -= 1
respawn()
if lives < 0:
lives = 3
rocks.free_all()
new_wave(wave)
break
scene.refresh()
clock.tick()
▶ Vyzkoušet v prohlížeči

step 6 — step6_waves.py · skóre + progrese

Sekce “step 6 — step6_waves.py · skóre + progrese”

Starship – krok 6 Menší kameny dávají víc bodů. SceneLabel ukazuje skóre + lodě. Když je rocks.count() == 0, je pole čisté, takže spustíme další, větší vlnu — hra graduje. 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 math
import terminalio
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_shapes as shp
import picogame_pool
import picogame_collide as collide
import picogame_ui as ui
W, H = 320, 240
BG = pg.rgb565(0, 0, 8)
NF = 16
scene, bufA, bufB = picogame_game.setup(background=BG)
btn = picogame_input.Buttons()
clock = picogame_clock.Clock(30)
ship_bm = shp.poly_frames(18, [(0, -8), (6, 7), (0, 4), (-6, 7)], NF, pg.rgb565(200, 220, 255))
DIRS = [(math.sin(f * 2 * math.pi / NF), -math.cos(f * 2 * math.pi / NF)) for f in range(NF)]
bullet_bm = shp.circle(4, pg.rgb565(255, 255, 120))
ROCK_BM = [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_R = [20, 12, 6]
ship = pg.Sprite(ship_bm, W // 2, H // 2)
ship.anchor = (0.5, 0.5)
rocks = picogame_pool.Pool(scene, ROCK_BM[0], 16, anchor=(0.5, 0.5))
bullets = picogame_pool.Pool(scene, bullet_bm, 6, anchor=(0.5, 0.5))
scene.add(ship)
hud = ui.SceneLabel(scene, pg, terminalio.FONT, 4, 4, pg.rgb565(255, 255, 255), BG)
ang = 0
vx = vy = 0.0
fire_cd = 0
lives = 3
inv = 60
wave = 3
score = 0
frame = 0
def wrap(x, y):
return x % W, y % H
def spawn_rock(size, x, y, rvx, rvy):
r = rocks.spawn()
if r is None:
return
r.data = {"size": size, "vx": rvx, "vy": rvy}
r.bitmap = ROCK_BM[size]
r.fx, r.fy = float(x), float(y)
def new_wave(n):
for i in range(n):
a = i * 2 * math.pi / n
spawn_rock(0, (W // 2 + int(140 * math.cos(a))) % W,
(H // 2 + int(110 * math.sin(a))) % H,
math.cos(a) * 1.2, math.sin(a) * 1.2)
def respawn():
global vx, vy, inv
ship.fx, ship.fy = float(W // 2), float(H // 2)
vx = vy = 0.0
inv = 90
new_wave(wave)
while True:
btn.poll()
frame += 1
fire_cd -= 1
if inv > 0:
inv -= 1
if btn.is_pressed(btn.LEFT):
ang = (ang - 1) % NF
if btn.is_pressed(btn.RIGHT):
ang = (ang + 1) % NF
dx, dy = DIRS[ang]
if btn.is_pressed(btn.UP):
vx += dx * 0.25; vy += dy * 0.25
speed = math.sqrt(vx * vx + vy * vy)
if speed > 5:
vx *= 5 / speed; vy *= 5 / speed
vx *= 0.99; vy *= 0.99
ship.fx, ship.fy = wrap(ship.fx + vx, ship.fy + vy)
ship.frame = ang
ship.visible = (inv <= 0) or (frame & 1)
if btn.just_pressed(btn.B) and fire_cd <= 0:
b = bullets.spawn()
if b:
b.data = {"vx": dx * 7, "vy": dy * 7, "life": 30}
b.move(ship.x, ship.y)
fire_cd = 6
for b in bullets.items:
if not b.visible:
continue
b.data["life"] -= 1
if b.data["life"] <= 0:
bullets.free(b)
continue
b.fx, b.fy = wrap(b.fx + b.data["vx"], b.fy + b.data["vy"])
for r in rocks.items:
if not r.visible:
continue
r.fx, r.fy = wrap(r.fx + r.data["vx"], r.fy + r.data["vy"])
size = r.data["size"]
rr = ROCK_R[size]
for b in bullets.items:
if not b.visible:
continue
if collide.is_within(b, r, rr):
bullets.free(b)
rocks.free(r)
score += (3 - size) * 20 # smaller rock -> more points
if size < 2:
for s in (-1, 1):
spawn_rock(size + 1, r.fx, r.fy,
r.data["vx"] + s * 0.8, r.data["vy"] - s * 0.8)
break
if inv <= 0 and r.visible and collide.is_within(ship, r, rr + 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()
▶ Vyzkoušet v prohlížeči

step 7 — step7_particles.py · exploze, výfuk, zvuk

Sekce “step 7 — step7_particles.py · exploze, výfuk, zvuk”

Starship – krok 7 Jeden systém Particles(fade=True) slouží dvěma efektům: výbuchu při zničení kamene (spousta rychlých, mizejících jisker) a plamenu tahu (pár krátkých jisker za lodí každý snímek, dokud tlačíš). fade=True ztmavuje částice, jak stárnou. tone() dá 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 math
import terminalio
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_shapes as shp
import picogame_pool
import picogame_collide as collide
import picogame_ui as ui
W, H = 320, 240
BG = pg.rgb565(0, 0, 8)
NF = 16
scene, bufA, bufB = picogame_game.setup(background=BG)
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_bm = shp.poly_frames(18, [(0, -8), (6, 7), (0, 4), (-6, 7)], NF, pg.rgb565(200, 220, 255))
DIRS = [(math.sin(f * 2 * math.pi / NF), -math.cos(f * 2 * math.pi / NF)) for f in range(NF)]
bullet_bm = shp.circle(4, pg.rgb565(255, 255, 120))
ROCK_BM = [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_R = [20, 12, 6]
ship = pg.Sprite(ship_bm, W // 2, H // 2)
ship.anchor = (0.5, 0.5)
rocks = picogame_pool.Pool(scene, ROCK_BM[0], 16, anchor=(0.5, 0.5))
bullets = picogame_pool.Pool(scene, bullet_bm, 6, anchor=(0.5, 0.5))
sparks = pg.Particles(160, size=2, fade=True) # NEW
scene.add(sparks)
scene.add(ship)
hud = ui.SceneLabel(scene, pg, terminalio.FONT, 4, 4, pg.rgb565(255, 255, 255), BG)
ang = 0
vx = vy = 0.0
fire_cd = 0
lives = 3
inv = 60
wave = 3
score = 0
frame = 0
def wrap(x, y):
return x % W, y % H
def spawn_rock(size, x, y, rvx, rvy):
r = rocks.spawn()
if r is None:
return
r.data = {"size": size, "vx": rvx, "vy": rvy}
r.bitmap = ROCK_BM[size]
r.fx, r.fy = float(x), float(y)
def new_wave(n):
for i in range(n):
a = i * 2 * math.pi / n
spawn_rock(0, (W // 2 + int(140 * math.cos(a))) % W,
(H // 2 + int(110 * math.sin(a))) % H,
math.cos(a) * 1.2, math.sin(a) * 1.2)
def respawn():
global vx, vy, inv
ship.fx, ship.fy = float(W // 2), float(H // 2)
vx = vy = 0.0
inv = 90
new_wave(wave)
while True:
btn.poll()
frame += 1
fire_cd -= 1
if inv > 0:
inv -= 1
if btn.is_pressed(btn.LEFT):
ang = (ang - 1) % NF
if btn.is_pressed(btn.RIGHT):
ang = (ang + 1) % NF
dx, dy = DIRS[ang]
if btn.is_pressed(btn.UP):
vx += dx * 0.25; vy += dy * 0.25
# thrust flame: a couple of orange sparks shot out the back
sparks.emit(ship.x - int(dx * 8), ship.y - int(dy * 8), 2, 2, 10, pg.rgb565(255, 150, 40))
speed = math.sqrt(vx * vx + vy * vy)
if speed > 5:
vx *= 5 / speed; vy *= 5 / speed
vx *= 0.99; vy *= 0.99
ship.fx, ship.fy = wrap(ship.fx + vx, ship.fy + vy)
ship.frame = ang
ship.visible = (inv <= 0) or (frame & 1)
if btn.just_pressed(btn.B) and fire_cd <= 0:
b = bullets.spawn()
if b:
b.data = {"vx": dx * 7, "vy": dy * 7, "life": 30}
b.move(ship.x, ship.y)
fire_cd = 6
if audio:
audio.sfx(snd_fire)
for b in bullets.items:
if not b.visible:
continue
b.data["life"] -= 1
if b.data["life"] <= 0:
bullets.free(b)
continue
b.fx, b.fy = wrap(b.fx + b.data["vx"], b.fy + b.data["vy"])
for r in rocks.items:
if not r.visible:
continue
r.fx, r.fy = wrap(r.fx + r.data["vx"], r.fy + r.data["vy"])
size = r.data["size"]
rr = ROCK_R[size]
for b in bullets.items:
if not b.visible:
continue
if collide.is_within(b, r, rr):
bullets.free(b)
rocks.free(r)
score += (3 - size) * 20
sparks.emit(r.x, r.y, 18, 3, 26, pg.rgb565(255, 200, 120)) # explosion
if audio:
audio.sfx(snd_boom)
if size < 2:
for s in (-1, 1):
spawn_rock(size + 1, r.fx, r.fy,
r.data["vx"] + s * 0.8, r.data["vy"] - s * 0.8)
break
if inv <= 0 and r.visible and collide.is_within(ship, r, rr + 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()
▶ Vyzkoušet v prohlížeči

step 8 — step8_states.py · stavový automat (vrchol)

Sekce “step 8 — step8_states.py · stavový automat (vrchol)”

Starship – krok 8 Hotová hra není jedna nekonečná smyčka — má stavy. Sledujeme state: TITLE čeká na stisk, PLAY běží hra, GAMEOVER ukáže finální skóre a vrátí se na titulek. new_game() resetuje všechno; jeden vycentrovaný SceneLabel zobrazuje zprávu pro stavy mimo hru; smrt ukončí hru místo tichého restartu. Tohle je to, co z mechaniky dělá hru. Uvidíš: titulek → hra → game over → titulek. Zkus: přidat high score, které přežije napříč hrami (viz picogame_save pro perzistenci v NVM).

23 collapsed lines
# Starship -- step 8: game states (title / playing / game over) + restart.
#
# What you learn: a state machine, the backbone of a finished game. Instead of one
# endless loop, we track a `state`: TITLE waits for a press to start, PLAY runs the
# game, GAMEOVER shows the final score and waits to return to the title. new_game()
# resets everything. A single SceneLabel shows the centred message for the non-PLAY
# states. This is the difference between a mechanic and a game.
#
# New vs step 7: a `state` variable + transitions, new_game() reset, a centred
# message label, ending the run on death instead of silently restarting.
#
# Run: python3 sim/run.py tutorials/02-starship/step8_states.py --hold B --shot /tmp/p8.png
import math
import terminalio
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_shapes as shp
import picogame_pool
import picogame_collide as collide
import picogame_ui as ui
W, H = 320, 240
BG = pg.rgb565(0, 0, 8)
NF = 16
TITLE, PLAY, GAMEOVER = 0, 1, 2
scene, bufA, bufB = picogame_game.setup(background=BG)
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_bm = shp.poly_frames(18, [(0, -8), (6, 7), (0, 4), (-6, 7)], NF, pg.rgb565(200, 220, 255))
DIRS = [(math.sin(f * 2 * math.pi / NF), -math.cos(f * 2 * math.pi / NF)) for f in range(NF)]
bullet_bm = shp.circle(4, pg.rgb565(255, 255, 120))
ROCK_BM = [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_R = [20, 12, 6]
ship = pg.Sprite(ship_bm, W // 2, H // 2)
ship.anchor = (0.5, 0.5)
rocks = picogame_pool.Pool(scene, ROCK_BM[0], 16, anchor=(0.5, 0.5))
bullets = picogame_pool.Pool(scene, bullet_bm, 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), BG)
msg = ui.SceneLabel(scene, pg, terminalio.FONT, 96, 112, pg.rgb565(255, 255, 255), BG)
state = TITLE
ang = 0
vx = vy = 0.0
fire_cd = 0
lives = 3
inv = 0
wave = 3
score = 0
frame = 0
def wrap(x, y):
return x % W, y % H
def spawn_rock(size, x, y, rvx, rvy):
r = rocks.spawn()
if r is None:
return
r.data = {"size": size, "vx": rvx, "vy": rvy}
r.bitmap = ROCK_BM[size]
r.fx, r.fy = float(x), float(y)
def new_wave(n):
for i in range(n):
a = i * 2 * math.pi / n
spawn_rock(0, (W // 2 + int(140 * math.cos(a))) % W,
(H // 2 + int(110 * math.sin(a))) % H,
math.cos(a) * 1.2, math.sin(a) * 1.2)
def new_game():
global ang, vx, vy, fire_cd, lives, inv, wave, score
ang = 0; vx = vy = 0.0; fire_cd = 0; lives = 3; inv = 60; wave = 3; score = 0
ship.fx, ship.fy = float(W // 2), float(H // 2)
rocks.free_all()
bullets.free_all()
sparks.clear()
new_wave(wave)
while True:
btn.poll()
frame += 1
if state == TITLE:
ship.visible = False
msg.set("STARSHIP PRESS A")
if btn.just_pressed(btn.A) or btn.just_pressed(btn.B):
new_game()
state = PLAY
scene.refresh()
clock.tick()
continue
if state == GAMEOVER:
msg.set("GAME OVER %05d A=MENU" % score)
if btn.just_pressed(btn.A) or btn.just_pressed(btn.B):
state = TITLE
sparks.tick()
scene.refresh()
clock.tick()
continue
# ---- state == PLAY ----
msg.set(" ")
fire_cd -= 1
if inv > 0:
inv -= 1
if btn.is_pressed(btn.LEFT):
ang = (ang - 1) % NF
if btn.is_pressed(btn.RIGHT):
ang = (ang + 1) % NF
dx, dy = DIRS[ang]
if btn.is_pressed(btn.UP):
vx += dx * 0.25; vy += dy * 0.25
sparks.emit(ship.x - int(dx * 8), ship.y - int(dy * 8), 2, 2, 10, pg.rgb565(255, 150, 40))
speed = math.sqrt(vx * vx + vy * vy)
if speed > 5:
vx *= 5 / speed; vy *= 5 / speed
vx *= 0.99; vy *= 0.99
ship.fx, ship.fy = wrap(ship.fx + vx, ship.fy + vy)
ship.frame = ang
ship.visible = (inv <= 0) or (frame & 1)
if btn.just_pressed(btn.B) and fire_cd <= 0:
b = bullets.spawn()
if b:
b.data = {"vx": dx * 7, "vy": dy * 7, "life": 30}
b.move(ship.x, ship.y)
fire_cd = 6
if audio:
audio.sfx(snd_fire)
for b in bullets.items:
if not b.visible:
continue
b.data["life"] -= 1
if b.data["life"] <= 0:
bullets.free(b)
continue
b.fx, b.fy = wrap(b.fx + b.data["vx"], b.fy + b.data["vy"])
for r in rocks.items:
if not r.visible:
continue
r.fx, r.fy = wrap(r.fx + r.data["vx"], r.fy + r.data["vy"])
size = r.data["size"]
rr = ROCK_R[size]
for b in bullets.items:
if not b.visible:
continue
if collide.is_within(b, r, rr):
bullets.free(b)
rocks.free(r)
score += (3 - size) * 20
sparks.emit(r.x, r.y, 18, 3, 26, pg.rgb565(255, 200, 120))
if audio:
audio.sfx(snd_boom)
if size < 2:
for s in (-1, 1):
spawn_rock(size + 1, r.fx, r.fy,
r.data["vx"] + s * 0.8, r.data["vy"] - s * 0.8)
break
if inv <= 0 and r.visible and collide.is_within(ship, r, rr + 6):
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)
vx = vy = 0.0
inv = 90
if lives < 0:
state = GAMEOVER
break
if rocks.count() == 0:
wave += 1
new_wave(wave)
sparks.tick()
hud.set("SCORE %05d SHIPS %d" % (score, max(0, lives)))
scene.refresh()
clock.tick()
▶ Vyzkoušet v prohlížeči

Kam dál: editor + picogame_scene — navrhuj úrovně jako data a načítej je, místo ručního zadávání pozic. Mrkni na webový editor a formát scény. A pokud sis ještě neprošel sourozenecké tutoriály, podívej se na Tutoriál 1 — Bounce a Tutoriál 3 — Quest.