Tu primer juego
En 6 pasos vas a hacer Lluvia de estrellas: un lince que recoge estrellas y esquiva rocas, con puntos, vidas, portada y récord. Cada paso es el programa completo, con lo nuevo marcado en amarillo, así que puedes copiarlo entero y ejecutarlo.
¿Prefieres empezar con algo que ya funciona? lince nuevo mi_juego crea una carpeta con un
juego pequeño (portada, partida y fin, con teclado y mando) listo para cambiarlo.
Necesitas Lince y SDL2 (mira la instalación). Los programas y las capturas de esta página salen del repositorio: cada paso se ejecuta al generarla, así que funcionan tal cual.
Paso 1 · Una ventana
Todo juego empieza igual: se abre una ventana y se entra en el bucle del juego, un mientras que se repite unas 60 veces por segundo hasta que se cierra la ventana.
En cada vuelta se borra la pantalla con juego.limpiar, se dibuja lo que toque y juego.presentar() lo enseña. Además espera lo justo para ir a 60 fotogramas por segundo y lee el teclado y el ratón.
Guárdalo como estrellas.lince y ejecútalo con lince estrellas.lince. Los colores son listas [rojo, verde, azul] de 0 a 255.

importar "juego"
juego.abrir("Lluvia de estrellas", 640, 480)
mientras juego.abierto() {
juego.limpiar([20, 24, 48])
juego.texto("HOLA", 272, 220, 4, [255, 220, 120])
juego.presentar()
}Paso 2 · El lince
Ahora el protagonista. juego.sprite convierte un dibujo hecho con letras en una imagen: cada letra es un píxel del color que le des, y el punto es transparente. juego.dibujar lo pinta cuatro veces más grande.
Para moverlo, si la flecha está pulsada se suma o se resta a x. Se multiplica por juego.dt(), los segundos que ha durado el último fotograma, para que vaya a 320 píxeles por segundo en cualquier ordenador, sea rápido o lento. matematica.limitar impide que se salga por los lados.
Prueba la recarga en caliente: deja el juego abierto, cambia VELOCIDAD o un color del sprite y guarda. El juego se actualiza al momento sin cerrarse. Por eso lo que quieras retocar va en un fijo: las constantes se toman del código nuevo, mientras que las variables sea, como x, conservan su valor.

En amarillo, lo nuevo de este paso.
importar "juego"
importar "matematica"
fijo ANCHO = 640
fijo ALTO = 480
fijo VELOCIDAD = 320 # píxeles por segundo
juego.abrir("Lluvia de estrellas", ANCHO, ALTO)
fijo LINCE = juego.sprite([
"K..........K",
"O..........O",
"OO........OO",
"OOOOOOOOOOOO",
"OOKOOOOOOKOO",
"OOOOOOOOOOOO",
"OWWWWKKWWWWO",
"OWWWWWWWWWWO",
".OWWWWWWWWO.",
"..OOOOOOOO.."
], {"O": [235, 150, 60], "K": [40, 30, 30], "W": [250, 240, 225]})
sea x = ANCHO / 2
mientras juego.abierto() {
si juego.tecla("izquierda") { x = x - VELOCIDAD * juego.dt() }
si juego.tecla("derecha") { x = x + VELOCIDAD * juego.dt() }
x = matematica.limitar(x, 24, ANCHO - 24)
juego.limpiar([20, 24, 48])
juego.dibujar(LINCE, x, ALTO - 40, {"escala": 4, "centrado": verdadero})
juego.presentar()
}Paso 3 · Estrellas que caen
Las estrellas se guardan en una lista, y cada una es un diccionario con su posición y su velocidad. Así puede haber tantas como haga falta.
La variable espera hace de temporizador: se le resta dt en cada fotograma y, cuando llega a cero, nace otra estrella y la cuenta vuelve a empezar. Nunca se usa tiempo.esperar dentro del bucle, porque congelaría el juego entero.
para cada mueve todas las estrellas y filtrar se queda solo con las que siguen en pantalla. La función que recibe filtrar dice qué estrellas se conservan.

En amarillo, lo nuevo de este paso.
importar "juego"
importar "matematica"
fijo ANCHO = 640
fijo ALTO = 480
fijo VELOCIDAD = 320 # píxeles por segundo
juego.abrir("Lluvia de estrellas", ANCHO, ALTO)
fijo LINCE = juego.sprite([
"K..........K",
"O..........O",
"OO........OO",
"OOOOOOOOOOOO",
"OOKOOOOOOKOO",
"OOOOOOOOOOOO",
"OWWWWKKWWWWO",
"OWWWWWWWWWWO",
".OWWWWWWWWO.",
"..OOOOOOOO.."
], {"O": [235, 150, 60], "K": [40, 30, 30], "W": [250, 240, 225]})
fijo ESTRELLA = juego.sprite([
"...Y...",
"..YYY..",
"YYYYYYY",
".YYYYY.",
"..YYY..",
".YY.YY.",
"YY...YY"
], {"Y": [255, 215, 80]})
sea x = ANCHO / 2
sea estrellas = [] # cada una es un diccionario: {"x", "y", "vel"}
sea espera = 0 # segundos que faltan para la siguiente
mientras juego.abierto() {
sea dt = juego.dt()
si juego.tecla("izquierda") { x = x - VELOCIDAD * dt }
si juego.tecla("derecha") { x = x + VELOCIDAD * dt }
x = matematica.limitar(x, 24, ANCHO - 24)
# una estrella nueva cada 0,6 segundos, en un sitio al azar
espera = espera - dt
si espera <= 0 {
estrellas.agregar({"x": matematica.aleatorio(20, ANCHO - 20), "y": -20,
"vel": matematica.aleatorio(120, 220)})
espera = 0.6
}
# caen, y las que salen por abajo se quitan de la lista
para cada diccionario e en estrellas { e["y"] = e["y"] + e["vel"] * dt }
estrellas = filtrar(estrellas, funcion(val diccionario e): logico { devolver e["y"] < ALTO + 20 })
juego.limpiar([20, 24, 48])
para cada diccionario e en estrellas {
juego.dibujar(ESTRELLA, e["x"], e["y"], {"escala": 3, "centrado": verdadero})
}
juego.dibujar(LINCE, x, ALTO - 40, {"escala": 4, "centrado": verdadero})
juego.presentar()
}Paso 4 · ¡Atrápalas!
juego.colision dice si dos rectángulos se solapan. El primero es el del lince: su x es el centro, así que el rectángulo empieza 24 píxeles a la izquierda. El segundo es el de la estrella.
Una estrella cogida no se borra en ese momento: se marca con "cogida" y el filtrar de después la quita. Borrar elementos de una lista mientras se recorre da problemas en casi todos los lenguajes.
Para que se note: juego.pitido hace un sonido sin necesitar archivos, y juego.particulas lanza unas chispas que se mueven y dibujan solas al llamar a juego.dibujar_particulas(). El marcador es juego.texto, y a_texto convierte el número en texto.

En amarillo, lo nuevo de este paso.
importar "juego"
importar "matematica"
fijo ANCHO = 640
fijo ALTO = 480
fijo VELOCIDAD = 320 # píxeles por segundo
juego.abrir("Lluvia de estrellas", ANCHO, ALTO)
fijo LINCE = juego.sprite([
"K..........K",
"O..........O",
"OO........OO",
"OOOOOOOOOOOO",
"OOKOOOOOOKOO",
"OOOOOOOOOOOO",
"OWWWWKKWWWWO",
"OWWWWWWWWWWO",
".OWWWWWWWWO.",
"..OOOOOOOO.."
], {"O": [235, 150, 60], "K": [40, 30, 30], "W": [250, 240, 225]})
fijo ESTRELLA = juego.sprite([
"...Y...",
"..YYY..",
"YYYYYYY",
".YYYYY.",
"..YYY..",
".YY.YY.",
"YY...YY"
], {"Y": [255, 215, 80]})
sea x = ANCHO / 2
sea estrellas = [] # cada una es un diccionario: {"x", "y", "vel"}
sea espera = 0 # segundos que faltan para la siguiente
sea puntos = 0
mientras juego.abierto() {
sea dt = juego.dt()
si juego.tecla("izquierda") { x = x - VELOCIDAD * dt }
si juego.tecla("derecha") { x = x + VELOCIDAD * dt }
x = matematica.limitar(x, 24, ANCHO - 24)
# una estrella nueva cada 0,6 segundos, en un sitio al azar
espera = espera - dt
si espera <= 0 {
estrellas.agregar({"x": matematica.aleatorio(20, ANCHO - 20), "y": -20,
"vel": matematica.aleatorio(120, 220), "cogida": falso})
espera = 0.6
}
# caen; si tocan al lince, punto, pitido y chispas
para cada diccionario e en estrellas {
e["y"] = e["y"] + e["vel"] * dt
si juego.colision(x - 24, ALTO - 60, 48, 40, e["x"] - 10, e["y"] - 10, 20, 20) {
e["cogida"] = verdadero
puntos = puntos + 1
juego.pitido(880, 0.06, 0.2)
juego.particulas(e["x"], e["y"], {"cantidad": 20, "color": [255, 215, 80],
"velocidad": [60, 200], "vida": [0.3, 0.6]})
}
}
estrellas = filtrar(estrellas, funcion(val diccionario e): logico {
devolver no e["cogida"] y e["y"] < ALTO + 20
})
juego.limpiar([20, 24, 48])
para cada diccionario e en estrellas {
juego.dibujar(ESTRELLA, e["x"], e["y"], {"escala": 3, "centrado": verdadero})
}
juego.dibujar(LINCE, x, ALTO - 40, {"escala": 4, "centrado": verdadero})
juego.dibujar_particulas()
juego.texto("PUNTOS " + a_texto(puntos), 16, 16, 3, [255, 255, 255])
juego.presentar()
}Paso 5 · Rocas y vidas
Un juego necesita algo que lo complique. Las rocas caen igual que las estrellas, pero son círculos: juego.colision_circulo_rect comprueba si un círculo toca un rectángulo.
La dificultad sube sola: reloj cuenta los segundos de partida y la pausa entre rocas se acorta con él, hasta un mínimo de 0,4 segundos que marca matematica.maximo.
Al quedarse sin vidas, dt pasa a valer 0: todo lo que se mueve con dt se detiene sin necesidad de más código.

En amarillo, lo nuevo de este paso.
importar "juego"
importar "matematica"
fijo ANCHO = 640
fijo ALTO = 480
fijo VELOCIDAD = 320 # píxeles por segundo
juego.abrir("Lluvia de estrellas", ANCHO, ALTO)
fijo LINCE = juego.sprite([
"K..........K",
"O..........O",
"OO........OO",
"OOOOOOOOOOOO",
"OOKOOOOOOKOO",
"OOOOOOOOOOOO",
"OWWWWKKWWWWO",
"OWWWWWWWWWWO",
".OWWWWWWWWO.",
"..OOOOOOOO.."
], {"O": [235, 150, 60], "K": [40, 30, 30], "W": [250, 240, 225]})
fijo ESTRELLA = juego.sprite([
"...Y...",
"..YYY..",
"YYYYYYY",
".YYYYY.",
"..YYY..",
".YY.YY.",
"YY...YY"
], {"Y": [255, 215, 80]})
sea x = ANCHO / 2
sea estrellas = [] # cada una es un diccionario: {"x", "y", "vel"}
sea espera = 0 # segundos que faltan para la siguiente
sea puntos = 0
sea rocas = []
sea espera_roca = 2
sea vidas = 3
sea reloj = 0 # segundos de partida: con el tiempo, más rocas
mientras juego.abierto() {
sea dt = juego.dt()
si vidas == 0 { dt = 0 } # sin vidas, todo se para
reloj = reloj + dt
si juego.tecla("izquierda") { x = x - VELOCIDAD * dt }
si juego.tecla("derecha") { x = x + VELOCIDAD * dt }
x = matematica.limitar(x, 24, ANCHO - 24)
# una estrella nueva cada 0,6 segundos, en un sitio al azar
espera = espera - dt
si espera <= 0 {
estrellas.agregar({"x": matematica.aleatorio(20, ANCHO - 20), "y": -20,
"vel": matematica.aleatorio(120, 220), "cogida": falso})
espera = 0.6
}
# caen; si tocan al lince, punto, pitido y chispas
para cada diccionario e en estrellas {
e["y"] = e["y"] + e["vel"] * dt
si juego.colision(x - 24, ALTO - 60, 48, 40, e["x"] - 10, e["y"] - 10, 20, 20) {
e["cogida"] = verdadero
puntos = puntos + 1
juego.pitido(880, 0.06, 0.2)
juego.particulas(e["x"], e["y"], {"cantidad": 20, "color": [255, 215, 80],
"velocidad": [60, 200], "vida": [0.3, 0.6]})
}
}
estrellas = filtrar(estrellas, funcion(val diccionario e): logico {
devolver no e["cogida"] y e["y"] < ALTO + 20
})
# rocas: cada vez más seguidas; si una da al lince, pierde una vida
espera_roca = espera_roca - dt
si espera_roca <= 0 {
rocas.agregar({"x": matematica.aleatorio(20, ANCHO - 20), "y": -20,
"vel": matematica.aleatorio(160, 260), "choca": falso})
espera_roca = matematica.maximo(0.4, 1.5 - reloj * 0.03)
}
para cada diccionario r en rocas {
r["y"] = r["y"] + r["vel"] * dt
si vidas > 0 y juego.colision_circulo_rect(r["x"], r["y"], 14, x - 24, ALTO - 60, 48, 40) {
r["choca"] = verdadero
vidas = vidas - 1
juego.pitido(110, 0.25)
juego.particulas(r["x"], r["y"], {"cantidad": 30, "color": [150, 140, 150],
"velocidad": [80, 260], "gravedad": 500})
}
}
rocas = filtrar(rocas, funcion(val diccionario r): logico {
devolver no r["choca"] y r["y"] < ALTO + 20
})
juego.limpiar([20, 24, 48])
para cada diccionario e en estrellas {
juego.dibujar(ESTRELLA, e["x"], e["y"], {"escala": 3, "centrado": verdadero})
}
juego.dibujar(LINCE, x, ALTO - 40, {"escala": 4, "centrado": verdadero})
para cada diccionario r en rocas { juego.circulo(r["x"], r["y"], 14, [120, 110, 120]) }
juego.dibujar_particulas()
juego.texto("PUNTOS " + a_texto(puntos), 16, 16, 3, [255, 255, 255])
juego.texto("VIDAS " + a_texto(vidas), ANCHO - 150, 16, 3, [255, 120, 120])
si vidas == 0 {
sea fin = "HAS PERDIDO"
juego.texto(fin, (ANCHO - juego.ancho_texto(fin, 5)) / 2, 200, 5, [255, 255, 255])
}
juego.presentar()
}Paso 6 · Portada y final
Un juego completo tiene una portada, la partida y una pantalla final. Cada una es una escena. juego.cambiar_escena("partida", 0.6) cambia de una a otra con un fundido a negro de 0,6 segundos, y juego.escena() dice en cuál se está. El bucle solo tiene que llamar a la función de cada escena.
Para ordenar el código, lo que antes iba suelto en el bucle ahora está repartido en funciones: mover hace avanzar la partida, dibujar_partida la pinta y empezar deja todo como al principio. juego.escena_nueva() vale verdadero en el primer fotograma de una escena, y es el momento de llamar a empezar.
juego.cuadro(2, 2) alterna entre 0 y 1 dos veces por segundo, lo justo para que «PULSA ESPACIO» parpadee. Y gracias a si no juego.recargado(), guardar un cambio con la recarga en caliente no te devuelve a la portada.

En amarillo, lo nuevo de este paso.
importar "juego"
importar "matematica"
fijo ANCHO = 640
fijo ALTO = 480
fijo VELOCIDAD = 320 # píxeles por segundo
juego.abrir("Lluvia de estrellas", ANCHO, ALTO)
fijo LINCE = juego.sprite([
"K..........K",
"O..........O",
"OO........OO",
"OOOOOOOOOOOO",
"OOKOOOOOOKOO",
"OOOOOOOOOOOO",
"OWWWWKKWWWWO",
"OWWWWWWWWWWO",
".OWWWWWWWWO.",
"..OOOOOOOO.."
], {"O": [235, 150, 60], "K": [40, 30, 30], "W": [250, 240, 225]})
fijo ESTRELLA = juego.sprite([
"...Y...",
"..YYY..",
"YYYYYYY",
".YYYYY.",
"..YYY..",
".YY.YY.",
"YY...YY"
], {"Y": [255, 215, 80]})
sea x = ANCHO / 2
sea estrellas = [] # cada una es un diccionario: {"x", "y", "vel"}
sea espera = 0 # segundos que faltan para la siguiente
sea puntos = 0
sea rocas = []
sea espera_roca = 2
sea vidas = 3
sea reloj = 0 # segundos de partida: con el tiempo, más rocas
sea record = 0
# Deja todo listo para una partida nueva
funcion empezar(): nulo {
x = ANCHO / 2
estrellas = []
rocas = []
espera = 0
espera_roca = 2
puntos = 0
vidas = 3
reloj = 0
juego.borrar_particulas()
}
# Un paso de la partida: lo mismo que antes, ahora dentro de una función
funcion mover(val numero dt): nulo {
reloj = reloj + dt
si juego.tecla("izquierda") { x = x - VELOCIDAD * dt }
si juego.tecla("derecha") { x = x + VELOCIDAD * dt }
x = matematica.limitar(x, 24, ANCHO - 24)
# una estrella nueva cada 0,6 segundos, en un sitio al azar
espera = espera - dt
si espera <= 0 {
estrellas.agregar({"x": matematica.aleatorio(20, ANCHO - 20), "y": -20,
"vel": matematica.aleatorio(120, 220), "cogida": falso})
espera = 0.6
}
# caen; si tocan al lince, punto, pitido y chispas
para cada diccionario e en estrellas {
e["y"] = e["y"] + e["vel"] * dt
si juego.colision(x - 24, ALTO - 60, 48, 40, e["x"] - 10, e["y"] - 10, 20, 20) {
e["cogida"] = verdadero
puntos = puntos + 1
juego.pitido(880, 0.06, 0.2)
juego.particulas(e["x"], e["y"], {"cantidad": 20, "color": [255, 215, 80],
"velocidad": [60, 200], "vida": [0.3, 0.6]})
}
}
estrellas = filtrar(estrellas, funcion(val diccionario e): logico {
devolver no e["cogida"] y e["y"] < ALTO + 20
})
# rocas: cada vez más seguidas; si una da al lince, pierde una vida
espera_roca = espera_roca - dt
si espera_roca <= 0 {
rocas.agregar({"x": matematica.aleatorio(20, ANCHO - 20), "y": -20,
"vel": matematica.aleatorio(160, 260), "choca": falso})
espera_roca = matematica.maximo(0.4, 1.5 - reloj * 0.03)
}
para cada diccionario r en rocas {
r["y"] = r["y"] + r["vel"] * dt
si juego.colision_circulo_rect(r["x"], r["y"], 14, x - 24, ALTO - 60, 48, 40) {
r["choca"] = verdadero
vidas = vidas - 1
juego.pitido(110, 0.25)
juego.particulas(r["x"], r["y"], {"cantidad": 30, "color": [150, 140, 150],
"velocidad": [80, 260], "gravedad": 500})
}
}
rocas = filtrar(rocas, funcion(val diccionario r): logico {
devolver no r["choca"] y r["y"] < ALTO + 20
})
si vidas == 0 {
record = matematica.maximo(record, puntos)
juego.cambiar_escena("fin", 1)
}
}
funcion dibujar_partida(): nulo {
juego.limpiar([20, 24, 48])
para cada diccionario e en estrellas {
juego.dibujar(ESTRELLA, e["x"], e["y"], {"escala": 3, "centrado": verdadero})
}
juego.dibujar(LINCE, x, ALTO - 40, {"escala": 4, "centrado": verdadero})
para cada diccionario r en rocas { juego.circulo(r["x"], r["y"], 14, [120, 110, 120]) }
juego.dibujar_particulas()
juego.texto("PUNTOS " + a_texto(puntos), 16, 16, 3, [255, 255, 255])
juego.texto("VIDAS " + a_texto(vidas), ANCHO - 150, 16, 3, [255, 120, 120])
}
# Escribe un texto centrado
funcion centrado(val texto t, val numero arriba, val numero escala, val lista color): nulo {
juego.texto(t, (ANCHO - juego.ancho_texto(t, escala)) / 2, arriba, escala, color)
}
funcion portada(): nulo {
juego.limpiar([20, 24, 48])
centrado("LLUVIA DE ESTRELLAS", 120, 5, [255, 215, 80])
juego.dibujar(LINCE, ANCHO / 2, 260, {"escala": 8, "centrado": verdadero})
si juego.cuadro(2, 2) == 0 { centrado("PULSA ESPACIO", 360, 3, [255, 255, 255]) }
si juego.tecla_pulsada("espacio") { juego.cambiar_escena("partida", 0.6) }
}
funcion pantalla_fin(): nulo {
juego.rectangulo(0, 160, ANCHO, 170, [0, 0, 0, 170])
centrado("HAS PERDIDO", 180, 5, [255, 255, 255])
centrado(a_texto(puntos) + " ESTRELLAS - RECORD " + a_texto(record), 250, 2, [255, 215, 80])
centrado("ESPACIO PARA REPETIR", 290, 2, [200, 200, 220])
si juego.tecla_pulsada("espacio") { juego.cambiar_escena("partida", 0.6) }
}
si no juego.recargado() { juego.cambiar_escena("portada") }
mientras juego.abierto() {
si juego.escena_nueva() y juego.escena() == "partida" { empezar() }
si juego.escena() == "portada" {
portada()
} sino {
si juego.escena() == "partida" { mover(juego.dt()) }
dibujar_partida()
si juego.escena() == "fin" { pantalla_fin() }
}
juego.presentar()
}Compártelo
Para pasarle el juego a alguien que no tiene Lince:
lince repartir estrellas.lince
Crea la carpeta estrellas-linux (o estrellas-windows) y un .zip con el
juego y SDL2 dentro. Quien lo reciba lo descomprime y juega. Si tu juego usa imágenes o sonidos que nombras en el
código, como juego.imagen("lince.png"), también van dentro.
¿Y ahora qué?
Algunas ideas para seguir con este mismo juego:
· Estrellas doradas que valgan 5 puntos y caigan más rápido.
· Un sonido de verdad con juego.sonido("estrella.wav") y juego.reproducir.
· Jugar con mando: juego.definir_accion("izquierda", ["izquierda", "mando:izq_x-"]) y
juego.direccion("izquierda", "derecha").
· Guardar el récord en un archivo con el módulo archivos.
El manual explica todo el lenguaje con ejemplos que se ejecutan, y en examples/ hay nueve minijuegos completos para leer y modificar.