Crear un videojuego es una de las mejores formas de aprender a programar, porque cada error se ve en pantalla y cada mejora se nota al instante. Python es un lenguaje fácil de leer y pygame es una biblioteca que se encarga de la ventana, los gráficos, el teclado y el sonido. Con ambos puedes pasar de cero a un juego terminado y publicado.
Esta guía recorre el camino completo: preparar el entorno, entender el bucle de juego, programar tus primeros juegos, diseñar uno propio y compartirlo.
Tabla de contenido
¿Qué es pygame y por qué usarlo?
pygame es una biblioteca de Python para crear juegos 2D. Funciona sobre SDL y ofrece lo básico para empezar: ventana, dibujo de formas e imágenes, eventos de teclado y ratón, sonido y control del tiempo.
Ventajas:
- Curva de aprendizaje suave: no necesitas un motor gráfico pesado.
- Tú controlas toda la lógica, lo que enseña cómo funciona realmente un juego.
- Es gratuito, multiplataforma y tiene una comunidad grande.
Límites: no es la mejor opción para juegos 3D ni para producciones grandes. Para aprender y para juegos 2D pequeños o medianos es excelente.
Existen dos paquetes que se importan igual (import pygame): pygame (el original) y pygame-ce (edición comunitaria con desarrollo muy activo). Elige uno e instala solo ese.
Paso 1: domina los fundamentos de Python

Antes de abrir pygame, asegúrate de manejar con soltura:
- Variables, números, cadenas y booleanos.
- Listas, tuplas y diccionarios.
- Condicionales (
if,elif,else) y bucles (for,while). - Funciones con parámetros y
return. - Clases, atributos y métodos (
self,__init__). - Módulos e
import, y lectura de mensajes de error.
El tutorial oficial de Python (docs.python.org/3/tutorial) es gratuito y suficiente. Con una o dos semanas de práctica diaria basta para comenzar.
Paso 2: prepara el entorno de desarrollo
1. Instala Python. Descarga la versión estable más reciente desde python.org (se recomienda 3.10 o superior). En Windows, marca la casilla Add python.exe to PATH.
2. Crea un entorno virtual. Aísla las bibliotecas de cada proyecto y evita conflictos:
mkdir mi_juego
cd mi_juego
python -m venv .venv
# Activar el entorno
# Windows (PowerShell): .venv\Scripts\Activate.ps1
# Linux / macOS: source .venv/bin/activate
3. Instala pygame:
python -m pip install --upgrade pip
python -m pip install pygame
4. Verifica la instalación:
import pygame
pygame.init()
print("pygame", pygame.version.ver)
pygame.quit()
Si aparece ModuleNotFoundError, normalmente el entorno virtual no está activado.
5. Elige un editor. Visual Studio Code (con la extensión de Python) o PyCharm Community son buenas opciones. Selecciona dentro del editor el intérprete de tu carpeta .venv.
Estructura de carpetas recomendada:
mi_juego/
├── .venv/
├── assets/
│ ├── imagenes/
│ ├── sonidos/
│ └── fuentes/
├── main.py
└── requirements.txt
Paso 3: entiende el bucle principal de un juego
Todo juego en pygame, sencillo o complejo, repite cuatro etapas muchas veces por segundo:
- Eventos: leer lo que hizo el jugador (teclas, ratón, cerrar la ventana).
- Actualizar: mover objetos, aplicar reglas, detectar colisiones.
- Dibujar: pintar el estado actual en pantalla.
- Controlar el tiempo: mantener una velocidad estable (por ejemplo, 60 cuadros por segundo).
Este es el esqueleto mínimo:
import sys
import pygame
pygame.init()
ANCHO, ALTO = 800, 600
FPS = 60
pantalla = pygame.display.set_mode((ANCHO, ALTO))
pygame.display.set_caption("Mi primer juego")
reloj = pygame.time.Clock()
ejecutando = True
while ejecutando:
# 1. Eventos
for evento in pygame.event.get():
if evento.type == pygame.QUIT:
ejecutando = False
# 2. Actualizar la lógica
# 3. Dibujar
pantalla.fill((20, 20, 30))
pygame.display.flip()
# 4. Controlar la velocidad
reloj.tick(FPS)
pygame.quit()
sys.exit()
Qué hace cada pieza:
pygame.display.set_mode(...)crea la ventana y devuelve la superficie sobre la que se dibuja todo.pygame.event.get()entrega los eventos pendientes. Si no se llama en cada cuadro, la ventana deja de responder.pantalla.fill(color)borra el cuadro anterior. Sin esto, los objetos dejan estela.pygame.display.flip()muestra en pantalla lo que se dibujó.reloj.tick(FPS)limita la velocidad y devuelve los milisegundos del cuadro anterior.
Coordenadas y rectángulos
En pygame el origen (0, 0) está en la esquina superior izquierda y el eje y crece hacia abajo. Subir un objeto significa disminuir su y.
Los objetos suelen representarse con pygame.Rect(x, y, ancho, alto), que ofrece atributos como left, right, top, bottom y center, y métodos como colliderect() para detectar choques. Un Rect guarda números enteros, así que para movimientos suaves conviene acumular la posición en variables decimales y asignarla redondeada.
Mover objetos sin depender de los FPS
Si mueves un objeto «5 píxeles por cuadro», irá más lento en un equipo que corra a 30 FPS que en uno que corra a 120. La solución es medir la velocidad en píxeles por segundo y multiplicarla por dt, la duración del último cuadro en segundos:
posición nueva = posición anterior + velocidad (px/s) × dt (s)
import sys
import pygame
pygame.init()
ANCHO, ALTO = 800, 600
FPS = 60
VELOCIDAD = 300 # píxeles por segundo
pantalla = pygame.display.set_mode((ANCHO, ALTO))
reloj = pygame.time.Clock()
jugador = pygame.Rect(0, 0, 50, 50)
jugador.center = (ANCHO // 2, ALTO // 2)
x, y = float(jugador.x), float(jugador.y)
ejecutando = True
while ejecutando:
dt = reloj.tick(FPS) / 1000
for evento in pygame.event.get():
if evento.type == pygame.QUIT:
ejecutando = False
teclas = pygame.key.get_pressed()
dx = teclas[pygame.K_RIGHT] - teclas[pygame.K_LEFT]
dy = teclas[pygame.K_DOWN] - teclas[pygame.K_UP]
x += dx * VELOCIDAD * dt
y += dy * VELOCIDAD * dt
# Limitar a los bordes de la ventana
x = max(0, min(ANCHO - jugador.width, x))
y = max(0, min(ALTO - jugador.height, y))
jugador.topleft = (round(x), round(y))
pantalla.fill((20, 20, 30))
pygame.draw.rect(pantalla, (80, 200, 120), jugador)
pygame.display.flip()
pygame.quit()
sys.exit()
Este patrón (dt, posición decimal, límites con max y min, asignación redondeada) se reutiliza en casi todos los juegos.
Teclado: mantener frente a pulsar
| Método | Detecta | Úsalo para |
|---|---|---|
pygame.key.get_pressed() | Tecla mantenida ahora mismo | Caminar, mover una paleta |
Evento pygame.KEYDOWN | El instante de la pulsación | Saltar, disparar, pausar, menús |
pygame.mouse.get_pos() y MOUSEBUTTONDOWN | Posición y clics del ratón | Menús, apuntar |
Imágenes, texto y sonido
from pathlib import Path
BASE = Path(__file__).parent
# Imagen (cargar una sola vez, antes del bucle)
nave = pygame.image.load(BASE / "assets" / "imagenes" / "nave.png").convert_alpha()
pantalla.blit(nave, (300, 400))
# Texto
fuente = pygame.font.Font(None, 36)
texto = fuente.render("Puntos: 10", True, (255, 255, 255))
pantalla.blit(texto, (10, 10))
# Sonido
pygame.mixer.init()
disparo = pygame.mixer.Sound(BASE / "assets" / "sonidos" / "disparo.wav")
disparo.play()
Dos reglas importantes: carga imágenes y sonidos una vez antes del bucle (hacerlo en cada cuadro vuelve el juego lento) y usa convert() o convert_alpha() tras cargar una imagen. Si usas recursos de terceros, revisa siempre su licencia.
Paso 4: tus primeros proyectos
Construye juegos pequeños y completos. Cada uno enseña una pieza distinta.
Proyecto 1: Pong
Enseña velocidad como vector, rebotes, ángulo de salida según el punto de impacto, una inteligencia artificial sencilla y un marcador.
import random
import sys
import pygame
ANCHO, ALTO = 800, 500
FPS = 60
FONDO = (15, 15, 25)
BLANCO = (240, 240, 240)
VEL_PALA = 420 # px/s, jugador
VEL_IA = 290 # px/s, rival (menor que la bola: se puede ganar)
VEL_BOLA_X = 320 # px/s
VEL_MAX_X = 700
def reiniciar_bola(sentido):
pos = pygame.Vector2(ANCHO / 2, ALTO / 2)
vel = pygame.Vector2(sentido * VEL_BOLA_X, random.uniform(-180, 180))
return pos, vel
def main():
pygame.init()
pantalla = pygame.display.set_mode((ANCHO, ALTO))
pygame.display.set_caption("Pong")
reloj = pygame.time.Clock()
fuente = pygame.font.Font(None, 64)
pala_j = pygame.Rect(30, 0, 12, 100)
pala_r = pygame.Rect(ANCHO - 42, 0, 12, 100)
y_j = y_r = ALTO / 2 - 50
bola = pygame.Rect(0, 0, 14, 14)
pos_bola, vel_bola = reiniciar_bola(random.choice([-1, 1]))
puntos_j = puntos_r = 0
corriendo = True
while corriendo:
dt = reloj.tick(FPS) / 1000
for evento in pygame.event.get():
if evento.type == pygame.QUIT:
corriendo = False
# Jugador (W/S o flechas)
teclas = pygame.key.get_pressed()
mov = (teclas[pygame.K_s] or teclas[pygame.K_DOWN]) - (
teclas[pygame.K_w] or teclas[pygame.K_UP]
)
y_j += mov * VEL_PALA * dt
# Rival: sigue la bola con velocidad limitada
objetivo = pos_bola.y - pala_r.height / 2
paso = VEL_IA * dt
y_r += max(-paso, min(paso, objetivo - y_r))
y_j = max(0, min(ALTO - pala_j.height, y_j))
y_r = max(0, min(ALTO - pala_r.height, y_r))
pala_j.y, pala_r.y = round(y_j), round(y_r)
# Bola
pos_bola += vel_bola * dt
bola.center = (round(pos_bola.x), round(pos_bola.y))
if bola.top <= 0:
bola.top = 0
pos_bola.y = bola.centery
vel_bola.y = abs(vel_bola.y)
elif bola.bottom >= ALTO:
bola.bottom = ALTO
pos_bola.y = bola.centery
vel_bola.y = -abs(vel_bola.y)
for pala, sentido in ((pala_j, 1), (pala_r, -1)):
if bola.colliderect(pala) and vel_bola.x * sentido < 0:
desvio = (bola.centery - pala.centery) / (pala.height / 2)
vel_bola.x = sentido * min(abs(vel_bola.x) * 1.06, VEL_MAX_X)
vel_bola.y = desvio * 320
# Puntos
if bola.right < 0:
puntos_r += 1
pos_bola, vel_bola = reiniciar_bola(1)
elif bola.left > ANCHO:
puntos_j += 1
pos_bola, vel_bola = reiniciar_bola(-1)
# Dibujo
pantalla.fill(FONDO)
pygame.draw.line(pantalla, (60, 60, 80), (ANCHO // 2, 0), (ANCHO // 2, ALTO), 2)
pygame.draw.rect(pantalla, BLANCO, pala_j)
pygame.draw.rect(pantalla, BLANCO, pala_r)
pygame.draw.ellipse(pantalla, BLANCO, bola)
marcador = fuente.render(f"{puntos_j} {puntos_r}", True, BLANCO)
pantalla.blit(marcador, marcador.get_rect(midtop=(ANCHO // 2, 15)))
pygame.display.flip()
pygame.quit()
sys.exit()
if __name__ == "__main__":
main()
Claves del código:
- El rebote contra una paleta solo se procesa si la bola se mueve hacia ella (
vel_bola.x * sentido < 0). Así se evitan rebotes múltiples en un mismo golpe. desviova de -1 (borde superior de la paleta) a 1 (borde inferior) y define el ángulo de salida.- El rival es ganable porque su velocidad máxima es menor que la de la bola.
Retos: termina la partida a 7 puntos, añade sonido al golpear, crea un modo para dos jugadores.
Proyecto 2: la serpiente (Snake)
Enseña a trabajar con una cuadrícula, a guardar el cuerpo en una lista y a separar el ritmo del juego de los FPS: la serpiente avanza en pasos discretos (por ejemplo, cada 110 ms) mientras la pantalla se redibuja a 60 FPS. La técnica es un acumulador de tiempo:
acumulador += reloj.tick(FPS) # milisegundos
while vivo and acumulador >= PASO_MS:
acumulador -= PASO_MS
# mover la serpiente una celda
Tres detalles que suelen fallar:
- Giro de 180°: si el jugador pulsa dos teclas muy rápido, la serpiente puede girar sobre sí misma. Compara la nueva dirección con la última dirección aplicada, no con la última tecla pulsada.
- Choque con la cola: al avanzar sin comer, la cola se libera, así que la cabeza puede entrar en la celda que la cola va a dejar. Por eso se comprueba la colisión contra
serpiente[:-1]. - Comida sobre el cuerpo: genera la comida solo en celdas libres.
Retos: guardar la puntuación máxima en un archivo, aumentar la velocidad cada 5 puntos, permitir cruzar los bordes.
Paso 5: proyectos intermedios
Proyecto 3: Invasores (estilo Space Invaders)
Introduce los sprites y los grupos. Un pygame.sprite.Sprite es un objeto con una imagen (image) y una posición (rect). Un Group permite actualizar y dibujar muchos de ellos con una sola línea:
class Bala(pygame.sprite.Sprite):
def __init__(self, centro_x, y):
super().__init__()
self.image = pygame.Surface((4, 14))
self.image.fill((255, 240, 120))
self.rect = self.image.get_rect(midbottom=(centro_x, y))
self.y = float(self.rect.y)
def update(self, dt):
self.y -= 520 * dt
self.rect.y = round(self.y)
if self.rect.bottom < 0:
self.kill()
# En el bucle principal:
todos.update(dt)
todos.draw(pantalla)
# Colisión entre dos grupos: se eliminan ambos elementos
pygame.sprite.groupcollide(balas, enemigos, True, True)
Otras ideas del juego: una cadencia de disparo comparando pygame.time.get_ticks() con el instante del último disparo, y una formación de enemigos que avanza por pasos, baja de fila y cambia de sentido al tocar un borde.
Retos: enemigos que disparen, varias vidas, niveles cada vez más rápidos, barreras destructibles.
Proyecto 4: juego de plataformas
Introduce la gravedad, el salto y la resolución de colisiones por ejes: primero se mueve en x y se corrigen choques; después se mueve en y y se corrigen. Hacerlo por separado evita atascos en esquinas y que el personaje atraviese el suelo.
GRAVEDAD = 1800 # px/s²
VEL_SALTO = 650 # px/s
# Eje X
self.pos.x += self.vel.x * dt
self.rect.x = round(self.pos.x)
for p in plataformas:
if self.rect.colliderect(p):
if self.vel.x > 0:
self.rect.right = p.left
elif self.vel.x < 0:
self.rect.left = p.right
self.pos.x = self.rect.x
# Eje Y
self.vel.y += GRAVEDAD * dt
self.pos.y += self.vel.y * dt
self.rect.y = round(self.pos.y)
for p in plataformas:
if self.rect.colliderect(p):
if self.vel.y > 0: # cayendo: aterriza
self.rect.bottom = p.top
elif self.vel.y < 0: # subiendo: golpea el techo
self.rect.top = p.bottom
self.vel.y = 0
self.pos.y = self.rect.y
# ¿Hay suelo justo debajo? (evita saltos que fallan por redondeo)
self.en_suelo = any(self.rect.move(0, 1).colliderect(p) for p in plataformas)
La altura máxima de un salto sale de la física básica:
h = v² / (2g)
h = (650 px/s)² / (2 × 1800 px/s²)
h ≈ 117 px
En la práctica el salto resulta algo menor por el redondeo a píxeles enteros. Si quieres una altura concreta h, despeja v = √(2gh). Un truco útil: limita dt (por ejemplo min(dt, 0.05)) para que un cuadro muy largo no haga atravesar el suelo al personaje.
Retos: monedas, enemigos que patrullen, «coyote time» (permitir saltar unos milisegundos después de dejar una plataforma), niveles definidos con una lista de cadenas de texto.
Paso 6: diseña y desarrolla tu propio juego
Diseña antes de programar
Escribe una página con las respuestas a estas preguntas:
- ¿Qué hace el jugador? El verbo central en una frase: esquivar, disparar, resolver, saltar.
- ¿Cómo se gana y cómo se pierde? Vidas, tiempo, puntuación.
- ¿Cuáles son los controles?
- ¿Qué objetos existen? Jugador, enemigos, proyectos, obstáculos, con sus atributos.
- ¿Qué pantallas hay? Menú, juego, pausa, fin de partida.
- ¿Cuál es el alcance mínimo? La versión más pequeña que ya sea divertida.
El error más común de quien empieza es diseñar un juego enorme y abandonarlo. Define un producto mínimo jugable que puedas terminar en dos o tres semanas.
Maneja las pantallas con una máquina de estados
Una variable de estado y una función por pantalla mantienen el código ordenado:
estado = "menu" # "menu", "jugando", "pausa", "fin"
while ejecutando:
dt = reloj.tick(FPS) / 1000
eventos = pygame.event.get()
if estado == "menu":
estado = pantalla_menu(pantalla, eventos)
elif estado == "jugando":
estado = pantalla_juego(pantalla, eventos, dt)
elif estado == "pausa":
estado = pantalla_pausa(pantalla, eventos)
elif estado == "fin":
estado = pantalla_fin(pantalla, eventos)
pygame.display.flip()
Cada función procesa eventos, dibuja y devuelve el nombre del estado siguiente.
Organiza el código
- Pon velocidades, tamaños y colores en constantes con nombre, en un único archivo (
config.py). - Crea una clase por tipo de objeto, con métodos
actualizar(dt)ydibujar(pantalla). - Separa la lógica del dibujo: podrás cambiar los gráficos sin romper las reglas.
- Agrupa el estado del juego en un objeto en lugar de usar variables globales.
Desarrolla por iteraciones
- Prototipo gris: solo rectángulos de color y la mecánica central. Si así no es divertido, no lo será con gráficos.
- Reglas completas: puntuación, vidas, victoria y derrota.
- Contenido: niveles, enemigos y objetos.
- Gráficos y sonido.
- Pulido: menús, pausa, dificultad.
- Pruebas con otras personas: observa a alguien jugar sin darle instrucciones.
Depuración
- Muestra los FPS con
reloj.get_fps()durante el desarrollo. - Dibuja los
Rectde colisión (pygame.draw.rect(pantalla, (255, 0, 0), rect, 1)) para ver qué choca realmente. - Si el juego va lento, mide antes de optimizar: casi siempre se cargan recursos dentro del bucle o se dibuja de más.
Paso 7: errores frecuentes y cómo resolverlos
| Síntoma | Causa probable y solución |
|---|---|
| La ventana «no responde» | No se llama a pygame.event.get() en cada cuadro |
| Pantalla negra | Falta pygame.display.flip() |
| Los objetos dejan estela | No se borra la pantalla con fill() |
| El juego va lento | Se cargan imágenes dentro del bucle; falta convert_alpha() |
| Los objetos solo se mueven de 1 en 1 píxel | Rect guarda enteros: acumula la posición en decimales |
| La velocidad cambia entre equipos | No se usa dt |
| El personaje atraviesa el suelo | Resuelve colisiones por ejes y limita dt |
FileNotFoundError al cargar imágenes | Usa Path(__file__).parent para construir las rutas |
| Teclas que fallan o se repiten | Confusión entre KEYDOWN y get_pressed() |
Paso 8: versiona, empaqueta y publica
Control de versiones con Git
git init
git add .
git commit -m "Primera versión jugable"
Crea un archivo .gitignore con al menos:
.venv/
__pycache__/
build/
dist/
Después puedes subir el repositorio a GitHub para compartir el código.
Crear un ejecutable con PyInstaller
python -m pip install pyinstaller
pyinstaller --onefile --windowed --name MiJuego --add-data "assets;assets" main.py
En Linux y macOS el separador de --add-data es : en lugar de ;. El ejecutable queda en la carpeta dist/. Debes empaquetar en el sistema operativo de destino: un ejecutable de Windows solo funciona en Windows.
Al empaquetar, las rutas a assets/ pueden dejar de funcionar. Se soluciona con una función que decide la ruta base:
import sys
from pathlib import Path
def ruta_recurso(relativa):
base = Path(getattr(sys, "_MEIPASS", Path(__file__).parent))
return base / relativa
Prueba siempre el ejecutable en otro equipo antes de distribuirlo; algunos antivirus marcan falsos positivos en archivos generados con PyInstaller.
Publicar y compartir
- Crea una página de proyecto en itch.io (o Game Jolt).
- Sube un
.zippor sistema operativo, con capturas de pantalla, controles y créditos. - Declara las licencias de los recursos de terceros.
- Publica el código en GitHub con un
READMEque explique cómo ejecutarlo. - Recoge comentarios, corrige errores y lanza nuevas versiones.
Terminar y publicar un juego pequeño enseña más que empezar diez grandes. Las game jams (por ejemplo, las de itch.io) son una buena forma de practicar con plazos cortos.
Recursos para seguir aprendiendo
- Documentación oficial de pygame: pygame.org/docs, para consultar módulos, clases y funciones.
- Tutorial oficial de Python: docs.python.org/3/tutorial.
- Libros: Automate the Boring Stuff with Python y Python Crash Course para el lenguaje; Making Games with Python & Pygame, de Al Sweigart, para juegos completos (comprueba que el código corresponda a pygame 2.x).
- Comunidades: Stack Overflow y r/pygame. Al pedir ayuda, incluye el mensaje de error completo y el fragmento mínimo de código que falla.
Ruta de aprendizaje orientativa
| Etapa | Duración aproximada |
|---|---|
| Python básico | 1 a 3 semanas |
| Entorno y fundamentos de pygame | 1 semana |
| Pong y Serpiente | 1 a 2 semanas |
| Invasores y Plataformas | 2 a 4 semanas |
| Juego propio | 3 a 8 semanas |
| Publicación | 1 semana |
Conclusión
Crear videojuegos con Python y pygame se reduce a dominar una idea (el bucle de eventos, lógica, dibujo y control del tiempo) y a practicarla con proyectos cada vez más ambiciosos. Empieza pequeño, escribe tú mismo el código en lugar de copiarlo, termina lo que empiezas y publica. Con constancia, en pocas semanas tendrás un juego propio que cualquiera pueda jugar.
Nota: los fragmentos de código de Pong, el esqueleto y el movimiento con dt los ejecuté y funcionan con pygame 2.6.1. Los de sprites y plataformas se probaron como parte de programas completos. Contrasta las ediciones de los libros citados con la que consultes.