Přeskočit na obsah

Tutoriál 1 — Bounce

Postavíme si kompletní brick-breaker, jeden herní mechanismus na každý krok. Každý stepN_*.py běží samostatně. Kterýkoli z nich spustíš takto:

python3 sim/run.py tutorials/01-bounce/stepN_name.py --shot /tmp/out.png

(přidej --hold RIGHT apod. pro podržení tlačítka, nebo --backend pygame, ať si to zahraješ naživo.)

Celý zdrojový kód najdeš na GitHubu.

Pointa posledního kroku je pointa celého tutoriálu: celou hru jsme postavili z barevných obdélníků a její proměna ve „skutečnou” grafiku je výměna jednoho řádku s bitmapou. Grafika je nezávislá na mechanikách — proto taky editor umí hru přeskinovat, aniž by sáhl do jejího kódu.


step 1 — step1_hello.py · vykreslovací smyčka

Sekce “step 1 — step1_hello.py · vykreslovací smyčka”

Bounce – krok 1 picogame je retained-mode: objekty jednou přidáš přes scene.add(), pak každý snímek měníš jejich stav a zavoláš scene.refresh() — engine překreslí. „Pádlo” je Sprite, jehož bitmapa je plný obdélník z shp.rect(w, h, colour). picogame_game.setup() udělá všechnu otravnou práci kolem displeje; picogame_clock.Clock(40) omezí smyčku na 40 FPS. Uvidíš: šedý pruh u spodního okraje. Zkus si: změnit velikost/barvu obdélníku.

11 collapsed lines
# Bounce -- step 1: get ONE thing on screen.
#
# What you learn: the picogame render loop. A game is (a) a Scene you add objects
# to ONCE, then (b) a loop that moves things and calls scene.refresh(). The engine
# is retained-mode: you don't redraw by hand, you change object state and refresh.
#
# New in this step: picogame_game.setup(), picogame_shapes.rect(), pg.Sprite,
# scene.add(), scene.refresh(), the frame clock.
#
# Run it: python3 sim/run.py tutorials/01-bounce/step1_hello.py --shot /tmp/s1.png
# On device: copy this file + the lib/ helpers to CIRCUITPY.
import picogame as pg
import picogame_game
import picogame_clock
import picogame_shapes as shp
W, H = 320, 240
PADDLE_W, PADDLE_H = 44, 8
# setup() takes over the display and gives us a Scene + its two strip buffers.
scene, bufA, bufB = picogame_game.setup(background=pg.rgb565(8, 10, 24))
clock = picogame_clock.Clock(40) # cap the loop to 40 FPS
# A "paddle" is just a Sprite whose bitmap is a solid rectangle. shp.rect(w,h,color)
# makes that bitmap -- a rectangle and an image sprite are the SAME kind of object
# (we'll prove that in step 9 by swapping the bitmap for art, with no other change).
paddle = pg.Sprite(shp.rect(PADDLE_W, PADDLE_H, pg.rgb565(220, 220, 230)),
(W - PADDLE_W) // 2, H - 16)
scene.add(paddle) # add it to the scene ONCE
while True:
scene.refresh() # the engine draws the scene
clock.tick() # sleep to the next frame
▶ Vyzkoušet v prohlížeči

step 2 — step2_move.py · vstup

Sekce “step 2 — step2_move.py · vstup”

Bounce – krok 2 picogame_input.Buttons() při každém poll() načte stav tlačítek. is_pressed(RIGHT) - is_pressed(LEFT) je úhledná osa −1/0/+1; pádlo posuneme a omezíme ho dovnitř obrazovky. Uvidíš: pádlo jezdí pomocí LEFT/RIGHT. Zkus si: změnit SPEED.

10 collapsed lines
# Bounce -- step 2: move the paddle with the buttons.
#
# What you learn: input. picogame_input.Buttons reads the board's buttons into a
# bitmask each frame; btn.is_pressed(btn.LEFT) is the held state. We move the paddle
# and clamp it to the screen so it can't leave.
#
# New vs step 1: picogame_input.Buttons, btn.poll()/btn.is_pressed(), sprite.move(),
# clamping with max()/min().
#
# Run: python3 sim/run.py tutorials/01-bounce/step2_move.py --hold RIGHT --shot /tmp/s2.png
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_shapes as shp
W, H = 320, 240
PADDLE_W, PADDLE_H = 44, 8
SPEED = 5
scene, bufA, bufB = picogame_game.setup(background=pg.rgb565(8, 10, 24))
btn = picogame_input.Buttons() # NEW: the buttons
clock = picogame_clock.Clock(40)
paddle = pg.Sprite(shp.rect(PADDLE_W, PADDLE_H, pg.rgb565(220, 220, 230)),
(W - PADDLE_W) // 2, H - 16)
scene.add(paddle)
while True:
btn.poll() # sample the buttons once per frame
# RIGHT minus LEFT gives -1 / 0 / +1 -- a tidy way to read a 1-axis control.
dx = btn.is_pressed(btn.RIGHT) - btn.is_pressed(btn.LEFT)
if dx:
x = paddle.x + dx * SPEED
x = max(0, min(W - PADDLE_W, x)) # clamp inside the screen
paddle.move(x, paddle.y)
scene.refresh()
clock.tick()
▶ Vyzkoušet v prohlížeči

step 3 — step3_ball.py · setrvačnost (sub-pixel)

Sekce “step 3 — step3_ball.py · setrvačnost (sub-pixel)”

Bounce – krok 3 Sprite ukládá svou pozici jako fixed-point, navenek vystavenou jako sprite.fx/sprite.fy (floaty). Přičti k fx/fy každý snímek rychlost a míček plyne hladce — i při rychlostech pod 1 px/snímek, které celočíselné souřadnice neumí vyjádřit. sprite.x/.y jsou zaokrouhlené pixely, na které engine kreslí. Uvidíš: míček odletí mimo obrazovku (to opravíme v dalším kroku). Zkus si: změnit vx, vy.

18 collapsed lines
# Bounce -- step 3: a ball with momentum (sub-pixel movement).
#
# What you learn: velocity + sub-pixel position. A Sprite stores its position as
# fixed-point, exposed as sprite.fx / sprite.fy (floats). Add a velocity to fx/fy
# every frame and the ball drifts smoothly -- even at speeds below 1 px/frame,
# which plain integer x/y could not represent. sprite.x / sprite.y are the rounded
# pixel coordinates the engine draws at.
#
# New vs step 2: sprite.fx/.fy (sub-pixel position), a velocity (vx, vy). The ball
# flies off-screen for now -- step 4 makes it bounce.
#
# Run: python3 sim/run.py tutorials/01-bounce/step3_ball.py --shot /tmp/s3.png
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_shapes as shp
W, H = 320, 240
PADDLE_W, PADDLE_H = 44, 8
BALL = 6
scene, bufA, bufB = picogame_game.setup(background=pg.rgb565(8, 10, 24))
btn = picogame_input.Buttons()
clock = picogame_clock.Clock(40)
paddle = pg.Sprite(shp.rect(PADDLE_W, PADDLE_H, pg.rgb565(220, 220, 230)),
(W - PADDLE_W) // 2, H - 16)
ball = pg.Sprite(shp.rect(BALL, BALL, pg.rgb565(255, 240, 120)), W // 2, H // 2)
scene.add(paddle)
scene.add(ball)
vx, vy = 2.4, -2.6 # NEW: the ball's velocity (px per frame)
while True:
btn.poll()
dx = btn.is_pressed(btn.RIGHT) - btn.is_pressed(btn.LEFT)
if dx:
paddle.move(max(0, min(W - PADDLE_W, paddle.x + dx * 5)), paddle.y)
# integrate velocity into the ball's sub-pixel position
ball.fx += vx
ball.fy += vy
scene.refresh()
clock.tick()
▶ Vyzkoušet v prohlížeči

step 4 — step4_walls.py · odraz

Sekce “step 4 — step4_walls.py · odraz”

Bounce – krok 4 Odraz je jen překlopení té složky rychlosti, která míří do zdi, plus přilepení míčku k okraji, aby skrz něj nepropadl. Levá/pravá překlápí vx, horní překlápí vy. Spodek zůstává otevřený — propadnutí pod něj je „miss”. Uvidíš: míček se navždy odráží mezi třemi zdmi. Zkus si: udělat otevřený i vršek a sleduj, jak uteče.

16 collapsed lines
# Bounce -- step 4: bounce off the walls.
#
# What you learn: reflection. A bounce is just flipping the velocity component that
# points into the wall, and pinning the position back to the edge so the ball can't
# tunnel out. Left/right flip vx; the top flips vy. We leave the BOTTOM open -- a
# ball that falls past it is a missed ball (step 5 turns that into "lose a life").
#
# New vs step 3: edge tests against ball.x/.y, inverting vx/vy on contact.
#
# Run: python3 sim/run.py tutorials/01-bounce/step4_walls.py --shot /tmp/s4.png
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_shapes as shp
W, H = 320, 240
PADDLE_W, PADDLE_H = 44, 8
BALL = 6
scene, bufA, bufB = picogame_game.setup(background=pg.rgb565(8, 10, 24))
btn = picogame_input.Buttons()
clock = picogame_clock.Clock(40)
paddle = pg.Sprite(shp.rect(PADDLE_W, PADDLE_H, pg.rgb565(220, 220, 230)),
(W - PADDLE_W) // 2, H - 16)
ball = pg.Sprite(shp.rect(BALL, BALL, pg.rgb565(255, 240, 120)), W // 2, H // 2)
scene.add(paddle)
scene.add(ball)
vx, vy = 2.4, -2.6
while True:
btn.poll()
dx = btn.is_pressed(btn.RIGHT) - btn.is_pressed(btn.LEFT)
if dx:
paddle.move(max(0, min(W - PADDLE_W, paddle.x + dx * 5)), paddle.y)
ball.fx += vx
ball.fy += vy
# walls: flip the component heading into the wall, and pin to the edge
if ball.fx < 0:
ball.fx = 0
vx = -vx
elif ball.fx > W - BALL:
ball.fx = W - BALL
vx = -vx
if ball.fy < 0:
ball.fy = 0
vy = -vy
scene.refresh()
clock.tick()
▶ Vyzkoušet v prohlížeči

step 5 — step5_paddle.py · box kolize + pocit ze hry

Sekce “step 5 — step5_paddle.py · box kolize + pocit ze hry”

Bounce – krok 5 pg.collide(ax1,ay1,ax2,ay2, bx1,by1,bx2,by2) je rychlý AABB test překryvu. Při zásahu pádla (jen když míček letí dolů) ho pošleme nahoru a poťukneme vx podle toho, kam na pádlo dopadl, takže můžeš mířit. Propadnutí pod spodek stojí život a znovu naservíruje míček. Uvidíš: opravdovou výměnu, kterou udržíš v běhu. Zkus si: změnit řídicí faktor 0.06.

16 collapsed lines
# Bounce -- step 5: the paddle hits the ball, and you can miss.
#
# What you learn: box collision + a control feel trick. pg.collide(ax1,ay1,ax2,ay2,
# bx1,by1,bx2,by2) is a fast axis-aligned overlap test. On a paddle hit we send the
# ball upward, and nudge vx by WHERE on the paddle it landed -- so you can aim. If
# the ball falls below the screen it's a miss: lose a life and re-serve.
#
# New vs step 4: pg.collide, steering the bounce by hit offset, lives + reset.
#
# Run: python3 sim/run.py tutorials/01-bounce/step5_paddle.py --hold LEFT --shot /tmp/s5.png
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_shapes as shp
W, H = 320, 240
PADDLE_W, PADDLE_H = 44, 8
BALL = 6
scene, bufA, bufB = picogame_game.setup(background=pg.rgb565(8, 10, 24))
btn = picogame_input.Buttons()
clock = picogame_clock.Clock(40)
paddle = pg.Sprite(shp.rect(PADDLE_W, PADDLE_H, pg.rgb565(220, 220, 230)),
(W - PADDLE_W) // 2, H - 16)
ball = pg.Sprite(shp.rect(BALL, BALL, pg.rgb565(255, 240, 120)), W // 2, H // 2)
scene.add(paddle)
scene.add(ball)
vx, vy = 2.4, -2.6
lives = 3
def serve():
global vx, vy
ball.move(W // 2, H // 2)
vx, vy = 2.4, -2.6
while True:
btn.poll()
dx = btn.is_pressed(btn.RIGHT) - btn.is_pressed(btn.LEFT)
if dx:
paddle.move(max(0, min(W - PADDLE_W, paddle.x + dx * 5)), paddle.y)
ball.fx += vx
ball.fy += vy
if ball.fx < 0:
ball.fx = 0; vx = -vx
elif ball.fx > W - BALL:
ball.fx = W - BALL; vx = -vx
if ball.fy < 0:
ball.fy = 0; vy = -vy
# paddle bounce: only when moving DOWN and the boxes overlap
if vy > 0 and pg.collide(ball.x, ball.y, ball.x + BALL, ball.y + BALL,
paddle.x, paddle.y, paddle.x + PADDLE_W, paddle.y + PADDLE_H):
vy = -abs(vy)
# steer: distance of ball centre from paddle centre -> sideways speed
vx += (ball.x + BALL / 2 - (paddle.x + PADDLE_W / 2)) * 0.06
if ball.fy > H: # missed the ball
lives -= 1
if lives <= 0:
lives = 3
serve()
scene.refresh()
clock.tick()
▶ Vyzkoušet v prohlížeči

step 6 — step6_bricks.py · Tilemap

Sekce “step 6 — step6_bricks.py · Tilemap”

Bounce – krok 6 Tilemap je mřížka založená na jedné tileset bitmapě, 1 bajt na buňku — mnohem levnější než sprite na každou cihlu. shp.tileset_colors(w, h, [colours]) postaví list, kde hodnota 0 je prázdno a 1..N jsou barvy. Namapuj pixel míčku na dlaždici (tx = px // BW), přečti ji a nastav ji na 0, čímž ji smažeš. Smaž celou zeď → poskládej znovu. Uvidíš: zeď 10×6, kterou rozbíjíš. Zkus si: změnit ROWS nebo barvy cihel.

17 collapsed lines
# Bounce -- step 6: a wall of bricks (a Tilemap).
#
# What you learn: the Tilemap. A grid of tiles backed by ONE bitmap (a tileset),
# stored as 1 byte per cell -- far cheaper than a Sprite per brick. We build the
# tileset with shp.tileset_colors (frame 0 = empty, 1..4 = colours), fill the grid,
# and on a ball hit we find the tile under the ball, read it, and set it to 0 to
# clear it. Map a pixel to a tile with tx = (px - origin_x) // tile_w.
#
# New vs step 5: pg.Tilemap, shp.tileset_colors, pixel->tile mapping, clearing a tile.
#
# Run: python3 sim/run.py tutorials/01-bounce/step6_bricks.py --shot /tmp/s6.png
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_shapes as shp
W, H = 320, 240
PADDLE_W, PADDLE_H = 44, 8
BALL = 6
BW, BH = 32, 16 # brick (tile) size
COLS, ROWS = W // BW, 6 # 10 x 6 wall
BRICK_Y = 28 # wall top (leaves a HUD strip)
scene, bufA, bufB = picogame_game.setup(background=pg.rgb565(8, 10, 24))
btn = picogame_input.Buttons()
clock = picogame_clock.Clock(40)
# tileset: value 0 empty, 1..4 = four brick colours
brick_colors = [pg.rgb565(220, 70, 70), pg.rgb565(230, 150, 50),
pg.rgb565(70, 200, 90), pg.rgb565(80, 150, 230)]
bricks = pg.Tilemap(shp.tileset_colors(BW, BH, brick_colors), COLS, ROWS)
bricks.move(0, BRICK_Y)
def build_wall():
global bricks_left
for ty in range(ROWS):
for tx in range(COLS):
bricks.tile(tx, ty, 1 + (ty % 4)) # row -> colour 1..4
bricks_left = COLS * ROWS
build_wall()
paddle = pg.Sprite(shp.rect(PADDLE_W, PADDLE_H, pg.rgb565(220, 220, 230)),
(W - PADDLE_W) // 2, H - 16)
ball = pg.Sprite(shp.rect(BALL, BALL, pg.rgb565(255, 240, 120)), W // 2, H // 2)
scene.add(bricks) # add the wall first (drawn under the ball)
scene.add(paddle)
scene.add(ball)
vx, vy = 2.4, -2.6
lives = 3
def serve():
global vx, vy
ball.move(W // 2, H // 2)
vx, vy = 2.4, -2.6
while True:
btn.poll()
dx = btn.is_pressed(btn.RIGHT) - btn.is_pressed(btn.LEFT)
if dx:
paddle.move(max(0, min(W - PADDLE_W, paddle.x + dx * 5)), paddle.y)
ball.fx += vx
ball.fy += vy
if ball.fx < 0:
ball.fx = 0; vx = -vx
elif ball.fx > W - BALL:
ball.fx = W - BALL; vx = -vx
if ball.fy < 0:
ball.fy = 0; vy = -vy
if vy > 0 and pg.collide(ball.x, ball.y, ball.x + BALL, ball.y + BALL,
paddle.x, paddle.y, paddle.x + PADDLE_W, paddle.y + PADDLE_H):
vy = -abs(vy)
vx += (ball.x + BALL / 2 - (paddle.x + PADDLE_W / 2)) * 0.06
# brick hit: the tile under the ball's centre
cx, cy = ball.x + BALL // 2, ball.y + BALL // 2
tx = cx // BW
ty = (cy - BRICK_Y) // BH
if 0 <= tx < COLS and 0 <= ty < ROWS and bricks.tile(tx, ty):
bricks.tile(tx, ty, 0) # clear the brick
bricks_left -= 1
vy = -vy
if bricks_left == 0: # cleared the wall -> rebuild
build_wall()
serve()
if ball.fy > H:
lives -= 1
if lives <= 0:
lives = 3
build_wall()
serve()
scene.refresh()
clock.tick()
▶ Vyzkoušet v prohlížeči

step 7 — step7_hud.py · text / stavová lišta

Sekce “step 7 — step7_hud.py · text / stavová lišta”

Bounce – krok 7 picogame_ui.SceneLabel vykreslí text do scény jako fixní vrstvu (kreslí ji refresh() a je nezávislá na kameře — hodí se, jakmile se svět začne posouvat). Používá přibalený terminalio.FONT, takže žádný font jako asset. label.set(...) překreslí jen tehdy, když se text změní. Uvidíš: SCORE / LIVES přes horní okraj. Zkus si: přidat počet cihel.

20 collapsed lines
# Bounce -- step 7: a score + lives status bar.
#
# What you learn: text / HUD. picogame_ui.SceneLabel renders text into the scene as a
# "fixed" layer -- it's drawn by scene.refresh() like everything else, and (because
# it's fixed) it would stay put even if the world scrolled (it doesn't here, but
# you'll want that in a platformer). It uses the bundled terminalio.FONT, so no font
# asset is needed. Call label.set(...) each frame; it only re-renders when the text
# actually changes.
#
# New vs step 6: terminalio.FONT, picogame_ui.SceneLabel, a running score.
#
# Run: python3 sim/run.py tutorials/01-bounce/step7_hud.py --shot /tmp/s7.png
import terminalio
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_shapes as shp
import picogame_ui as ui
W, H = 320, 240
PADDLE_W, PADDLE_H = 44, 8
BALL = 6
BW, BH = 32, 16
COLS, ROWS = W // BW, 6
BRICK_Y = 28
BG = pg.rgb565(8, 10, 24)
scene, bufA, bufB = picogame_game.setup(background=BG)
btn = picogame_input.Buttons()
clock = picogame_clock.Clock(40)
brick_colors = [pg.rgb565(220, 70, 70), pg.rgb565(230, 150, 50),
pg.rgb565(70, 200, 90), pg.rgb565(80, 150, 230)]
bricks = pg.Tilemap(shp.tileset_colors(BW, BH, brick_colors), COLS, ROWS)
bricks.move(0, BRICK_Y)
def build_wall():
global bricks_left
for ty in range(ROWS):
for tx in range(COLS):
bricks.tile(tx, ty, 1 + (ty % 4))
bricks_left = COLS * ROWS
build_wall()
paddle = pg.Sprite(shp.rect(PADDLE_W, PADDLE_H, pg.rgb565(220, 220, 230)),
(W - PADDLE_W) // 2, H - 16)
ball = pg.Sprite(shp.rect(BALL, BALL, pg.rgb565(255, 240, 120)), W // 2, H // 2)
scene.add(bricks)
scene.add(paddle)
scene.add(ball)
# NEW: a HUD label. Adding it to the scene happens inside SceneLabel (as a fixed layer).
hud = ui.SceneLabel(scene, pg, terminalio.FONT, 4, 2, pg.rgb565(255, 255, 255), BG)
vx, vy = 2.4, -2.6
score = 0
lives = 3
def serve():
global vx, vy
ball.move(W // 2, H // 2)
vx, vy = 2.4, -2.6
while True:
btn.poll()
dx = btn.is_pressed(btn.RIGHT) - btn.is_pressed(btn.LEFT)
if dx:
paddle.move(max(0, min(W - PADDLE_W, paddle.x + dx * 5)), paddle.y)
ball.fx += vx
ball.fy += vy
if ball.fx < 0:
ball.fx = 0; vx = -vx
elif ball.fx > W - BALL:
ball.fx = W - BALL; vx = -vx
if ball.fy < 0:
ball.fy = 0; vy = -vy
if vy > 0 and pg.collide(ball.x, ball.y, ball.x + BALL, ball.y + BALL,
paddle.x, paddle.y, paddle.x + PADDLE_W, paddle.y + PADDLE_H):
vy = -abs(vy)
vx += (ball.x + BALL / 2 - (paddle.x + PADDLE_W / 2)) * 0.06
cx, cy = ball.x + BALL // 2, ball.y + BALL // 2
tx, ty = cx // BW, (cy - BRICK_Y) // BH
if 0 <= tx < COLS and 0 <= ty < ROWS and bricks.tile(tx, ty):
bricks.tile(tx, ty, 0)
bricks_left -= 1
score += 10 # NEW: score on a hit
vy = -vy
if bricks_left == 0:
build_wall()
serve()
if ball.fy > H:
lives -= 1
if lives <= 0:
lives = 3
score = 0
build_wall()
serve()
hud.set("SCORE %05d LIVES %d" % (score, lives)) # update text, then draw it
scene.refresh() # draws the scene incl. the HUD
clock.tick()
▶ Vyzkoušet v prohlížeči

step 8 — step8_particles.py · šťáva (částice + zvuk)

Sekce “step 8 — step8_particles.py · šťáva (částice + zvuk)”

Bounce – krok 8 pg.Particles je levný systém pro výbuchy: emit(x, y, count, speed, life, colour) a pak tick() každý snímek. Při každém rozbití vybuchneme v barvě cihly. picogame_audio.tone() udělá pípnutí bez .wav — blip na každý zásah. (Audio je obalené v try/except, takže je tiché, ale bezpečné tam, kde žádný zvukový výstup není, třeba v simulátoru.) Uvidíš: barevné jiskry + (na hardwaru) blip. Zkus si: změnit count/gravity u částic.

20 collapsed lines
# Bounce -- step 8: juice (particles + sound).
#
# What you learn: feedback that makes a hit feel good. pg.Particles is a cheap
# burst system: emit(x, y, count, speed, life, colour) spawns particles, tick()
# advances them (with gravity), and the scene draws them. We burst on every brick
# break, in the brick's colour. And picogame_audio.tone() builds a short square-wave
# beep with no .wav file -- a tiny blip on each hit. (Audio is wrapped in try/except
# so it degrades gracefully where there's no audio output, e.g. the simulator.)
#
# New vs step 7: pg.Particles (emit/tick), picogame_audio.tone() + Audio().sfx().
#
# Run: python3 sim/run.py tutorials/01-bounce/step8_particles.py --shot /tmp/s8.png
import terminalio
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_shapes as shp
import picogame_ui as ui
W, H = 320, 240
PADDLE_W, PADDLE_H = 44, 8
BALL = 6
BW, BH = 32, 16
COLS, ROWS = W // BW, 6
BRICK_Y = 28
BG = pg.rgb565(8, 10, 24)
scene, bufA, bufB = picogame_game.setup(background=BG)
btn = picogame_input.Buttons()
clock = picogame_clock.Clock(40)
# optional audio: a beep on each hit (no asset needed). None if no audio backend.
try:
import picogame_audio
audio = picogame_audio.Audio()
blip = picogame_audio.tone(660, 35)
except Exception:
audio = None
blip = None
brick_colors = [pg.rgb565(220, 70, 70), pg.rgb565(230, 150, 50),
pg.rgb565(70, 200, 90), pg.rgb565(80, 150, 230)]
brick_ts = shp.tileset_colors(BW, BH, brick_colors)
bricks = pg.Tilemap(brick_ts, COLS, ROWS)
bricks.move(0, BRICK_Y)
def build_wall():
global bricks_left
for ty in range(ROWS):
for tx in range(COLS):
bricks.tile(tx, ty, 1 + (ty % 4))
bricks_left = COLS * ROWS
build_wall()
paddle = pg.Sprite(shp.rect(PADDLE_W, PADDLE_H, pg.rgb565(220, 220, 230)),
(W - PADDLE_W) // 2, H - 16)
ball = pg.Sprite(shp.rect(BALL, BALL, pg.rgb565(255, 240, 120)), W // 2, H // 2)
particles = pg.Particles(96, size=2, gravity=0.12) # NEW
scene.add(bricks)
scene.add(particles) # behind paddle+ball
scene.add(paddle)
scene.add(ball)
hud = ui.SceneLabel(scene, pg, terminalio.FONT, 4, 2, pg.rgb565(255, 255, 255), BG)
vx, vy = 2.4, -2.6
score = 0
lives = 3
def serve():
global vx, vy
ball.move(W // 2, H // 2)
vx, vy = 2.4, -2.6
while True:
btn.poll()
dx = btn.is_pressed(btn.RIGHT) - btn.is_pressed(btn.LEFT)
if dx:
paddle.move(max(0, min(W - PADDLE_W, paddle.x + dx * 5)), paddle.y)
ball.fx += vx
ball.fy += vy
if ball.fx < 0:
ball.fx = 0; vx = -vx
elif ball.fx > W - BALL:
ball.fx = W - BALL; vx = -vx
if ball.fy < 0:
ball.fy = 0; vy = -vy
if vy > 0 and pg.collide(ball.x, ball.y, ball.x + BALL, ball.y + BALL,
paddle.x, paddle.y, paddle.x + PADDLE_W, paddle.y + PADDLE_H):
vy = -abs(vy)
vx += (ball.x + BALL / 2 - (paddle.x + PADDLE_W / 2)) * 0.06
cx, cy = ball.x + BALL // 2, ball.y + BALL // 2
tx, ty = cx // BW, (cy - BRICK_Y) // BH
if 0 <= tx < COLS and 0 <= ty < ROWS:
cell = bricks.tile(tx, ty)
if cell:
bricks.tile(tx, ty, 0)
bricks_left -= 1
score += 10
vy = -vy
# burst in the brick's colour at the brick's centre
bx_px = tx * BW + BW // 2
by_px = BRICK_Y + ty * BH + BH // 2
particles.emit(bx_px, by_px, 14, 3, 22, brick_colors[cell - 1])
if audio:
audio.sfx(blip)
if bricks_left == 0:
build_wall()
serve()
if ball.fy > H:
lives -= 1
if lives <= 0:
lives = 3
score = 0
build_wall()
serve()
particles.tick() # advance the burst each frame
hud.set("SCORE %05d LIVES %d" % (score, lives))
scene.refresh()
clock.tick()
▶ Vyzkoušet v prohlížeči

step 9 — step9_sprites.py · obdélníky → sprity

Sekce “step 9 — step9_sprites.py · obdélníky → sprity”

Bounce – krok 9 Pointa. Měníme jen ty dvě bitmapy: míček se stane kulatým terčíkem (shp.circle) a pádlo dostane vícebarevnou bitmapu s odleskem. Porovnej tenhle soubor se step 8 — celá herní smyčka je bajt po bajtu identická. Sprite je jedno, jestli je jeho bitmapa obdélník, generovaný tvar, nebo PNG, které jsi naimportoval v editoru. Uvidíš: tu samou hru, teď s kulatým míčkem a stínovaným pádlem. Zkus si: načíst skutečné PNG přes pipeline editor → scéna a přiřadit ho jako bitmapu.

23 collapsed lines
# Bounce -- step 9: from rectangles to sprites (the orthogonality lesson).
#
# What you learn: art is independent of mechanics. We built a COMPLETE game out of
# coloured rectangles. To make it look like a real game we change ONLY the bitmaps:
# the ball becomes a round disc (shp.circle) and the paddle gets a multi-colour
# bitmap with a highlight stripe. Compare this file to step 8: the entire game loop
# -- movement, bouncing, collision, scoring, particles -- is byte-for-byte the same.
# A Sprite doesn't care whether its bitmap is a rectangle, a generated shape, or a
# PNG you imported in the editor. (To use real PNG art: draw/import it in the editor,
# export a scene, and load it with picogame_scene -- see tutorials/README.md.)
#
# New vs step 8: only the two bitmap definitions changed (ball + paddle art).
#
# Run: python3 sim/run.py tutorials/01-bounce/step9_sprites.py --shot /tmp/s9.png
import array
import terminalio
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_shapes as shp
import picogame_ui as ui
W, H = 320, 240
PADDLE_W, PADDLE_H = 44, 8
BALL = 6
BW, BH = 32, 16
COLS, ROWS = W // BW, 6
BRICK_Y = 28
BG = pg.rgb565(8, 10, 24)
scene, bufA, bufB = picogame_game.setup(background=BG)
btn = picogame_input.Buttons()
clock = picogame_clock.Clock(40)
try:
import picogame_audio
audio = picogame_audio.Audio()
blip = picogame_audio.tone(660, 35)
except Exception:
audio = None
blip = None
def paddle_art(w, h):
"""A 2-colour paddle bitmap: blue body + a lighter highlight on the top row.
This is what 'real sprite art' is -- a PAL8 bitmap with more than one colour."""
pal = array.array("H", [pg.rgb565(0, 0, 0), pg.rgb565(70, 110, 210), pg.rgb565(150, 190, 255)])
data = bytearray(b"\x01" * (w * h)) # index 1 = body
for x in range(w):
data[x] = 2 # index 2 = highlight on the top row
return pg.Bitmap(data, w, h, format=pg.PAL8, palette=pal, frames=1, stride=w, transparent=0)
brick_colors = [pg.rgb565(220, 70, 70), pg.rgb565(230, 150, 50),
pg.rgb565(70, 200, 90), pg.rgb565(80, 150, 230)]
bricks = pg.Tilemap(shp.tileset_colors(BW, BH, brick_colors), COLS, ROWS)
bricks.move(0, BRICK_Y)
def build_wall():
global bricks_left
for ty in range(ROWS):
for tx in range(COLS):
bricks.tile(tx, ty, 1 + (ty % 4))
bricks_left = COLS * ROWS
build_wall()
# >>> the ONLY change from step 8: art instead of plain rectangles <<<
paddle = pg.Sprite(paddle_art(PADDLE_W, PADDLE_H), (W - PADDLE_W) // 2, H - 16)
ball = pg.Sprite(shp.circle(BALL, pg.rgb565(255, 240, 120)), W // 2, H // 2)
# >>> everything below is identical to step 8 <<<
particles = pg.Particles(96, size=2, gravity=0.12)
scene.add(bricks)
scene.add(particles)
scene.add(paddle)
scene.add(ball)
hud = ui.SceneLabel(scene, pg, terminalio.FONT, 4, 2, pg.rgb565(255, 255, 255), BG)
vx, vy = 2.4, -2.6
score = 0
lives = 3
def serve():
global vx, vy
ball.move(W // 2, H // 2)
vx, vy = 2.4, -2.6
49 collapsed lines
while True:
btn.poll()
dx = btn.is_pressed(btn.RIGHT) - btn.is_pressed(btn.LEFT)
if dx:
paddle.move(max(0, min(W - PADDLE_W, paddle.x + dx * 5)), paddle.y)
ball.fx += vx
ball.fy += vy
if ball.fx < 0:
ball.fx = 0; vx = -vx
elif ball.fx > W - BALL:
ball.fx = W - BALL; vx = -vx
if ball.fy < 0:
ball.fy = 0; vy = -vy
if vy > 0 and pg.collide(ball.x, ball.y, ball.x + BALL, ball.y + BALL,
paddle.x, paddle.y, paddle.x + PADDLE_W, paddle.y + PADDLE_H):
vy = -abs(vy)
vx += (ball.x + BALL / 2 - (paddle.x + PADDLE_W / 2)) * 0.06
cx, cy = ball.x + BALL // 2, ball.y + BALL // 2
tx, ty = cx // BW, (cy - BRICK_Y) // BH
if 0 <= tx < COLS and 0 <= ty < ROWS:
cell = bricks.tile(tx, ty)
if cell:
bricks.tile(tx, ty, 0)
bricks_left -= 1
score += 10
vy = -vy
particles.emit(tx * BW + BW // 2, BRICK_Y + ty * BH + BH // 2,
14, 3, 22, brick_colors[cell - 1])
if audio:
audio.sfx(blip)
if bricks_left == 0:
build_wall()
serve()
if ball.fy > H:
lives -= 1
if lives <= 0:
lives = 3
score = 0
build_wall()
serve()
particles.tick()
hud.set("SCORE %05d LIVES %d" % (score, lives))
scene.refresh()
clock.tick()
▶ Vyzkoušet v prohlížeči

Kam dál: 02-starship (poolování, rotace, střílení, stavový automat), nebo skoč rovnou na webový editor a picogame_scene, ať stavíš úrovně jako data místo ručního psaní. Podívat se můžeš i na formát scény nebo na sourozenecký 03-quest.