Tabla de Contenidos
Fundamentos de Pyxel
El primer objetivo es entender cómo funciona un videojuego por dentro.
Este módulo forma parte del Programación de videojuegos retro con Pyxel
En un programa Python convencional, el código suele ejecutarse de principio a fin. En un videojuego, en cambio, existe un bucle que se repite continuamente.
Cómo funciona un videojuego
Antes de escribir código, necesitamos comprender una diferencia fundamental entre un programa convencional y un videojuego.
Imagina un programa que calcula la suma de dos números:
a = 10 b = 20 print(a + b)
El programa ejecuta las instrucciones y termina.
Un videojuego funciona de otra manera: permanece ejecutándose y actualiza continuamente su estado.
Por ejemplo:
- Comprueba si has pulsado una tecla.
- Actualiza la posición del personaje.
- Comprueba si hay colisiones.
- Dibuja el escenario.
- Repite todo el proceso.
Este proceso se denomina Game Loop (bucle de juego).
El bucle principal del videojuego
El bucle se compone de:
- Inicio del juego: Inicialización de variables y recursos
- Actualizar (update) el estado del juego: Movimiento, enemigos, puntuación, colisiones…
- Dibujar (draw) lo que vemos: Fondo, personajes, objetos, interfaz…
El bucle se repite continuamente hasta cerrar el juego (unas 60 veces por segundo). Pyxel se encarga de gestionar este ciclo por nosotros.
Nosotros solamente tenemos que proporcionar dos funciones:
update(): actualiza la lógica del juego.draw(): dibuja el estado actual del juego.
Es importante comprender desde el principio que estas dos responsabilidades deben estar separadas.
Nuestra primera ventana
import pyxel class Game: def __init__(self): pyxel.init(160, 120, title="Mi primer juego") pyxel.run(self.update, self.draw) def update(self): pass def draw(self): pyxel.cls(0) if __name__ == "__main__": Game()
Lo ejecutamos con:
pyxel main.py
Si todo ha ido bien, debería aparecer una ventana negra de 160 × 120 píxeles.
Analicemos el código
Vamos por partes.
Primero, importamos el módulo Pyxel:
import pyxel
Esto nos permite utilizar las funciones que proporciona el motor.
Creamos nuestra clase Game:
class Game:
Esta clase representa nuestro videojuego.
A medida que el proyecto crezca, guardaremos dentro de ella el estado general de la partida.
Inicializamos Pyxel:
pyxel.init(160, 120, title="Mi primer juego")
Los dos primeros argumentos indican la resolución:
- 160 píxeles de ancho.
- 120 píxeles de alto.
Es una resolución pequeña, apropiada para empezar a experimentar con pixel art.
Arrancamos el bucle de juego:
pyxel.run(self.update, self.draw)
Aquí sucede algo importante. Le estamos diciendo a Pyxel que ejecute continuamente nuestros dos métodos:
self.updateself.draw
No estamos llamando a los métodos con paréntesis porque estamos proporcionando las funciones para que Pyxel las ejecute cuando corresponda.
Limpiamos la pantalla:
pyxel.cls(0)
cls() limpia la pantalla y establece el color de fondo.
El número 0 representa el negro dentro de la paleta predeterminada de Pyxel.
El bloque principal:
if __name__ == "__main__": Game()
Es una buena práctica habitual de Python. Permite que el archivo pueda importarse desde otro módulo sin arrancar automáticamente el juego.
Dibujar nuestro primer personaje
Ahora vamos a añadir un personaje. Para empezar, utilizaremos un rectángulo de 8 × 8 píxeles.
Modifica el código de esta manera:
import pyxel class Game: def __init__(self): pyxel.init(160, 120, title="Mi primer juego") self.player_x = 76 self.player_y = 56 pyxel.run(self.update, self.draw) def update(self): pass def draw(self): pyxel.cls(0) pyxel.rect( self.player_x, self.player_y, 8, 8, 11 ) if __name__ == "__main__": Game()
Ahora tenemos un pequeño cuadrado de color azul claro en el centro de la pantalla.
Entendiendo pyxel.rect()
La función tiene esta estructura:
pyxel.rect(x, y, width, height, color)
| Parámetro | Significado |
|---|---|
x | Posición horizontal |
y | Posición vertical |
width | Anchura |
height | Altura |
color | Color de la paleta |
En nuestro ejemplo:
pyxel.rect(76, 56, 8, 8, 11)
Estamos dibujando un rectángulo:
- A 76 píxeles del borde izquierdo.
- A 56 píxeles del borde superior.
- Con 8 píxeles de anchura.
- Con 8 píxeles de altura.
- De color 11.
¿Por qué las coordenadas son 76 y 56?
Porque queremos centrarlo:
x=160−82=76 x=2160−8=76 y=120−82=56 y=2120−8=56
Una característica importante de Pyxel y de muchos motores 2D es que el origen de coordenadas está en la esquina superior izquierda:
- X aumenta hacia la derecha.
- Y aumenta hacia abajo.
Esto es algo que debes interiorizar porque lo utilizaremos constantemente.
Moviendo nuestro personaje
Ahora llega la parte divertida. Queremos que nuestro personaje pueda desplazarse utilizando las flechas del teclado.
Para ello utilizaremos:
pyxel.btn(pyxel.KEY_LEFT)
Esta función comprueba si una tecla está pulsada.
Devuelve True mientras la mantengas pulsada y False cuando la sueltes.
Hay dos conceptos de entrada que conviene distinguir:
| Función | Comportamiento |
|---|---|
pyxel.btn() | Mientras mantienes pulsada la tecla |
pyxel.btnp() | En el momento de pulsarla |
Para mover un personaje utilizaremos normalmente btn().
Nuestro primer sistema de movimiento. Modifica el método update():
def update(self): if pyxel.btn(pyxel.KEY_LEFT): self.player_x -= 1 if pyxel.btn(pyxel.KEY_RIGHT): self.player_x += 1 if pyxel.btn(pyxel.KEY_UP): self.player_y -= 1 if pyxel.btn(pyxel.KEY_DOWN): self.player_y += 1
Ahora puedes mover el cuadrado por la pantalla. ¡Ya tienes un personaje controlable!
¿Qué está ocurriendo?
Vamos a analizar una instrucción:
self.player_x -= 1
Es equivalente a:
self.player_x = self.player_x - 1
Cada vez que Pyxel ejecuta update() y detecta que la flecha izquierda está pulsada, resta una unidad a la posición horizontal.
Por tanto:
- Izquierda: X disminuye.
- Derecha: X aumenta.
- Arriba: Y disminuye.
- Abajo: Y aumenta.
Hay algo importante que debes observar: el movimiento se produce en cada actualización del juego, no solamente una vez cuando pulsas la tecla.
Añadir velocidad y límites de pantalla
Nuestro movimiento actual tiene un pequeño problema: el personaje puede desaparecer por los bordes de la ventana.
Vamos a solucionarlo.
También vamos a introducir una variable llamada player_speed.
En lugar de escribir constantemente 1, utilizaremos esa variable.
Sustituye tu código por esta versión:
import pyxel class Game: def __init__(self): pyxel.init(160, 120, title="Mi primer juego") self.player_x = 76 self.player_y = 56 self.player_width = 8 self.player_height = 8 self.player_speed = 2 pyxel.run(self.update, self.draw) def update(self): if pyxel.btn(pyxel.KEY_LEFT): self.player_x -= self.player_speed if pyxel.btn(pyxel.KEY_RIGHT): self.player_x += self.player_speed if pyxel.btn(pyxel.KEY_UP): self.player_y -= self.player_speed if pyxel.btn(pyxel.KEY_DOWN): self.player_y += self.player_speed self.player_x = max( 0, min(self.player_x, 160 - self.player_width) ) self.player_y = max( 0, min(self.player_y, 120 - self.player_height) ) def draw(self): pyxel.cls(0) pyxel.rect( self.player_x, self.player_y, self.player_width, self.player_height, 11 ) if __name__ == "__main__": Game()
Una pequeña lección de programación. Quiero que entiendas especialmente estas expresiones:
self.player_x = max( 0, min(self.player_x, 160 - self.player_width) )
Lo que estamos haciendo es limitar la posición horizontal.
Primero:
min(self.player_x, 160 - self.player_width)
Impide que el personaje sobrepase el límite derecho.
Después:
max(0, ...)
Impide que su posición sea negativa.
La misma lógica se aplica al eje vertical.
Este procedimiento se denomina clamping y lo utilizaremos continuamente en desarrollo de videojuegos.
Buenas prácticas que hemos introducido
Aunque solamente llevamos unos minutos, ya estamos aplicando algunas decisiones de diseño importantes.
Variables con nombres descriptivos
En lugar de:
x = 76 y = 56 v = 2
Utilizamos:
self.player_x = 76 self.player_y = 56 self.player_speed = 2
Esto facilita muchísimo entender el código cuando un proyecto crece.
Separación de responsabilidades
Hemos reservado:
__init__()para inicializar el juego.update()para actualizar su lógica.draw()para representar visualmente el estado.
Intentaremos mantener esta separación durante todo el curso.
Evitar valores mágicos
En lugar de escribir repetidamente 8, hemos creado:
self.player_width = 8 self.player_height = 8
Así podemos modificar las dimensiones del personaje desde un único lugar.
Más adelante extraeremos también la resolución de pantalla a constantes.
Separar el estado de su representación
La posición del personaje se guarda en:
self.player_x self.player_y
El dibujo utiliza esas variables.
No necesitamos que el dibujo determine dónde debe estar el personaje. Esa decisión corresponde a la lógica del juego.
Ejercicios prácticos
- Ejercicio 1 — Cambiar la velocidad
- Ejercicio 2 — Cambiar el tamaño
- Ejercicio 3 — Movimiento con WASD
- Ejercicio 4 — Cambiar de color
- Ejercicio 5 — Movimiento diagonal
Si pulsas simultáneamente izquierda y arriba, el personaje se desplaza en diagonal. Pero ¿te has dado cuenta de que la velocidad diagonal es mayor que la velocidad horizontal o vertical? Esto sucede porque estamos sumando dos movimientos independientes. Es un problema clásico del desarrollo de videojuegos 2D. Más adelante aprenderemos a trabajar con vectores y normalización de velocidades.
Qué debes haber aprendido
- Qué es el bucle principal de un videojuego.
- Qué diferencia hay entre update() y draw().
- Qué representa una coordenada X y una coordenada Y.
- Cómo detectar si una tecla está pulsada.
- Cómo representar un objeto mediante un rectángulo.
- Cómo controlar los límites de movimiento.
- Por qué organizamos el programa dentro de una clase.

