1. Tu primer juego: el bucle y las teclas
Un juego es un programa que sigue funcionando aunque no pulses nada. Cada pocos milisegundos mira las teclas, mueve un poco las cosas y lo vuelve a dibujar todo. En este primer paso de la ruta construyes Coin Chase: un orbe brillante que diriges con las flechas, una moneda que atrapar, un reloj de 30 segundos y una puntuación. Es pequeño, pero tiene todas las partes de un juego de verdad.

| Objetivo | Tiempo | Nivel | Necesitas |
|---|---|---|---|
| Un juego completo con bucle, teclas mantenidas, temporizador y puntuación | 25 minutos | Principiante | Python y Pyzed instalados (Instalación); ninguna experiencia con juegos |
Qué aprenderás
Sección titulada «Qué aprenderás»- Las tres partes de todo juego: estado, actualización y dibujo.
- Ejecutar código una vez por fotograma con
pz.frames(self.tick). - Mover cosas con velocidad ×
dt, para que el juego vaya igual de rápido en cualquier pantalla. - Leer las teclas mantenidas (
game.vector) y las pulsadas una vez (on_key). - Pausar el juego (y la CPU) devolviendo cero suscripciones.
Juégalo primero:
uv run --with pyzed-ui python examples/games/coin_chase.pypython3 -m venv .venvsource .venv/bin/activatepython -m pip install pyzed-uipython examples/games/coin_chase.pypy -m venv .venv.venv\Scripts\Activate.ps1python -m pip install pyzed-uipython examples\games\coin_chase.pypython3 examples/games/coin_chase.pypy examples\games\coin_chase.pyLas tres partes
Sección titulada «Las tres partes»flowchart LR
S["Estado<br/>x, y, score, time_left"] --> U["Actualizar: tick(dt)<br/>leer teclas · mover · colisiones"]
U --> D["Dibujar: view()<br/>canvas + widgets del HUD"]
D -->|siguiente fotograma| U
Pyzed llama a tick(dt) antes de cada fotograma que muestra la pantalla (60, 120 o 144 veces por segundo) y
luego a view() para dibujar. Nunca escribes el bucle: solo dices qué pasa en un fotograma.
-
El estado. Todo lo que el juego necesita recordar vive en atributos de la app.
reset()les da valor, así que el mismo método empieza la primera ronda y cada ronda nueva:W, H = 560, 380SPEED = 260 # pixels per secondPLAYER_R = 14COIN_R = 9ROUND = 30.0 # seconds per roundclass CoinChase(pz.App):def __init__(self, seed=None):self.random = random.Random(seed)self.held = None # the keys held down; None reads the keyboardself.best = 0self.reset()def reset(self):self.x, self.y = W / 2, H / 2self.score = 0self.time_left = ROUNDself.paused = self.over = Falseself.pops = [] # [x, y, age]: rings that grow and fade where a coin was takenself.clock = 0.0self.coin = self.new_coin()heldes para pruebas y demos: le asignan{"right"}para mover sin teclado. Cuando esNone, el juego lee las teclas reales. -
Enciende el bucle.
subscriptions()devuelve lo que debe estar en marcha ahora.pz.frames(self.tick)significa “llama aticken cada fotograma”. Pyzed vuelve a preguntar tras cada cambio, así que no devolver nada pausa el juego, y la CPU descansa:def subscriptions(self):if self.paused or self.over:return []return [pz.frames(self.tick)] -
Mueve con velocidad × dt.
dtson los segundos desde el fotograma anterior (unos0.0167a 60 Hz). Si movieras 4 píxeles fijos por fotograma, el orbe iría el doble de rápido en una pantalla de 120 Hz. Multiplicar pordtda píxeles por segundo:def tick(self, dt):self.clock += dtself.time_left = max(0.0, self.time_left - dt)dx, dy = game.vector(held=self.held) # arrows / WASD, diagonals normalizedself.x = game.clamp(self.x + dx * SPEED * dt, PLAYER_R, W - PLAYER_R)self.y = game.clamp(self.y + dy * SPEED * dt, PLAYER_R, H - PLAYER_R)game.vector()convierte las flechas y WASD en una dirección:(1, 0)a la derecha,(0.71, -0.71)arriba a la derecha (así las diagonales no son más rápidas).game.clampmantiene el orbe dentro del tablero. -
¿Tocó la moneda? Dos círculos se tocan cuando la distancia entre sus centros es menor que la suma de sus radios.
math.hypotmide esa distancia:cx, cy = self.coinif math.hypot(cx - self.x, cy - self.y) < PLAYER_R + COIN_R:self.score += 1self.pops.append([cx, cy, 0.0])self.coin = self.new_coin()for pop in self.pops:pop[2] += dtself.pops = [p for p in self.pops if p[2] < 0.4]if self.time_left <= 0:self.over = Trueself.best = max(self.best, self.score)Cada “pop” es un anillo que vive 0,4 segundos. Efectos así son la forma más barata de que un juego se sienta bien; la página de consejos tiene más.
-
Teclas pulsadas una vez. Mantener una tecla mueve el orbe; pulsarla hace algo una vez.
on_keyrecibe cada pulsación como texto:def on_key(self, combo):if combo == "space" and not self.over:self.paused = not self.pausedelif combo == "enter" and self.over:self.reset() -
Dibuja el fotograma.
view()convierte el estado en una imagen. El tablero es unpz.canvascon formas depyzed.draw; la moneda estrecha su ancho concospara parecer que gira, ygame.glowañade una luz suave:def board(self):shapes = [draw.background("#0b0e14")]...for x, y, age in self.pops:k = age / 0.4 # 0 -> 1 over the ring's lifeshapes.append(draw.circle(x, y, COIN_R + k * 26, stroke=game.rgba("#fde68a", 1 - k), width=3))shapes += game.glow(self.x, self.y, PLAYER_R, "#22d3ee", layers=4, strength=0.5)return pz.canvas(shapes, width=W, height=H) -
El HUD son widgets. La puntuación, el reloj y la tarjeta de pausa son componentes normales de Pyzed apilados sobre el canvas con
pz.stack, así que reciben el tema, los iconos y el efectoglassgratis:hud = pz.row([ui.label("Coin Chase", weight="semibold"),pz.hspace(),ui.badge(f"{self.score} coins", icon="circle-dollar-sign", tint="warning"),ui.badge(f"{math.ceil(self.time_left)} s", icon="timer",tint="error" if self.time_left < 5 else "accent"),], spacing=8, align="center")play = pz.stack([self.board(), self.overlay()], width=W, height=H)
Prueba esto
Sección titulada «Prueba esto»- Haz que el orbe vaya más rápido un segundo después de cada moneda (un temporizador
boostque baja entick). - Añade una segunda moneda, más rara, que vale 5 puntos y desaparece a los 3 segundos.
- Guarda la mejor puntuación en un archivo; el paso 8 enseña cómo.
Código completo
Sección titulada «Código completo»"""Coin Chase: your first game. A frame loop, keys held down and a 30-second timer.
python examples/games/coin_chase.py
Arrows or WASD move the orb, collect as many coins as you can in 30 seconds. Space pauses,Enter plays again. Shows the three parts of every real-time game:
1. **State**: plain attributes on the app (position, score, time left).2. **Update**: ``tick(dt)``, called by ``pz.frames`` once per displayed frame, moves things by *speed x dt* and checks what happened.3. **Draw**: ``view()`` turns the state into a canvas, with the HUD as ordinary widgets.
Tutorial: https://albakore.github.io/pyzed/games/first-game/"""
import mathimport random
import pyzed as pzfrom pyzed import draw, game, ui
W, H = 560, 380SPEED = 260 # pixels per secondPLAYER_R = 14COIN_R = 9ROUND = 30.0 # seconds per round
class CoinChase(pz.App): title = "Coin Chase" size = (W + 32, H + 92) theme = "One Dark"
def __init__(self, seed=None): self.random = random.Random(seed) #: The keys held down; ``None`` reads the keyboard. Tests and demos set it to steer. self.held = None self.best = 0 self.reset()
# -- 1. state --------------------------------------------------------------------------- def reset(self): self.x, self.y = W / 2, H / 2 self.score = 0 self.time_left = ROUND self.paused = self.over = False self.pops = [] # [x, y, age]: rings that grow and fade where a coin was taken self.clock = 0.0 # seconds since the round started (for the coin's spin) self.coin = self.new_coin()
def new_coin(self): """A coin somewhere not too close to the player.""" while True: x = self.random.uniform(30, W - 30) y = self.random.uniform(30, H - 30) if math.hypot(x - self.x, y - self.y) > 120: return (x, y)
# -- 2. update -------------------------------------------------------------------------- def subscriptions(self): # No frames while paused or over: the game stops and so does the CPU. if self.paused or self.over: return [] return [pz.frames(self.tick)]
def tick(self, dt): self.clock += dt self.time_left = max(0.0, self.time_left - dt)
# Move by speed x dt: the same speed at 60, 120 or 144 frames per second. dx, dy = game.vector(held=self.held) # arrows / WASD, diagonals normalized self.x = game.clamp(self.x + dx * SPEED * dt, PLAYER_R, W - PLAYER_R) self.y = game.clamp(self.y + dy * SPEED * dt, PLAYER_R, H - PLAYER_R)
# Two circles touch when the distance between centers is less than the radii added. cx, cy = self.coin if math.hypot(cx - self.x, cy - self.y) < PLAYER_R + COIN_R: self.score += 1 self.pops.append([cx, cy, 0.0]) self.coin = self.new_coin()
for pop in self.pops: pop[2] += dt self.pops = [p for p in self.pops if p[2] < 0.4]
if self.time_left <= 0: self.over = True self.best = max(self.best, self.score)
def on_key(self, combo): if combo == "space" and not self.over: self.paused = not self.paused elif combo == "enter" and self.over: self.reset()
# -- 3. draw ---------------------------------------------------------------------------- def board(self): shapes = [draw.background("#0b0e14")] for gx in range(0, W, 40): # a faint grid, to feel the motion shapes.append(draw.line(gx, 0, gx, H, stroke="#ffffff08")) for gy in range(0, H, 40): shapes.append(draw.line(0, gy, W, gy, stroke="#ffffff08"))
cx, cy = self.coin spin = abs(math.cos(self.clock * 4)) # squash the coin's width: it seems to spin shapes += game.glow(cx, cy, COIN_R * 0.6, "#fbbf24", layers=3, strength=0.5, core=False) shapes.append(draw.path([("M", cx, cy - COIN_R)] + [ ("L", cx + math.sin(a) * COIN_R * max(spin, 0.15), cy - math.cos(a) * COIN_R) for a in (i * math.pi / 8 for i in range(1, 17))], fill="#fbbf24", stroke="#fde68a", width=1.5))
for x, y, age in self.pops: k = age / 0.4 # 0 -> 1 over the ring's life shapes.append(draw.circle(x, y, COIN_R + k * 26, stroke=game.rgba("#fde68a", 1 - k), width=3))
shapes += game.glow(self.x, self.y, PLAYER_R, "#22d3ee", layers=4, strength=0.5) return pz.canvas(shapes, width=W, height=H)
def overlay(self): if self.over: title, lines = "Time!", [f"{self.score} coins · best {self.best}", "Enter plays again"] elif self.paused: title, lines = "Paused", ["Space continues"] else: return None card = pz.container(pz.column([ui.heading(title, level=2), *[ui.muted(t) for t in lines]], spacing=8, align="center"), padding=(20, 32)).style(glass=True, radius=14) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center")
def view(self): t = self.resolved_theme() hud = pz.row([ ui.label("Coin Chase", weight="semibold"), pz.hspace(), ui.badge(f"{self.score} coins", icon="circle-dollar-sign", tint="warning"), ui.badge(f"{math.ceil(self.time_left)} s", icon="timer", tint="error" if self.time_left < 5 else "accent"), ui.badge(f"Best {self.best}", icon="trophy", tint="text_muted"), ], spacing=8, align="center") play = pz.stack([self.board(), self.overlay()], width=W, height=H) return pz.container(pz.column([hud, pz.container(play, radius=10, clip=True), ui.muted("Arrows / WASD move · Space pauses · Enter restarts", size="sm")], spacing=10), padding=16, width="fill", height="fill", bg=t.background)
if __name__ == "__main__": pz.run(CoinChase)