Currículo: esta unidad cubre parte de los saberes básicos del Bloque E – Programación (TICO.1.E.1 y TICO.1.E.2) correspondiente a 1º Bachillerato. Además, se evalúan los criterios que puedes encontrar al final de esta página.
Tabla de contenidos
- 0. Preparando el entorno de desarrollo
- 1. Instrucciones básicas y escenario.
- 2. Nuestro personaje empieza a moverse.
- 3. Disparar proyectiles.
- 4. Aparecen los enemigos.
- 5. Sistema de puntuación.
- 6. Perder la partida.
- 7. Sistema de vidas.
- 8. Varios enemigos al mismo tiempo.
- 9. Explosiones.
- 10. Sonido.
- 11. Enemigos más inteligentes
- 12. Múltiples disparos.
- 13. Temporizador de disparo
- 14. Enemigos que disparan
- 15. Fondo en movimiento
- 16. Pantallas de inicio y reinicio
- 18. ¿Y ahora qué? – Como seguir aprendiendo por tu cuenta.
- 19. El último consejo
Este es un itinerario de los apuntes del Tema 11 – Fundamentos de programación que utiliza el camino de “aprender a programar jugando” en lugar de el aprendizaje formal de cada uno de los elementos del lenguaje Python.
En este caso usaremos Windows 11.
0. Preparando el entorno de desarrollo
Vamos a aprender a programar en Python.
Para ello tenemos que utilizar una herramienta de programación llamada Visual Studio Code.
Lo instalamos.
Luego desde Chrome nos vamos a la sección de descargas de la web de Python.
Le damos hacia abajo y descargamos la versión 3.13.13 para Windows (64bit):

Durante el proceso de instalación sólo debemos tener la precaución primero de tener estas dos opciones marcadas:

Y segundo, pulsar en esta opción:

Cuando termine, nos vamos a la Terminal de Visual Studio Code. Si no está abierta puedes abrirla pulsando CTRL ñ.
Escribimos python –version
Y pulsamos INTRO.

Debe salirnos: Python 3.13.13
Así confirmamos que tenemos el lenguaje y su motor de ejecución correctamente instalados.
Lo que haremos ahora es instalar dos librerías que nos ayudarán a programar pequeños videojuegos.
Desde la terminal ejecutamos: pip install pygame
Esperamos que se instale.
Desde la terminal ejecutamos: pip install pgzero
Esperamos que se instale.
Para comprobar que tenemos todo lo necesario vamos a intentar abrir un juego de ejemplo que viene ya programado.
Así que, desde la terminal ejecutamos: python -m pygame.examples.aliens
Si se te abre una ventana con el juego es que todo ha ido bien y ahora sí, tenemos todo lo que necesitamos para empezar a aprender a programar:

1. Instrucciones básicas y escenario.
Hoy no vamos a estudiar Python “como en un libro”. Hoy vamos a empezar a pensar como desarrolladores de videojuegos.
Todos los videojuegos que conoces: Minecraft, Fortnite, Rocket League, etc., por complejos que parezcan, están construidos a partir de ideas muy simples:
- Un personaje.
- Un escenario.
- Reglas.
- Interacción.
Nuestro objetivo en esta unidad será aprender a construir exactamente eso… pero desde cero.
Lo primero que debéis entender es esto: un ordenador no piensa, no imagina, no interpreta.
Solo ejecuta instrucciones, una detrás de otra.
Si el código dice:
print("Hola mundo")Python no “entiende” el saludo. Simplemente recibe una orden y la ejecuta.
Es igual que cuando en Code.org movíais un personaje diciendo: Avanza, Gira o Salta.
Aquí hacemos exactamente lo mismo, pero escribiéndolo nosotros.
Probad este código:
print("Hola, soy mi primer programa")
print("Hoy empiezo a programar videojuegos")Ejecutadlo y observad el resultado.
Ya habéis creado vuestro primer programa real.
Ahora piensa que en un videojuego tenemos al menos un jugador. Ese jugador necesita datos: su nombre, su puntuación y sus vidas.
Eso se guarda en variables.
Escribid:
nombre = "Alex" vidas = 3 puntuacion = 0 print(nombre) print(vidas) print(puntuacion)
Aquí estamos guardando información que luego podremos cambiar.
Esto será fundamental cuando creemos enemigos, monedas, personajes o niveles.
Ahora llega la parte interesante.
Vamos a crear una ventana de videojuego con Pygame Zero.
Cread un archivo llamado: juego.py
Y escribid:
WIDTH = 800 HEIGHT = 600 def draw(): screen.clear() screen.draw.text( "Mi primer videojuego en Python", center=(400,300), fontsize=50 )
Para ejecutar este programa, ve a la Terminal y lanza: python -m pgzero juego.py
Y ocurrirá algo importante: por primera vez no habéis hecho un programa de consola.
Habéis creado una ventana de videojuego.
Eso significa que ya estáis trabajando como desarrolladores.
Vamos a analizar lo que acabamos de hacer.
Cuando escribimos WIDTH y HEIGHT, estamos definiendo el tamaño de nuestra pantalla.
Cuando usamos draw(), estamos diciendo: “Python, cada vez que el juego necesite dibujar la pantalla, ejecuta este código“.
Esto ya es pensamiento computacional aplicado al desarrollo real.
Sin darnos cuenta, estamos entrando en conceptos profesionales como: tiempo de ejecución, ciclo de juego, renderizado o eventos.
Y todo eso usando muy pocas líneas.
Es posible que al ejecutar el programa te haya aparecido la ventana de tu videojuego algo esquinada. Para no tener que estar constantemente moviendo la ventana con cada nueva ejecución del programa, vamos a situar estas dos líneas al inicio de nuestro programa. Así no tendrás que volver a preocuparte por la posición de la ventaja del juego.
import os os.environ["SDL_VIDEO_CENTERED"] = "1"
Ejercicio 11.1 – Modificar el código base
Ahora quiero que cada uno personalice su programa. Cambiad: el texto, el tamaño de la ventana, la posición del mensaje y el tamaño de la letra. Probad varias configuraciones hasta que encontréis una que os guste para vuestro juego.
Hasta ahora habéis conseguido algo importante: escribir código en Python y hacer que aparezca una ventana de videojuego usando Pygame Zero.
Pero ahora aparece una pregunta importante:
¿Cómo es posible que la ventana siga ahí, aunque nosotros no estemos escribiendo más código?
La respuesta está en uno de los conceptos más importantes del desarrollo de videojuegos: el ciclo de juego o game loop.
Cuando ejecutamos nuestro programa, el ordenador no dibuja la pantalla una sola vez. En realidad, está repitiendo constantemente un proceso parecido a este:
- Primero borra la pantalla anterior.
- Después vuelve a dibujar todos los elementos.
- Después comprueba si el jugador ha pulsado alguna tecla.
- Después vuelve a empezar.
Y esto ocurre decenas de veces por segundo.
En un videojuego real, ese ciclo ocurre tan rápido que nuestros ojos lo perciben como movimiento continuo.
Vamos a comprobarlo con un pequeño experimento.
Cread este archivo:
WIDTH = 800
HEIGHT = 600
contador = 0
def update():
global contador
contador += 1
def draw():
screen.clear()
screen.draw.text(str(contador), center=(400, 300), fontsize=60,color="white")Ahora ejecutadlo.
¿Qué ocurre?
El número sube sin parar.
Eso demuestra que draw() no se ejecuta una sola vez. Se está ejecutando continuamente.
Acabáis de descubrir cómo piensa un videojuego.
Ahora que terminamos esta primera sesión quiero que tengáis clara una idea: programar no consiste en memorizar comandos. Consiste en convertir una idea en instrucciones que una máquina pueda ejecutar.
En el siguiente haremos que los objetos se muevan por pantalla con el teclado. Ahí empezarán a ponerse las cosas interesantes.
2. Nuestro personaje empieza a moverse.
Hasta ahora hemos conseguido algo importante: sabemos abrir una ventana de videojuego y dibujar elementos en pantalla.
Pero un videojuego donde no podemos mover nada… se parece más a una presentación de PowerPoint con esteroides que a un juego.
Avancemos al siguiente paso: controlar nuestro primer personaje con el teclado.
Y con esto empieza lo realmente divertido.
Antes de escribir código, tenemos que entender una idea fundamental.
Cada objeto tiene una posición en pantalla definida por dos números:
- X → posición horizontal (izquierda-derecha).
- Y → posición vertical (arriba-abajo).
Visualmente sería algo parecido a esto:

Eso significa que si cambiamos la posición X o Y de un objeto… el objeto se moverá.
Y eso es exactamente lo que vamos a hacer.
Primero vamos a crear una carpeta dentro de nuestro proyecto llamada: images.
Dentro de ella vamos a guardar una imagen llamada: nave.png
Puede ser cualquier «sprite» sencillo: una nave, un personaje, una pelota… aunque una nave queda bastante épica 😅 para empezar.
Un sprite es una imagen 2D que usamos dentro de un videojuego para representar un objeto en pantalla.
Algo así:

Para nuestro caso podemos usar uno así:

Creamos un nuevo archivo y escribimos:
WIDTH = 800
HEIGHT = 600
nave = Actor("nave")
nave.pos = (400, 300)
def draw():
screen.clear()
nave.draw()Y debería aparecer nuestra nave en pantalla.
Acabamos de aprender una herramienta nueva: Actor
Un Actor es un objeto del juego.
Puede ser: una nave, un enemigo, una moneda, una explosión,…
Recuerda que para lanzar nuestro programa usamos: python -m pgzero juego.py
Ahora viene la magia. Vamos a añadir una funcionalidad nueva: mover nuestro personaje.
WIDTH = 800
HEIGHT = 600
nave = Actor("nave")
nave.pos = (400, 300)
def update():
if keyboard.left:
nave.x -= 5
if keyboard.right:
nave.x += 5
if keyboard.up:
nave.y -= 5
if keyboard.down:
nave.y += 5
def draw():
screen.clear()
nave.draw()En cada actualización de la pantalla el juego comprueba si se ha pulsado una tecla. Dependiendo de la tecla que se detecte movemos al actor 5 píxeles en la dirección indicada por el teclado. Usamos 5 píxeles en lugar de 1 para que el movimiento sea más ágil, no tan lento.
¿Detectas algún problema de jugabilidad?
Por si no te has dado cuenta, si mantenemos pulsada una flecha la nave puede escaparse fuera de la pantalla.
Y eso, salvo que estemos haciendo un simulador de abandono espacial, no suele interesarnos.
Vamos a arreglarlo.
Simplemente, ajustamos nuestra función update():
WIDTH = 800
HEIGHT = 600
nave = Actor("nave")
nave.pos = (400, 300)
def update():
if keyboard.left and nave.x > 0:
nave.x -= 5
if keyboard.right and nave.x < WIDTH:
nave.x += 5
if keyboard.up and nave.y > 0:
nave.y -= 5
if keyboard.down and nave.y < HEIGHT:
nave.y += 5
def draw():
screen.clear()
nave.draw()Ahora la nave queda encerrada dentro de la pantalla, como cualquier protagonista de videojuego clásico.
Nadie sale del mapa sin permiso del programador.
Ejercicio 11.2 – Experimentar con el movimiento
Queremos personalizar nuestro juego:
- Cambiad la imagen de la nave.
- Probad distintas velocidades: 2, 5, 10, 30.
- Responded: ¿Cuál parece desesperadamente lento? ¿Qué velocidad hace que el control sea más cómodo? ¿Cuál hace que el personaje parezca demasiado rápido?
3. Disparar proyectiles.
Estupendo.
Tenemos una nave que se mueve por la pantalla. Pero claro, una nave espacial que no dispara es básicamente un taxi con luces.
Ahora toca añadir una mecánica fundamental en muchísimos videojuegos: disparar proyectiles.
Para disparar necesitamos tres cosas:
- Una imagen para el proyectil.
- Crear el proyectil cuando pulsemos una tecla.
- Hacer que el proyectil se mueva.
Bueno pues vamos a ello.
Primero guardamos una imagen llamada: laser.png dentro de la carpeta images.

Vamos a crear un nuevo Actor llamado laser y luego dibujarlo en draw():
WIDTH = 800
HEIGHT = 600
nave = Actor("nave")
nave.pos = (400, 500)
laser = Actor("laser")
laser.pos = nave.pos
def update():
if keyboard.left and nave.x > 0:
nave.x -= 5
if keyboard.right and nave.x < WIDTH:
nave.x += 5
if keyboard.up and nave.y > 0:
nave.y -= 5
if keyboard.down and nave.y < HEIGHT:
nave.y += 5
def draw():
screen.clear()
nave.draw()
laser.draw()Si ejecutamos ahora, veremos el láser encima de la nave.
Todavía no dispara, pero ya existe.
Nuestro juego será horizontal, como muchos juegos de naves laterales.
Eso significa que:
- la nave estará a la izquierda,
- los enemigos aparecerán por la derecha,
- y nuestros disparos viajarán de izquierda a derecha.
Así que primero colocamos la nave en el lado izquierdo colocándola en la posición (100,300).
Y ahora vamos a hacer que el láser avance hacia la derecha, por lo que tenemos que mover la coordenada x. Esto es algo que ya sabemos hacer porque es exactamente lo mismo que cuando movíamos a la nave: laser.x += 8
Aquí quedaría de momento el juego:
WIDTH = 800
HEIGHT = 600
nave = Actor("nave")
nave.pos = (100, 300)
laser = Actor("laser")
laser.pos = nave.pos
def update():
if keyboard.left and nave.x > 0:
nave.x -= 5
if keyboard.right and nave.x < WIDTH:
nave.x += 5
if keyboard.up and nave.y > 0:
nave.y -= 5
if keyboard.down and nave.y < HEIGHT:
nave.y += 5
laser.x += 8
def draw():
screen.clear()
nave.draw()
laser.draw()Puede que ni hayas visto el láser porque se lanza una sola vez.
¿Y eso?
Tenemos el mismo problema de antes: el láser sale disparado desde el primer segundo y desaparece por el margen derecho de la pantalla viajando hacia la derecha eternamente.
Vamos a arreglarlo.
Queremos disparar solamente cuando pulsemos ESPACIO.
Para eso vamos a crear una variable nueva: laser_activo = False
Esta variable nos dice si el disparo está activo o no.
El código completo queda así:
WIDTH = 800
HEIGHT = 600
nave = Actor("nave")
nave.pos = (100, 300)
laser = Actor("laser")
laser.pos = nave.pos
laser_activo = False
def update():
global laser_activo
if keyboard.left and nave.x > 0:
nave.x -= 5
if keyboard.right and nave.x < WIDTH:
nave.x += 5
if keyboard.up and nave.y > 0:
nave.y -= 5
if keyboard.down and nave.y < HEIGHT:
nave.y += 5
if keyboard.space and laser_activo == False:
laser.pos = nave.pos
laser_activo = True
if laser_activo:
laser.x += 8
if laser.x > WIDTH:
laser_activo = False
def draw():
screen.clear()
nave.draw()
if laser_activo:
laser.draw()La lógica que vemos programada es establecer la variable laser_activo a False cuando ha desaparecido de la pantalla, y a True cuando está visible en la pantalla de juego.
De esta manera, lo inicializamos a False, el primer if comprueba si se ha pulsado la tecla Espacio y la variable está a False, y si es así, colocamos el láser en su sitio (sobre la nave) y cambiamos la variable a True.
Después otro if lo que hace es mover al láser mientras esté en pantalla (variable a True).
Finalmente, otro if comprueba si el láser ha escapado por el margen derecho de la pantalla para establecer la variable nuevamente a False.
Si te fijas, el laser aparece desde el centro de la nave, algo que la verdad no es demasiado realista. Vamos a intentar mejorarlo haciendo que el láser salga desde el morro de la nave: laser.pos = (nave.x + 40, nave.y)
Es decir, en vez de colocarlo en la misma posición, lo colocamos 40px más a la derecha y listo.
Aunque quizás te parezca poca cosa, acabamos de aprender una idea importante, y es que los objetos del juego pueden tener estados. Nuestro láser puede estar activo o inactivo. Y según su estado, el juego se comporta de una forma u otra.
Esto será fundamental cuando tengamos: enemigos vivos o destruidos, monedas recogidas,
vidas del jugador o explosiones, entre muchas otras dinámicas de juego.
Ejercicio 11.3 – Experimentar con el disparo
Ahora toca experimentar con el disparo.
- Cambiad la velocidad del disparo: 5, 8, 12, 20,…
- Cambiad la posición inicial del láser.
- Probad diferentes sprites para el disparo.
- Responded: ¿Qué velocidad hace el juego más divertido? ¿Qué velocidad hace que sea demasiado fácil? ¿Cuál parece demasiado lenta?
4. Aparecen los enemigos.
Ya somos capaces de mover nuestra nave, disparar proyectiles, controlar cuándo un disparo está activo, pero ¡no ha nadie a quién disparar!
Toca añadir nuestro primer enemigo.
Y aquí es donde un videojuego empieza a sentirse como un videojuego de verdad.
Primero necesitamos un nuevo sprite.
Así que, como siempre, dentro de la carpeta images, guardamos una imagen llamada: enemigo.png
Puede ser una nave enemiga, un meteorito, un dron… o esa bromita típica que alguno seguro intentará poner.
En mi caso, voy a usar esta:

Debajo del código de nuestra nave y nuestro láser, añadimos:
enemigo = Actor("enemigo")
enemigo.pos = (700, 300)¿Dónde aparece?
En el lado derecho de la pantalla. Exactamente donde queremos.
Ahora modificamos draw():
def draw():
screen.clear()
nave.draw()
if laser_activo:
laser.draw()
enemigo.draw()Ejecutamos.
Y por primera vez tendremos dos personajes en pantalla: nuestra nave a la izquierda y el enemigo esperando a la derecha.
🚨 Se palpa tensión.
Un enemigo quieto no impresiona demasiado.
Vamos a hacerlo avanzar hacia nosotros.
Dentro de update() añadimos: enemigo.x -= 3
¿Por qué -=?
Porque queremos que venga desde la derecha hacia la izquierda.
Ahora el enemigo empieza a acercarse.
Si dejas correr el juego unos segundos podrás comprobar el mismo problema de todos los actores, que desaparecen de la pantalla.
En esta ocasión lo que vamos a hacer es que en cuanto desaparezca por la izquierda, vuelva a aparecer por la derecha.
enemigo.x -= 3
if enemigo.x < 0:
enemigo.x = WIDTHEstupendo, tenemos enemigos infinitos que ¡nos atraviesan! 🤦♀️
Necesitamos programar colisiones.
Ahora llega la parte divertida.
Queremos saber: ¿Ha golpeado nuestro láser al enemigo?
Python nos lo pone fácil. Añadimos esto al final de update():
if laser_activo and laser.colliderect(enemigo):
print("¡Impacto!")Ejecutamos el juego, y cuando el disparo toque al enemigo… en la terminal aparecerá: ¡Impacto!
Acabamos de detectar nuestra primera colisión.
Y esto es enorme.
Porque la base de muchísimos videojuegos es precisamente esta pregunta: ¿Han chocado dos objetos?
Pero claro, de nada sirve detectar el impacto si el enemigo no es destruido. Así que, vamos a ello.
Ahora no queremos solo imprimir un mensaje. Queremos que el enemigo desaparezca y vuelva a aparecer. Sustituimos el código anterior por:
if laser_activo and laser.colliderect(enemigo):
laser_activo = False
enemigo.x = WIDTH
enemigo.y = random.randint(50, HEIGHT - 50)Ahora, cada vez que acertemos: el disparo desaparece, el enemigo vuelve a empezar, aparece a una altura diferente. Y eso hace que el juego sea mucho más dinámico.
En este apartado hemos introducido varios conceptos nuevos: crear nuevos actores, movimiento automático, colisiones, números aleatorios y reaparición de enemigos. Sin darnos cuenta, acabamos de construir la base de: un shooter, un juego de naves, un matamarcianos o incluso un juego de supervivencia.
Ejercicio 11.4 – Experimentar con los enemigos
Ahora toca experimentar:
- Cambiad la velocidad del enemigo.
- Probad diferentes sprites.
- Haced que el enemigo aparezca más arriba o más abajo.
- Intentad responder: ¿Qué ocurre si el enemigo es demasiado rápido? ¿Y si es demasiado lento? ¿Dónde está el equilibrio?
5. Sistema de puntuación.
Hasta ahora nuestro juego ya tiene una nave, disparos, enemigos y colisiones. Pero todavía falta algo fundamental en casi cualquier videojuego: los puntos.
Igual que hicimos con el estado del láser, vamos a guardar la puntuación en una variable. Así que podemos añadir esto, justo debajo de la variable del láser:
laser_activo = False puntos = 0
Esta variable irá aumentando cada vez que destruyamos un enemigo.
Ahora queremos que aparezcan visibles durante la partida.
Dentro de draw() añadimos:
screen.draw.text(
"Puntos: " + str(puntos),
topleft=(20,20),
fontsize=40,
color="white"
)El draw() quedaría así:
def draw():
screen.clear()
nave.draw()
if laser_activo:
laser.draw()
enemigo.draw()
screen.draw.text(
"Puntos: " + str(puntos),
topleft=(20,20),
fontsize=40,
color="white"
)Vamos a hacer ahora que se sumen puntos cuando se destruyen enemigos. Básicamente lo que debemos hacer es localizar la parte del código donde se detecta la colisión y aumentar la cantidad de puntos conseguidos:
puntos += 1
Prueba el juego y mata al primer enemigo.
¿Qué ha pasado?
Lo que ha ocurrido es que se produce un UnboundLocalError. Y la verdad es que es lógico, porque estamos intentando modificar el valor de una variable global dentro de una función -la función update()-. Para arreglarlo simplemente vamos a decir al comenzar la función que vamos a usar la variable global puntos igual que hacíamos con la del láser:
#.....
def update():
global laser_activo
global puntos
#...Ahora ya tenemos la recompensa visual cada vez que destruimos un enemigo.
Ejercicio 11.5 – Experimentar con la puntuación
Vamos a mejorar el sistema:
- Cambiad la cantidad de puntos obtenidos: +1, +10, +100.
- Cambiad: tamaño del texto, color, posición.
- Intentad añadir también aunque todavía no funcione: record=0.
- Responded: ¿Qué hace más satisfactorio un juego? ¿Destruir enemigos? ¿Ver subir números? ¿Las dos cosas? Spoiler: la industria del videojuego lleva décadas explotando exactamente eso.
6. Perder la partida.
Si un videojuego no puede derrotarnos, normalmente deja de tener emoción bastante rápido. Así que vamos a ponerle precio a nuestra vida.
¿Cuándo perdemos?
Vamos a decidir una regla sencilla: si el enemigo toca nuestra nave, la partida termina.
Eso significa que necesitamos detectar una colisión entre: enemigo y nave.
Y eso ya sabemos hacerlo.
Por otro lado, igual que hicimos con laser_activo, vamos a crear una variable nueva game_over para controlar si seguimos vivos o no.
Esta variable nos dirá:
False→ seguimos jugando.True→ la partida ha terminado.
Para detectar la derrota, añadimos al final de update():
#.....
if enemigo.colliderect(nave):
game_over = True
#...Pero aparecerá el problema recurrente de estar modificando una variable global. Así que arriba de update() añadimos:
#..... global game_over #...
Ejecutad el juego.
Cuando el enemigo toque la nave… no parece pasar nada visible. Pero internamente game_over ha cambiado a True y eso significa que la partida ya sabe que hemos perdido.
Ahora toca mostrárselo al jugador.
Dentro de draw(), al final, añadimos:
if game_over:
screen.draw.text(
"GAME OVER",
center=(400,300),
fontsize=80,
color="red"
)Ahora sí, ejecuta el juego y observa.
Mmmm, ocurre algo raro. Aunque aparece GAME OVER, todo sigue moviéndose. El enemigo continúa. La nave responde. Los disparos siguen funcionando.
Es decir, el juego está “muerto”, pero muy activo al mismo tiempo. Eso suele pasar bastante en programación.
Vamos a arreglarlo deteniendo todos los elementos del juego.
Al principio de update() añadimos:
if game_over:
returnEso significa “Si el juego está en GAME OVER deja de ejecutar esta función inmediatamente“. Lo hace, el juego sale del bucle principal y acaba.
Acabamos de añadir una de las mecánicas más importantes de cualquier videojuego: la posibilidad de perder. Y aunque parezca sencillo, esto introduce conceptos muy importantes: estados del juego, lógica condicional, detener procesos, control del flujo del programa. Muchos juegos reales funcionan exactamente así: jugando, pausado, game over, menú, victoria… Todo son estados.
Ejercicio 11.6 – Experimentar con el fin de la partida
Vamos a experimentar:
- Cambiad: el color del GAME OVER, el tamaño, la posición.
- Probad otros mensajes:
"Has perdido","Fin de la partida","La galaxia ha caído". - Intentad añadir una segunda condición de derrota: por ejemplo si el enemigo llega al borde izquierdo.
- Responded: ¿Qué hace más emocionante un juego? ¿Poder ganar? ¿Poder perder? ¿Las dos cosas?
Y ahora sí… ya tenemos algo que empieza a parecerse muchísimo a un videojuego completo.
7. Sistema de vidas.
Morir a la primera está bien para Dark Souls. Pero quizá nosotros todavía no queremos sufrir tanto.
Muchos videojuegos usan vidas porque permiten: cometer errores, recuperarse y aprender jugando. Sin vidas, el jugador vive en tensión constante. Con vidas, el jugador tiene margen para equivocarse. Y ese equilibrio es muy importante en diseño de videojuegos.
Para las vidas necesitamos también una variable que situaremos junto a la que controla los puntos:
#... puntos = 0 vidas = 3 #...
Eso significa que el jugador puede recibir tres impactos antes de morir.
Ahora vamos a modificar la lógica de la derrota, cambiando esta parte:
#...
if enemigo.colliderect(nave):
game_over = True
#...Y la sustituimos por:
#...
if enemigo.colliderect(nave):
vidas -= 1
enemigo.x = WIDTH
enemigo.y = random.randint(50, HEIGHT - 50)
if vidas <= 0:
game_over = True
#...Para hacer que el enemigo se resitúe en el margen derecho de la pantalla. Eso evita un problema importante: que el enemigo se quede “pegado” a la nave quitando todas las vidas a la vez.
Y además, que si agota sus vidas marquemos el juego como terminado.
Finalmente añadimos el calificador global también a esta variable vidas al inicio de uppdate():
#...
def update():
global laser_activo
global puntos
global game_over
global vidas
#...Muy bien, pero al igual que los puntos, queremos que el jugador sepa en todo momento cuántas vidas le quedan. En draw() añadimos:
#...
screen.draw.text(
"Vidas: " + str(vidas),
topleft=(20,70),
fontsize=40,
color="white"
)
#...Ahora nuestra pantalla tiene: la nave, enemigos, disparos, puntuación, vidas, game over. Y todo eso lo hemos construido desde cero. Sin motores gigantes. Sin plantillas. Sin copiar un juego entero de internet. Solo programando poco a poco. Y eso tiene muchísimo mérito para ser tu primera experiencia de programación.
Ejercicio 11.7 – Jugando con las vidas
- Cambiad el número inicial de vidas.
- Reflexiona: ¿Qué hace más divertido un juego? ¿Mucha dificultad? ¿Mucha facilidad? ¿Un equilibrio?
- Intentad cambiar el color del texto de vidas cuando quede solo una.
Pelear uno contra uno está muy bien para programar, pero para jugar es bastante aburrido. Vamos a por la programación de varios enemigos al mismo tiempo.
Y ahí…el caos empezará oficialmente.
8. Varios enemigos al mismo tiempo.
Con la excusa de programar varios enemigos, empezaremos a trabajar con una de las herramientas más importantes de la programación: las listas.
Cuando hacemos esto:
#...
enemigo = Actor("enemigo")
#...Eso crea un único enemigo. Pero… ¿y si queremos 5? ¿O 20? ¿O 200? No vamos a crear cada uno de los enemigos escribiendo esa instrucción una y otra vez. Necesitamos una estrategia mejor.
Necesitamos una lista.
Una lista es una colección de elementos. Por ejemplo:
# podemos tener una lista de números numeros = [3, 7, 2, 9] # podemos tener una lista de cadenas de caracteres nombres = ["Ana", "Luis", "Marta"] # y también una lista de objetos enemigos = [enemigo1, enemigo2, enemigo3]
La idea es guardar muchos elementos dentro de una sola variable. Y eso nos permitirá controlar muchísimos enemigos fácilmente.
Empecemos a crear enemigos. Vamos a sustituir:
enemigo = Actor("enemigo")
enemigo.pos = (700, 300)Por:
enemigos = []
Eso crea una lista vacía.
Ahora añadimos este código debajo:
for i in range(5):
enemigo = Actor("enemigo")
enemigo.x = random.randint(800, 1600)
enemigo.y = random.randint(50, HEIGHT - 50)
enemigos.append(enemigo)La línea 1 significa: repite lo que viene a continuación 5 veces.
Después creamos un enemigo, lo colocamos aleatoriamente en pantalla y lo añadimos a la lista de enemigos.
A la hora de mostrar los enemigos también tenemos que modificar la función que lo hace:
# sustituimos esto:
enemigo.draw()
# por esto otro:
for enemigo in enemigos:
enemigo.draw()También tenemos que modificar el movimiento del enemigo para que se aplique el código a todos los enemigos, no sólo a uno:
# sustituimos esto:
enemigo.x -= 3
# por esto otro:
for enemigo in enemigos:
enemigo.x -= 3Y también, la funcionalidad de desaparecer por la izquierda y reaparecer por la derecha:
# sustituimos esto:
if enemigo.x < 0:
enemigo.x = WIDTH
# por esto otro:
if enemigo.x < 0:
enemigo.x = WIDTH
enemigo.y = random.randint(50, HEIGHT - 50)Todo junto quedaría así:
for enemigo in enemigos:
enemigo.x -= 3
if enemigo.x < 0:
enemigo.x = WIDTH
enemigo.y = random.randint(50, HEIGHT - 50)Ahora todos los enemigos se mueven. Y de repente… la pantalla empieza a parecer bastante más peligrosa.
Vayamos a ajustar las colisiones.
Antes comprobábamos:
laser.colliderect(enemigo)
Pero ahora necesitamos revisarlos todos. Así que hacemos:
for enemigo in enemigos:
if laser_activo and laser.colliderect(enemigo):
laser_activo = False
puntos += 1
enemigo.x = WIDTH
enemigo.y = random.randint(50, HEIGHT - 50)Hacemos lo mismo con las vidas, porque todos pueden hacernos daño:
for enemigo in enemigos:
if enemigo.colliderect(nave):
vidas -= 1
enemigo.x = WIDTH
enemigo.y = random.randint(50, HEIGHT - 50)
if vidas <= 0:
game_over = TrueAhora, gracias a las listas podríamos tener muchas posibilidades. Se me ocurre: hacer enemigos gigantes, crear tipos distintos de enemigos, cambiar velocidades, crear oleadas,… Seguro que a ti se te ocurren muchas otras.
Ejercicio 11.8 – Las hordas de enemigos
- Probad distintas velocidades.
- Intenta responder: ¿Cuándo el juego empieza a ser injusto? ¿Cuándo deja de ser divertido? Eso es diseño de dificultad. Y es muchísimo más complicado de lo que parece.
Los videojuegos reales funcionan continuamente recorriendo listas: enemigos, balas, partículas, objetos, jugadores, etc. Que domines las listas te da unas capacidades como programador muy importantes.
En la próxima sesión añadiremos algo espectacular: explosiones. Porque destruir enemigos sin explosiones… es técnicamente correcto, pero visualmente muy triste.
9. Explosiones.
Cuando destruimos un enemigo simplemente desaparece. Sin ruido. Sin efecto. Sin drama. Y eso en un videojuego espacial queda bastante poco épico. Así que vamos a arreglarlo añadiendo explosiones.
Además, para la narrativa de tus videojuegos, recuerda: si algo explota, automáticamente parece más importante.
Empecemos por localizar y alojar en nuestra carpeta images el sprite de la explosión.
Yo usaré este:

Aquí aparece un problema nuevo. La explosión debe aparecer, durar un instante y desaparecer sola. Es decir, no es un objeto permanente.
Vamos a crear la explosión situando debajo de nuestras variables:
explosion = Actor("explosion")
explosion.x = -100
explosion.y = -100
explosion_activa = False¿Por qué ponemos -100? Porque queremos esconder la explosión fuera de la pantalla. Ahora mismo no debe verse. La explosión solo aparecerá cuando destruyamos un enemigo.
Ahora mostramos la explosión añadiendo en draw():
if explosion_activa:
explosion.draw()Luego tenemos que localizar la colisión del láser con el enemigo y activar la explosión:
if laser_activo and laser.colliderect(enemigo):
laser_activo = False
puntos += 1
explosion.pos = enemigo.pos
explosion_activa = True
enemigo.x = WIDTH
enemigo.y = random.randint(50, HEIGHT - 50)Añadimos en update() lo de siempre, el acceso global de nuestra variable explosion_activa.
Ahora la explosión aparece, pero nunca desaparece. Parece que el enemigo haya explotado eternamente. Necesitamos que dure solo un momento, así que vamos a crear un temporizador de explosión.
Creamos una variable:
tiempo_explosion = 0
La hacemos global:
global tiempo_explosion
Y cuando activemos la explosión:
explosion.pos = enemigo.pos explosion_activa = True tiempo_explosion = 10
¿Qué significa ese 10? Significa que la explosión durará 10 frames. Como el juego se actualiza muchas veces por segundo, será un instante breve.
Para hacer desaparecer la explosión, añadimos esto en el update():
if explosion_activa:
tiempo_explosion -= 1
if tiempo_explosion <= 0:
explosion_activa = FalseLo que acabamos de hacer es: cuando destruimos un enemigo iniciamos el temporizador a 10, después cada frame hacemos -1 para que vaya bajando hasta 0, momento en el que apagamos la explosión.
Esto es un temporizador. Y los temporizadores se usan constantemente en videojuegos.
Recapitulemos y repasemos nuestro videojuego completo hasta ahora:
import random
WIDTH = 800
HEIGHT = 600
# ------------------------------------------------
# NAVE DEL JUGADOR
# ------------------------------------------------
nave = Actor("nave")
nave.pos = (100, 300)
# ------------------------------------------------
# DISPARO
# ------------------------------------------------
laser = Actor("laser")
laser.pos = nave.pos
laser_activo = False
# ------------------------------------------------
# ENEMIGOS
# ------------------------------------------------
enemigos = []
for i in range(5):
enemigo = Actor("enemigo")
enemigo.x = random.randint(800, 1600)
enemigo.y = random.randint(50, HEIGHT - 50)
enemigos.append(enemigo)
# ------------------------------------------------
# EXPLOSIÓN
# ------------------------------------------------
explosion = Actor("explosion")
explosion.x = -100
explosion.y = -100
explosion_activa = False
tiempo_explosion = 0
# ------------------------------------------------
# PUNTOS Y VIDAS
# ------------------------------------------------
puntos = 0
vidas = 3
# ------------------------------------------------
# GAME OVER
# ------------------------------------------------
game_over = False
# ------------------------------------------------
# UPDATE
# ------------------------------------------------
def update():
global laser_activo
global puntos
global vidas
global game_over
global explosion_activa
global tiempo_explosion
# --------------------------------------------
# DETENER EL JUEGO SI HAY GAME OVER
# --------------------------------------------
if game_over:
return
# --------------------------------------------
# MOVIMIENTO DE LA NAVE
# --------------------------------------------
if keyboard.left and nave.x > 0:
nave.x -= 5
if keyboard.right and nave.x < WIDTH:
nave.x += 5
if keyboard.up and nave.y > 0:
nave.y -= 5
if keyboard.down and nave.y < HEIGHT:
nave.y += 5
# --------------------------------------------
# DISPARO
# --------------------------------------------
if keyboard.space and laser_activo == False:
laser.pos = (nave.x + 40, nave.y)
laser_activo = True
# --------------------------------------------
# MOVIMIENTO DEL LÁSER
# --------------------------------------------
if laser_activo:
laser.x += 8
# --------------------------------------------
# DESACTIVAR DISPARO
# --------------------------------------------
if laser.x > WIDTH:
laser_activo = False
# --------------------------------------------
# MOVIMIENTO DE LOS ENEMIGOS
# --------------------------------------------
for enemigo in enemigos:
enemigo.x -= 3
# ----------------------------------------
# REAPARICIÓN
# ----------------------------------------
if enemigo.x < 0:
enemigo.x = WIDTH
enemigo.y = random.randint(50, HEIGHT - 50)
# ----------------------------------------
# COLISIÓN CON EL LÁSER
# ----------------------------------------
if laser_activo and laser.colliderect(enemigo):
laser_activo = False
puntos += 1
explosion.pos = enemigo.pos
explosion_activa = True
tiempo_explosion = 10
enemigo.x = WIDTH
enemigo.y = random.randint(50, HEIGHT - 50)
# ----------------------------------------
# COLISIÓN CON LA NAVE
# ----------------------------------------
if enemigo.colliderect(nave):
vidas -= 1
enemigo.x = WIDTH
enemigo.y = random.randint(50, HEIGHT - 50)
if vidas <= 0:
game_over = True
# --------------------------------------------
# TEMPORIZADOR DE EXPLOSIÓN
# --------------------------------------------
if explosion_activa:
tiempo_explosion -= 1
if tiempo_explosion <= 0:
explosion_activa = False
# ------------------------------------------------
# DRAW
# ------------------------------------------------
def draw():
screen.clear()
# --------------------------------------------
# DIBUJAR NAVE
# --------------------------------------------
nave.draw()
# --------------------------------------------
# DIBUJAR LÁSER
# --------------------------------------------
if laser_activo:
laser.draw()
# --------------------------------------------
# DIBUJAR ENEMIGOS
# --------------------------------------------
for enemigo in enemigos:
enemigo.draw()
# --------------------------------------------
# DIBUJAR EXPLOSIÓN
# --------------------------------------------
if explosion_activa:
explosion.draw()
# --------------------------------------------
# TEXTO DE PUNTOS
# --------------------------------------------
screen.draw.text(
"Puntos: " + str(puntos),
topleft=(20, 20),
fontsize=40,
color="white"
)
# --------------------------------------------
# TEXTO DE VIDAS
# --------------------------------------------
screen.draw.text(
"Vidas: " + str(vidas),
topleft=(20, 70),
fontsize=40,
color="white"
)
# --------------------------------------------
# GAME OVER
# --------------------------------------------
if game_over:
screen.draw.text(
"GAME OVER",
center=(400, 300),
fontsize=80,
color="red"
)Ejercicio 11.9 – La explosión más espectacular
- Experimenta cambiando la duración de la explosión, tamaño del sprite y posición.
- Prueba a crear explosiones gigantes, explosiones muy rápidas, explosiones lentas…
- ¿Qué hace que una explosión “quede bien”? ¿Tamaño, duración, velocidad, sonido, todo junto?
Un profesor de programación de videojuegos que tuve siempre me decía que: “una explosión silenciosa… es básicamente una decepción luminosa“.
Y tenía mucha razón, así que vamos a por el sonido.
10. Sonido.
En este apartado vamos a añadir: sonidos de disparo, explosiones y música. Aquí el juego empezará a sentirse muchísimo más vivo.
Al igual que hicimos para las imágenes, vamos a crear dentro de nuestro proyecto una carpeta llamada sounds, y ahí meteremos los tres archivos de audio que necesitamos para nuestros tres propósitos. En los Efectos de Sonido de Pixabay podéis encontrar todo lo que necesitáis.
Mi carpeta queda así:

Para hacer que suene el láser localizamos la parte de nuestro programa donde se hace laser_activo = True y ponemos:
if keyboard.space and laser_activo == False:
sounds.laser.play()
laser.pos = (nave.x + 40, nave.y)
laser_activo = TrueCuando hacemos laser.play() PyGame Zero busca directamente el archivo laser.wav en la carpeta sounds/ así que el nombre de archivo debe coincidir exactamente o se producirá un error.
Ahora buscamos la parte donde se produce la explosión y añadimos también su efecto de sonido:
if laser_activo and laser.colliderect(enemigo):
sounds.explosion.play()
laser_activo = False
puntos += 1
explosion.pos = enemigo.pos
explosion_activa = True
tiempo_explosion = 10
enemigo.x = WIDTH
enemigo.y = random.randint(50, HEIGHT - 50)Ya esto es otra cosa, el juego ha ganado en inmersión.
Ahora viene algo todavía más importante, la música de fondo, porque la música cambia completamente cómo sentimos un videojuego.
Vamos a crear una nueva carpeta music/ y ahí meteremos el archivo fondo.mp3 que te guste.
Para activar la música, localizamos la zona de las variables principales -al inicio del script- y simplemente ponemos:
music.play("fondo")Por defecto, la música se repite automáticamente, así que tendremos música constante durante toda la partida, como en un videojuego real.
Si queremos podemos ajustar el volumen de la música haciendo:
music.set_volume(0.3)
Siendo:
0→ silencio.0.5→ volumen medio.1→ volumen máximo.
Ejercicio 11.10 – Música maestro
- Probad distintos sonidos para tus disparos, explosiones y música hasta que encuentres la combinación perfecta.
- Ajusta el volumen, la duración y la intensidad de tu música de fondo.
- Piensa: ¿Qué cambia más la sensación del juego? ¿Gráficos? ¿Sonido?, ¿Música?
Si te fijas ya no estamos construyendo solo lógica, estamos construyendo experiencia.
En la próxima sesión añadiremos algo muy importante, enemigos más inteligentes, porque hasta ahora nuestros enemigos tenían un comportamiento completamente predecible, algo que no es lo más divertido del mundo.
11. Enemigos más inteligentes
En esta sección vamos a hacer que los enemigos sigan al jugador, persigan nuestra nave y resulten mucho más peligrosos.
Esto modela una idea importantísima en videojuegos: el comportamiento.
Ahora mismo el enemigo solo hace esto: enemigo.x -= 3, es decir “ve siempre a la izquierda”. Pero queremos algo mejor, queremos que el enemigo intente colocarse a nuestra altura.
Dentro del bucle for enemigo in enemigos:, añadimos esto:
if enemigo.y < nave.y:
enemigo.y += 2
if enemigo.y > nave.y:
enemigo.y -= 2El código es bastante descriptivo: “si el enemigo está más arriba baja, y si está más abajo sube“. La consecuencia inmediata es que el enemigo intenta seguirnos.
Si pruebas ahora el juego empiezas a notar nerviosismo. Tu cerebro reacciona muy fuerte a la persecución. Acaba de aumentar el interés por jugar.
Vamos a ajustar un poco la dificultad.
Ahora mismo usamos 2 como velocidad vertical. Prueba 1, 3, 5, 10, y observa: enemigos lentos, agresivos, casi imposibles. Estás diseñando la dificultad en tiempo real.
Es posible que al mover la nave por el escenario algunos enemigos “vibren” un poco cuando coinciden exactamente con nuestra altura. Podemos suavizarlo usando: abs().
Sustituimos el bloque anterior por este:
if abs(enemigo.y - nave.y) > 5:
if enemigo.y < nave.y:
enemigo.y += 2
if enemigo.y > nave.y:
enemigo.y -= 2La función abs() calcula el valor absoluto de un número. Por ejemplo abs(-10) es 10. Así comprobamos la distancia entre enemigo y nave sin preocuparnos del signo.
Ejercicio 11.11 – Comportamiento de los enemigos
- Cambiad la velocidad horizontal y vertical de los enemigos para observar el efecto en la jugabilidad.
- Probad enemigos muy lentos, muy agresivos y casi imposibles de derrotar.
- Reflexiona: ¿Qué hace que un enemigo parezca inteligente? ¿Su velocidad? ¿Su precisión? ¿Su comportamiento?
- Intenta crear un enemigo lento y otro rápido.
A continuación, seguiremos mejorando el apartado de comportamiento, añadiendo más potencia de fuego a nuestra nave.
12. Múltiples disparos.
Nuestra nave solo es capaz de lanzar un láser. Hasta que el láser no impacta o se pierde, no podemos disparar otro, y esto es bastante limitante y poco jugable.
Pero queremos poder tener muchos disparos, varios proyectiles en pantalla e incluso disparo continuo. Así que vamos a cambiar completamente el sistema.
Igual que hicimos cuando quisimos que aparecieran varios enemigos, vamos a gestionar los proyectiles también con listas.
Vamos a eliminar:
laser = Actor("laser")
laser.pos = nave.pos
laser_activo = FalseY lo vamos a sustituir por simplemente una lista vacía de proyectiles:
lasers = []
Ahora tendremos una lista que almacenará todos los disparos activos.
Para crear un nuevo disparo, localizamos esta línea:
if keyboard.space and laser_activo == False:
Y la sustituimos por:
if keyboard.space:
laser = Actor("laser")
laser.pos = (nave.x + 40, nave.y)
lasers.append(laser)
sounds.laser.play()Desde este momento, cada vez que pulsamos espacio se crea un nuevo láser, se coloca delante de la nave y se añade a la lista. Es decir, cada disparo es ahora un objeto independiente.
Veamos como los mostramos en pantalla.
Antes hacíamos:
if laser_activo:
laser.draw()Y ahora lo hacemos pero con todos los disparos almacenados en la lista:
for laser in lasers:
laser.draw()Continuamos, moviéndolos por la pantalla.
Donde antes teníamos:
laser.x += 8
Ahora tendremos:
for laser in lasers:
laser.x += 8Lo siguiente será eliminar todos aquellos proyectiles que desaparezcan por la derecha de la pantalla, sino tendremos miles de proyectiles consumiendo memoria en una lista infinita que acabará bloqueando el juego.
Para ello, dentro del bucle anterior, hacemos:
if laser.x > WIDTH:
lasers.remove(laser)Y dejamos el bucle así:
for laser in lasers:
laser.x += 8
if laser.x > WIDTH:
lasers.remove(laser)Ahora necesitamos revisar todos los láseres y detectar cuando tocan a los enemigos.
Sustituimos la colisión antigua por esto nuevo:
for enemigo in enemigos:
for laser in lasers:
if laser.colliderect(enemigo):
sounds.explosion.play()
puntos += 1
explosion.pos = enemigo.pos
explosion_activa = True
tiempo_explosion = 10
enemigo.x = WIDTH
enemigo.y = random.randint(50, HEIGHT - 50)
lasers.remove(laser)
breakLas líneas 1 y 3 son un bucle dentro de otro, necesario para programar “comprueba cada láser contra cada enemigo“.
¿Cuál es la finalidad de break? Cuando un disparo impacta, break detiene el bucle interno. Así evitamos errores extraños como una misma bala destruyendo varios enemigos o colisiones duplicadas.
Ejercicio 11.12 – Probando ráfagas de fuego
- Vamos a experimentar cambiando la velocidad de disparo, el tamaño del láser o la cantidad de enemigos.
- Probad disparos lentos, disparos rapidísimos y lluvia absoluta de proyectiles.
- ¿Cuándo el juego deja de ser divertido y empieza a ser caos visual?
- Intenta hacer un disparo doble –just for fun 🤷♀️-.
Ahora surge un nuevo problema, y es que si el usuario deja pulsado el botón de disparo la cantidad de lásers que salen son completamente absurdos.
Tenemos que usar temporizadores, de nuevo.
13. Temporizador de disparo
El problema de las ráfagas interminables de disparos se producen porque la función update() se ejecuta muchísimas veces por segundo. Vamos a arreglarlo ahora mismo.
En la zona de variables, creamos una variable nueva:
tiempo_disparo = 0
Esta variable será un contador que funcionará así:
0-> puede disparar15-> esperar141312- …
0-> puede volver a disparar
Es decir, después de disparar tendremos que esperar 15 frames antes de volver a hacerlo.
Dentro de update() hacemos:
global tiempo_disparo
if tiempo_disparo > 0:
tiempo_disparo -= 1Y al pulsar la tecla de disparo añadimos una nueva condición. Sustituimos:
if keyboard.space:
Por:
if keyboard.space and tiempo_disparo == 0: tiempo_disparo = 15
Así quedaría, en consecuencia, nuestra gestión del disparo:
if keyboard.space and tiempo_disparo == 0:
laser = Actor("laser")
laser.pos = (nave.x + 40, nave.y)
lasers.append(laser)
sounds.laser.play()
tiempo_disparo = 15Acabamos de experimentar la programación de algo importantísimo, el balance. Porque un arma demasiado rápida elimina toda dificultad, pero un arma demasiado lenta frustra al jugador.
Diseñar videojuegos muchas veces consiste en encontrar ese equilibrio. Y eso es muchísimo más difícil de lo que parece.
Dominando todo lo anterior nada te impide mostrar recarga, energía, munición, barras de cooldown. Muchísimos juegos funcionan exactamente así. Porque detrás de muchos sistemas espectaculares solo hay temporizadores bien usados.
Ejercicio 11.13 – Modificando temportizadores de disparo
- Probad a programar un disparo ultrarrápido, otro muy lento y otro equilibrado.
- ¿Cuándo el juego deja de ser divertido?
- Intentad crear un arma rápida pero débil y otra lenta pero potente.
Quizás ha llegado el momento de dejar de abusar de nuestra superioridad. ¿Qué tal si los enemigos también pudieran disparar? 👇
14. Enemigos que disparan
Igual que nuestra nave tiene proyectiles a los que llamamos lasers los enemigos tendrán sus propias balas a las que llamaremos laser_enemigo. Funcionarán exactamente igual: desaparecen, se crean, se mueven y colisionan.
Añadimos disparos_enemigos = [] justo debajo de lasers = [].
Después, dentro del bucle for enemigo in enemigos: vamos a añadir:
if random.randint(1, 100) == 1:
disparo = Actor("laser_enemigo")
disparo.pos = (enemigo.x - 40, enemigo.y)
disparos_enemigos.append(disparo)¿Por qué usamos ese randint(1,100)? Para que los enemigos disparen de forma aleatoria, así no sabes nunca quién ni cuándo te va a disparar 👹.
Pero claro, para eso tenemos que crear un nuevo sprite llamado laser_enemigo.png y almacenarlo en la carpeta images/.
Vamos a hacer que los disparos enemigos se muevan y desaparezcan cuando lleguen al margen izquierdo de la pantalla (recuerda que los disparos del protagonista se mueven hacia la derecha, y los enemigos hacia la izquierda). En update() añadimos:
for disparo in disparos_enemigos:
disparo.x -= 6
if disparo.x < 0:
disparos_enemigos.remove(disparo)Además, tendríamos que añadir la pérdida de vida del protagonista en caso de recibir un impacto. Así que, añadimos esto al código anterior:
for disparo in disparos_enemigos:
disparo.x -= 6
if disparo.x < 0:
disparos_enemigos.remove(disparo)
if disparo.colliderect(nave):
vidas -= 1
disparos_enemigos.remove(disparo)
if vidas <= 0:
game_over = TruePor último, mostramos los disparos en el escenario:
for disparo in disparos_enemigos:
disparo.draw()Estoy seguro que tras probar esta nueva versión del juego tu personaje ha acabado masacrado 😅. Pero también estoy seguro de que vas a ser capaz de identificar dónde se lanzan los proyectiles de los enemigos, para ralentizar su uso.
Ejercicio 11.14 – Ajustando la frecuencia de fuego enemigo
- Cambia la frecuencia del disparo enemigo, la velocidad y el tamaño.
- Intentad crear enemigos francotiradores, enemigos muy agresivos o enemigos lentos pero peligrosos.
- Piensa, ¿qué genera más tensión? ¿Muchos enemigos, disparos rápidos, disparos imprevisibles?
15. Fondo en movimiento
Muchos videojuegos realmente no “mueven” al jugador por el mundo, lo que hacen es mover el escenario y eso crea la sensación de velocidad. Aunque parezca una tontería, es muchísimo más importante de lo que parece. Porque un juego puede tener buenos gráficos, buenos sonidos y buenas mecánicas, pero si no transmite movimiento se nota todo muy raro.
Nuestro fondo va a ser un actor más, así que igual que en los casos anteriores debemos buscar un sprite PNG y colocarlo en la carpeta de imágenes. En mi caso utilizaré este fondo.png:

Pero ahora nos encontramos con un pequeño problema, porque si hacemos esto:
fondo = Actor("fondo")el fondo será estático, y nosotros queremos que se desplace continuamente.
Tenemos que hacer scrolling.
Hacer scrolling significa desplazar algún elemento de pantalla vertical y/o horizontalmente. En nuestro caso será sólo horizontalmente. Así que necesitamos una variable que controle el movimiento horizontal del fondo.
La colocamos en la zona de variables:
fondo_x = 0
Y dentro del update() actualizamos su posición:
global fondo_x fondo_x -= 2
¿Por qué -=? Porque queremos que el fondo se desplace hacia la izquierda. Así parece que nuestra nave avanza hacia la derecha.
Al hacer lo anterior aparecerá un nuevo problema: el fondo saldrá completamente de pantalla. Necesitamos reiniciarlo. Para ello, añadimos:
if fondo_x <= -WIDTH:
fondo_x = 0Parece que nuestro fondo podría funcionar así, pero todavía no lo hemos hecho visible en la pantalla porque no lo hemos “dibujado”.
Así que vamos a modificar draw() teniendo la precaución de dibujar el fondo antes que todo lo demás, porque lo primero que se dibuja queda “debajo”.
screen.blit("fondo", (fondo_x, 0))
screen.blit("fondo", (fondo_x + WIDTH, 0))¿Por qué lo dibujamos dos veces?
Estamos dibujando un fondo y otro justo al lado. Cuando el primero desaparece, el segundo ocupa su lugar. Y así conseguimos un bucle infinito.
Otro detalle que debes cuidar es que coincida el extremo derecho del fondo con el extremo izquierdo para que no se note la continuidad:

Si cuando hagas la búsqueda de tu fondo usas la palabra seamless (“sin costuras”) podrás encontrar lo que buscas.
Para mejorar la inmersión del jugador muchos videojuegos usan varios fondos moviéndose a distintas velocidades. Por ejemplo:
- Estrellas lejanas → lentas.
- Nebulosas → medias.
- Partículas cercanas → rápidas.
Eso se llama efecto parallax, y crea profundidad visual.
Vuestro juego ya empieza a acercarse a técnicas reales de la industria.
Ejercicio 11.15 – Diseñando fondos inmersivos
- Prueba distintas velocidades de fondo.
- Busca fondos espaciales, nebulosas, estrellas, galaxias.
- ¿Qué da más sensación de velocidad mover la nave, mover el fondo, mover ambos?
- Intenta crear un efecto parallax.
16. Pantallas de inicio y reinicio
Hasta ahora nuestro juego empieza directamente tras ejecutar el archivo. Eso no está mal para probar código, pero un videojuego real suele tener una pantalla de inicio y también la posibilidad de reiniciar la partida porque ahora mismo, cuando aparece GAME OVER, el juego se queda ahí para siempre.
Poco práctico.
Lo que vamos a hacer es cambiar la variable game_over por otra variable llamada estado que almacenará los posibles valores:
→ Pantalla inicial.iniciojugando→ Partida activa.game_over→ Partida perdida.
Por tanto:
# Eliminamos esta línea game_over = False # Añadimos esta otra variable estado = "inicio"
Además, vamos a crear una nueva función que quedará a la espera de que pulsemos la tecla ESPACIO para que se inicie la partida, literalmente significa “Si estamos en la pantalla de inicio y pulsamos ESPACIO, empieza la partida”:
def on_key_down(key):
global estado
if estado == "inicio" and key == keys.SPACE:
estado = "jugando"Otra cosa que debemos preparar es que todo el juego se pare cuando no estemos jugando. Así que al principio de update() colocamos:
global estado
if estado != "jugando":
returnDe esta manera, el juego solo se mueve cuando el estado es "jugando".
Para programar el fin del juego:
# Sustituimos esta linea game_over = True # Por esta otra estado = "game_over"
A continuación, tendremos que dibujar distintas situaciones en pantalla.
Al principio de draw() escribimos:
if estado == "inicio":
screen.draw.text(
"SPACE SHOOTER",
center=(400, 220),
fontsize=80,
color="white"
)
screen.draw.text(
"Pulsa ESPACIO para empezar",
center=(400, 340),
fontsize=40,
color="white"
)
returnEl return evita que se dibuje el resto del juego.
Al final de draw() cambiamos el texto anterior de GAME OVER por:
if estado == "game_over":
screen.draw.text(
"GAME OVER",
center=(400, 260),
fontsize=80,
color="red"
)
screen.draw.text(
"Pulsa R para reiniciar",
center=(400, 360),
fontsize=40,
color="white"
)Ahora añadimos otra parte en on_key_down():
def on_key_down(key):
global estado
if estado == "inicio" and key == keys.SPACE:
estado = "jugando"
if estado == "game_over" and key == keys.R:
reiniciar_partida()Pero esa función todavía no existe.
Vamos a crearla.
def reiniciar_partida():
global estado
global puntos
global vidas
global lasers
global disparos_enemigos
global explosion_activa
global tiempo_explosion
global tiempo_disparo
estado = "jugando"
puntos = 0
vidas = 3
lasers = []
disparos_enemigos = []
explosion_activa = False
tiempo_explosion = 0
tiempo_disparo = 0
nave.pos = (100, 300)
for enemigo in enemigos:
enemigo.x = random.randint(800, 1600)
enemigo.y = random.randint(50, HEIGHT - 50)¿Qué hace esta función? Devuelve el juego a su estado inicial:
- puntos a 0,
- vidas a 3,
- nave al inicio,
- enemigos recolocados,
- disparos eliminados,
- explosión apagada.
Es como pulsar “Nueva partida“. Pero programado por nosotros.
Ejercicio 11.16 – Configurando las pantallas auxiliares
- Probad a personalizar el título del juego, el mensaje de inicio, el mensaje de derrota, la tecla para reiniciar.
- También podéis añadir una pantalla final con el resumen de los puntos conseguidos.
Ahora sí, nuestro juego ya tiene estructura de videojuego completo.
18. ¿Y ahora qué? – Como seguir aprendiendo por tu cuenta.
Si has llegado hasta aquí, has conseguido algo que mucha gente nunca llega a hacer: has creado un videojuego desde cero.
No has usado plantillas, no has copiado un proyecto entero, no has pulsado un botón mágico que genera juegos automáticamente.
Has ido construyéndolo paso a paso, entendiendo cada pieza, y eso tiene muchísimo mérito, pero ahora llega la mejor parte: crear cosas nuevas por tu cuenta.
Créeme, tienes los conocimientos necesarios para implementar con éxito todas y cada una de las mejoras que vienen a continuación. Aún así, te dejo una pista para orientarte un poco.
| Mejora | Pista |
|---|---|
| Añadir más tipos de enemigos Ahora mismo todos los enemigos son iguales. Pero en los videojuegos reales suelen existir distintos tipos: enemigos rápidos, enemigos lentos, enemigos resistentes, enemigos que disparan mucho, enemigos gigantes. | Piensa en qué propiedades tiene actualmente un enemigo: posición, velocidad, comportamiento. ¿Qué ocurriría si algunos enemigos tuvieran valores distintos? |
| Crear un jefe final ¿Qué sería un juego de naves sin un jefe final enorme ocupando media pantalla? | Un jefe no deja de ser un enemigo con algunas diferencias: más tamaño, más vidas, ataques especiales. Pregúntate: ¿Cómo podría hacer que un enemigo necesite varios impactos para ser destruido? |
| Añadir varios niveles Ahora mismo el juego nunca termina. Podríamos crear: nivel 1, nivel 2, nivel 3… Cada uno más difícil que el anterior. | Ya tienes una variable de puntuación. ¿Qué podría ocurrir cuando el jugador alcance cierta cantidad de puntos? |
| Crear distintos tipos de armas Actualmente todas las balas son iguales. Pero podríamos tener: disparo doble, disparo triple, láser gigante, disparos explosivos. | Si ya sabes crear un disparo… ¿qué impediría crear dos o tres a la vez? |
| Añadir escudos Quizá el jugador pueda recoger un escudo temporal. Durante unos segundos los disparos enemigos no harían daño. | Ya has trabajado con temporizadores. Piensa: ¿Cómo podría hacer que una protección dure solo unos segundos? |
| Crear power-ups Los power-ups son objetos que aparecen durante la partida y mejoran temporalmente al jugador. Por ejemplo: más velocidad, más vidas, disparo rápido, puntos extra. | Un power-up es simplemente otro objeto que aparece en pantalla y puede colisionar con la nave. La pregunta importante es: ¿Qué ocurre después de recogerlo? |
| Añadir animaciones Ahora mismo los enemigos aparecen, explotan, desaparecen. Pero podrían tener animaciones. | ¿Y si una explosión no fuera una única imagen? ¿Y si estuviera formada por varias imágenes distintas mostradas una detrás de otra? |
| Crear una tabla de récords ¿Quién ha conseguido la mejor puntuación? Ahora mismo no lo sabemos. | Piensa qué ocurre al terminar una partida. Si la puntuación actual es mejor que la anterior… ¿qué deberíamos guardar? |
| Añadir sonidos más complejos Muchos juegos tienen: música para el menú, música para la partida, música para el jefe final. | Ya sabes reproducir sonidos, ¿y si cada estado del juego tuviera una música diferente? |
| Crear enemigos realmente inteligentes Nuestros enemigos ya persiguen a la nave. Pero todavía son bastante simples. | Pregúntate: ¿Qué haría un enemigo humano? Quizá: esquivar disparos, atacar desde lejos, esconderse, rodear al jugador. |
| Diseñar tus propios sprites Muchos desarrolladores empiezan usando gráficos descargados. Y está bien. Pero llega un momento en que apetece crear algo propio. | No hace falta ser artista. Empieza con cuadrados, círculos, naves sencillas. Muchísimos juegos famosos comenzaron con gráficos muy simples. |
| Crear un juego completamente distinto Esta es posiblemente la idea más importante de todas. Ahora ya no sabes hacer únicamente este juego. Sabes programar. Y eso significa que puedes crear: un juego de coches, un plataformas, un juego de fútbol, un laberinto, una aventura gráfica, un juego de estrategia. | Todos los videojuegos están construidos con las mismas piezas básicas: objetos, movimiento, colisiones, reglas, puntuaciones. Lo único que cambia es cómo las combinamos. |
19. El último consejo
Cuando empecemos un proyecto nuevo, es muy fácil pensar “Quiero crear el próximo Minecraft.” o “Voy a hacer un MMORPG con miles de jugadores.“
La experiencia demuestra que casi siempre funciona mejor otra estrategia:
Así es como aprenden los programadores. Así es como aprenden los desarrolladores de videojuegos. Y así es exactamente como hemos aprendido nosotros durante este tema.
Programar no consiste en saberlo todo, consiste en ser capaz de decir: “No sé hacerlo todavía… pero voy a intentarlo.“
Y esa es, probablemente, la habilidad más valiosa que puede desarrollar un programador.
Espero que el aprendizaje de Python guiado con la construcción desde cero de un videojuego te haya resultado interesante. Para mi ha sido un placer acompañarte en este viaje. Ojalá que seas capaz de seguir desarrollando el juego. Si lo haces, no dudes en escribirme para enseñármelo, me encantará revisarlo.
🤖 Hasta la próxima 🖖