v0.10
Lenguaje Módulo juego Tutorial

El módulo juego

Todo lo que hace falta para un juego 2D: la ventana, el dibujo, los sprites y sus animaciones, el teclado, el ratón, los mandos y la pantalla táctil, el sonido, los mapas, la física, los menús... Se usa con importar "juego".

Los 34 ejemplos de esta página se ejecutan al generarla, sin pantalla, y la imagen de cada uno es una captura real de lo que dibuja. Si algo deja de funcionar, la página deja de cuadrar y la integración continua lo detecta. Para la lista completa de funciones y opciones, mira la especificación; para empezar desde cero, el tutorial.

Nada coincide con esa búsqueda.

Empezar

La ventana y el bucle

Un juego es un bucle: en cada vuelta se mira la entrada, se mueven las cosas, se dibuja todo y juego.presentar() lo enseña, espera al siguiente fotograma (60 por segundo) y lee el teclado, el ratón y los mandos. juego.abierto() pasa a falso al cerrar la ventana.

Al guardar el archivo con el juego en marcha, el juego sigue con el código nuevo sin perder dónde estaba (recarga en caliente); F5 empieza de cero.

importar "juego"

juego.abrir("Mi primer juego", 480, 270)
sea x = 40

mientras juego.abierto() {
    x = x + 150 * juego.dt()                    # 150 píxeles por segundo, vaya a los fps que vaya
    juego.limpiar([25, 30, 45])
    juego.circulo(x, 135, 24, [255, 200, 60])
    juego.texto("HOLA, LINCE", 20, 20, 3)
    juego.presentar()
}
Captura de «La ventana y el bucle»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/010_primer_juego.lince

Píxeles grandes

Con {"escala": n}, la ventana es n veces mayor y cada píxel del juego se ve como un cuadrado de n × n, como en las consolas portátiles. Todo se sigue midiendo en el tamaño pequeño (aquí 160 × 90). juego.pantalla_completa() lo pone a pantalla completa, escalado sin deformarse.

importar "juego"

juego.abrir("Píxeles", 160, 90, {"escala": 3})

mientras juego.abierto() {
    juego.limpiar([20, 24, 40])
    juego.rectangulo(0, 70, 160, 20, [70, 140, 70])
    juego.circulo(130, 20, 9, [250, 220, 100])
    juego.rectangulo(30, 54, 10, 16, [230, 120, 60])
    juego.texto("160 X 90", 4, 4, 1)
    juego.presentar()
}
Captura de «Píxeles grandes»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/020_pixeles_grandes.lince

Dibujar

Formas y colores

Un color es [rojo, verde, azul] (de 0 a 255), con un cuarto número para la transparencia, o un texto como "#ffc850". Lo que se dibuja después tapa a lo de antes.

importar "juego"

juego.abrir("Formas", 480, 270)
mientras juego.abierto() {
    juego.limpiar([30, 30, 40])
    juego.rectangulo(30, 30, 120, 80, [200, 70, 70])
    juego.contorno(180, 30, 120, 80, "#ffc850")
    juego.circulo(400, 70, 45, [80, 160, 230])
    juego.circulo(370, 90, 45, [255, 255, 255, 90])            # medio transparente
    juego.linea(30, 160, 450, 160, [120, 220, 120])
    para (i = 0; i < 8; i++) {
        juego.rectangulo(30 + i * 55, 190, 45, 45, [30 * i, 255 - 30 * i, 160])
    }
    juego.presentar()
}
Captura de «Formas y colores»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/110_formas_y_colores.lince

Oscurecer la pantalla

juego.cubrir(color) pinta toda la ventana por encima de lo dibujado, sin moverse con la cámara: con un negro medio transparente se oscurece para una pausa, y con otro color se tiñe (agua, daño, noche).

importar "juego"

juego.abrir("Pausa", 480, 270)
mientras juego.abierto() {
    juego.limpiar([60, 130, 200])
    juego.rectangulo(0, 200, 480, 70, [70, 150, 70])
    juego.circulo(240, 170, 30, [240, 180, 70])
    juego.cubrir([0, 0, 0, 150])
    juego.texto("PAUSA", 240, 120, {"escala": 4, "alinear": "centro"})
    juego.presentar()
}
Captura de «Oscurecer la pantalla»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/120_cubrir.lince

Texto y letras

Texto con la letra incluida

Lince trae una letra de píxeles (mayúsculas, cifras, signos; las tildes y la ñ se marcan). escala la agranda, color la pinta y alinear ("centro" o "derecha") cambia de qué lado cuenta la x. juego.ancho_texto() dice lo que ocupa, para centrar a mano.

importar "juego"

juego.abrir("Texto", 480, 270)
mientras juego.abierto() {
    juego.limpiar([20, 20, 30])
    juego.texto("PUNTOS: 1250", 20, 20, 2)
    juego.texto("¡NIVEL 3!", 240, 90, {"escala": 5, "alinear": "centro", "color": [255, 210, 60]})
    juego.texto("VIDAS 3", 460, 20, {"escala": 2, "alinear": "derecha", "color": [240, 90, 90]})
    sea ancho = juego.ancho_texto("PULSA ESPACIO", 2)
    juego.texto("PULSA ESPACIO", (480 - ancho) / 2, 200, 2)
    juego.presentar()
}
Captura de «Texto con la letra incluida»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/210_texto.lince

Fuentes .ttf

juego.fuente(ruta, tamaño) carga una fuente TrueType u OpenType (aquí, Press Start 2P, con licencia libre OFL). Trae el español entero: tildes, ñ, ¿ y ¡. Con ancho, el texto se parte en líneas por los espacios; juego.medir_texto() da [ancho, alto].

importar "juego"

juego.abrir("Fuentes", 480, 270)
fijo LETRA = juego.fuente("recursos/PressStart2P-Regular.ttf", 16)
fijo PEQUENA = juego.fuente("recursos/PressStart2P-Regular.ttf", 8)
sea dialogo = "¿Has visto al lince? Dicen que vive en la sierra, junto al río."
mientras juego.abierto() {
    juego.limpiar([15, 20, 35])
    juego.texto("¡Año de nieves!", 240, 30, {"fuente": LETRA, "alinear": "centro", "color": [255, 220, 90]})
    juego.texto(dialogo, 40, 90, {"fuente": PEQUENA, "ancho": 400, "color": [220, 230, 240]})
    sea medida = juego.medir_texto(dialogo, {"fuente": PEQUENA, "ancho": 400})
    juego.contorno(34, 84, medida[0] + 12, medida[1] + 12, [90, 110, 160])
    juego.presentar()
}
Captura de «Fuentes .ttf»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/220_fuentes.lince

Imágenes y sprites

Dibujos hechos con texto

juego.sprite(filas, colores) hace una imagen sin archivos: un carácter por píxel, y cada carácter con su color (los que no están, transparentes). Sirve para empezar sin dibujos y para cosas pequeñas.

importar "juego"

juego.abrir("Sprites", 480, 270)
fijo NAVE = juego.sprite([
    "....X....",
    "...XXX...",
    "..XXOXX..",
    ".XXXXXXX.",
    "XX.XXX.XX",
    "X...R...X"
], {"X": [120, 200, 240], "O": [255, 255, 255], "R": [255, 120, 40]})
mientras juego.abierto() {
    juego.limpiar([10, 10, 25])
    juego.dibujar(NAVE, 60, 100, {"escala": 4})
    juego.dibujar(NAVE, 200, 80, {"escala": 8})
    juego.dibujar(NAVE, 380, 110, {"escala": 4, "angulo": 180})
    juego.presentar()
}
Captura de «Dibujos hechos con texto»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/310_sprite.lince

Girar, voltear, teñir

juego.dibujar(imagen, x, y, opciones) admite escala, angulo, espejo (mirar al otro lado), voltear, alfa, color (teñir), blanco (el destello de cuando a alguien le dan) y mezcla ("sumar" para luces y fuego). Con centrado, la x y la y son el centro.

importar "juego"
importar "matematica"

juego.abrir("Opciones", 480, 270)
fijo LINCE = juego.hoja("recursos/lince.png", 16)
mientras juego.abierto() {
    juego.limpiar([40, 50, 70])
    sea opciones = [{"escala": 4}, {"escala": 4, "espejo": verdadero}, {"escala": 4, "angulo": 30},
                    {"escala": 4, "alfa": 110}, {"escala": 4, "color": [120, 160, 255]}, {"escala": 4, "blanco": 0.8}]
    para (i = 0; i < 6; i++) {
        sea op = opciones[i]
        op["cuadro"] = 0
        op["centrado"] = verdadero
        juego.dibujar(LINCE, 50 + (i % 3) * 150, 80 + matematica.piso(i / 3) * 110, op)
    }
    juego.presentar()
}
Captura de «Girar, voltear, teñir»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/320_dibujar_opciones.lince

Hojas de sprites

Una hoja con todos los fotogramas

Una hoja de sprites es una sola imagen con todos los fotogramas, una animación por fila. juego.hoja(ruta, 16) la parte en cuadros de 16 × 16, numerados desde 0. Con fila, cuadro cuenta dentro de esa fila, y juego.cuadro(cuantos, por_segundo) dice cuál toca ahora. juego.cuadros_de(hoja, fila) cuenta los cuadros con dibujo de una fila (en una rejilla, las celdas vacías del final no cuentan).

importar "juego"

juego.abrir("Hoja", 480, 270)
fijo LINCE = juego.hoja("recursos/lince.png", 16)
mientras juego.abierto() {
    juego.limpiar([200, 225, 240])
    juego.dibujar(LINCE, 20, 20, {"escala": 3})                  # cuadro 0
    para (fila = 0; fila < 4; fila++) {
        sea cuantos = juego.cuadros_de(LINCE, fila)           # los que tienen dibujo
        sea op = {"fila": fila, "cuadro": juego.cuadro(cuantos, 6), "escala": 4}
        juego.dibujar(LINCE, 140 + fila * 85, 120, op)
    }
    juego.texto("CUADROS: {LINCE["cuadros"]}  POR FILA: {LINCE["por_fila"]}", 20, 230, 2)
    juego.presentar()
}
Captura de «Una hoja con todos los fotogramas»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/410_hoja.lince

Hojas desordenadas

Sin decir cómo partirla, juego.hoja(ruta) busca sola las figuras (lo separado por fondo), las agrupa en filas y las ordena, aunque midan distinto. Así sirven las hojas descargadas tal cual. Con {"fondo": "auto"} también quita un fondo de color.

importar "juego"
importar "matematica"

juego.abrir("Hoja sola", 480, 270)
fijo FIGURAS = juego.hoja("recursos/figuras.png")
mientras juego.abierto() {
    juego.limpiar([30, 30, 40])
    juego.dibujar(juego.imagen("recursos/figuras.png"), 20, 20, {"escala": 2})
    para (i = 0; i < FIGURAS["cuadros"]; i++) {
        juego.dibujar(FIGURAS, 290 + (i % 3) * 60, 30 + matematica.piso(i / 3) * 70, {"cuadro": i})
        juego.texto("{i}", 290 + (i % 3) * 60, 30 + matematica.piso(i / 3) * 70 + 50, 1)
    }
    juego.texto("{FIGURAS["cuadros"]} FIGURAS, {FIGURAS["filas"]} FILAS", 20, 230, 2)
    juego.presentar()
}
Captura de «Hojas desordenadas»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/420_hoja_sola.lince

Animaciones a partir de un dibujo

Con un solo dibujo, juego.animar(imagen, tipo) hace los cuadros de una animación: "andar" y "correr" mueven las piernas; "respirar", "saltar", "golpe", "morir", "flotar" y "girar" mueven el dibujo entero. Da una hoja, así que se puede cambiar después por una dibujada a mano sin tocar el resto.

importar "juego"

juego.abrir("Animar", 480, 270)
fijo DIBUJO = juego.hoja("recursos/lince.png", 16)
fijo QUIETO = juego.sprite(["..X..", ".XXX.", "XXXXX", ".X.X.", ".X.X."], {"X": [230, 140, 60]})
fijo TIPOS = ["respirar", "saltar", "flotar", "girar", "morir"]
sea hojas = []
para cada texto t en TIPOS { hojas.agregar(juego.animar(QUIETO, t)) }
mientras juego.abierto() {
    juego.limpiar([25, 35, 50])
    para (i = 0; i < TIPOS.longitud(); i++) {
        juego.dibujar(hojas[i], 30 + i * 90, 90, {"cuadro": juego.cuadro(hojas[i]["cuadros"], 8), "escala": 8})
        juego.texto(TIPOS[i].mayusculas(), 30 + i * 90, 190, 1)
    }
    juego.presentar()
}
Captura de «Animaciones a partir de un dibujo»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/430_animar.lince

Animador

El animador

juego.animador(hoja, animaciones) lleva por ti qué animación toca, desde cuándo y qué viene después. Cada animación dice su fila (o sus cuadros), por_segundo, si se repite y la siguiente. juego.animacion(pj, "andar") la cambia (si ya estaba en ella, sigue por donde iba) y juego.dibujar(pj, x, y) dibuja el cuadro que toca.

importar "juego"

juego.abrir("Animador", 480, 270)
fijo LINCE = juego.hoja("recursos/lince.png", 16)
sea pj = juego.animador(LINCE, {
    "quieto":  {"fila": 0, "por_segundo": 3},
    "andar":   {"fila": 1, "por_segundo": 10},
    "saltar":  {"fila": 2},
    "zarpazo": {"fila": 3, "por_segundo": 12, "repetir": falso, "siguiente": "quieto"}
})
sea x = 60
mientras juego.abierto() {
    x = x + 80 * juego.dt()
    juego.animacion(pj, "andar")
    juego.limpiar([190, 220, 240])
    juego.rectangulo(0, 200, 480, 70, [90, 160, 80])
    juego.dibujar(pj, x, 136, {"escala": 4})
    juego.texto("ANIMACIÓN: {pj["actual"]}", 20, 20, 2)
    juego.presentar()
}
Captura de «El animador»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/510_animador.lince

Entrada

Teclas y ratón

juego.tecla("derecha") dice si está pulsada ahora; juego.tecla_pulsada("espacio"), si se ha pulsado en este fotograma (para saltar una sola vez), y juego.tecla_soltada(), si se ha soltado. Igual con los botones del mando (boton, boton_pulsado, boton_soltado). juego.raton() da [x, y] y juego.clic(1), si se ha hecho clic con el botón izquierdo (juego.boton_raton(1) si está pulsado, y juego.boton_raton_soltado(1) si se acaba de soltar).

importar "juego"

juego.abrir("Entrada", 480, 270)
sea x = 60
sea marcas = []
mientras juego.abierto() {
    si juego.tecla("derecha") { x = x + 200 * juego.dt() }
    si juego.tecla("izquierda") { x = x - 200 * juego.dt() }
    si juego.clic(1) y marcas.longitud() < 5 { marcas.agregar(juego.raton()) }
    juego.limpiar([30, 30, 45])
    juego.rectangulo(x, 180, 40, 40, [230, 140, 60])
    para cada lista m en marcas { juego.circulo(m[0], m[1], 10, [120, 200, 255]) }
    juego.texto("RATÓN: {juego.raton()}", 20, 20, 2)
    juego.presentar()
}
Captura de «Teclas y ratón»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/610_teclas_y_raton.lince

Acciones: teclado y mando a la vez

Una acción junta varias entradas con un nombre: teclas, botones de mando ("mando:a"), la palanca ("mando:izq_x-"), el ratón o la pantalla táctil. El juego pregunta por la acción y funciona igual con todo. juego.direccion("izquierda", "derecha") va de −1 a 1, gradual con la palanca.

Con mandos, en un móvil sale solo un mando en la pantalla táctil (ver Móvil), y el jugador puede cambiar las teclas (ver Controles).

importar "juego"

juego.abrir("Acciones", 480, 270)
juego.definir_accion("izquierda", ["izquierda", "a", "mando:izquierda", "mando:izq_x-"])
juego.definir_accion("derecha", ["derecha", "d", "mando:derecha", "mando:izq_x+"])
juego.definir_accion("saltar", ["espacio", "w", "mando:a"])
sea x = 60
sea vy = 0
sea suelo = 200
sea yy = suelo
mientras juego.abierto() {
    x = x + juego.direccion("izquierda", "derecha") * 180 * juego.dt()
    si juego.accion_pulsada("saltar") y yy >= suelo { vy = -420 }
    vy = vy + 1200 * juego.dt()
    yy = yy + vy * juego.dt()
    si yy > suelo { yy = suelo }
    juego.limpiar([30, 35, 50])
    juego.rectangulo(0, suelo + 30, 480, 40, [80, 140, 80])
    juego.rectangulo(x, yy, 30, 30, [230, 140, 60])
    juego.texto("SALTAR: {juego.acciones()["saltar"]}", 20, 20, 2)
    juego.presentar()
}
Captura de «Acciones: teclado y mando a la vez»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/620_acciones.lince

Escribir un nombre

juego.editar(texto, máximo) añade lo que se teclea (con tildes y ñ) y aplica la tecla de borrar; se llama en cada fotograma. En el móvil saca el teclado de la pantalla mientras se use.

importar "juego"

juego.abrir("Nombre", 480, 270)
sea nombre = ""
mientras juego.abierto() {
    nombre = juego.editar(nombre, 12)
    juego.limpiar([20, 25, 40])
    juego.texto("¡NUEVO RÉCORD! TU NOMBRE:", 240, 90, {"escala": 2, "alinear": "centro"})
    juego.texto(nombre + "_", 240, 140, {"escala": 4, "alinear": "centro", "color": [255, 220, 80]})
    juego.presentar()
}
Captura de «Escribir un nombre»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/630_escribir.lince

Móvil

El mando de la pantalla táctil

En un móvil, si alguna acción usa el mando, sale solo un mando en la pantalla como el de los juegos de móvil: a la izquierda una palanca que aparece donde se apoya el pulgar y a la derecha los botones que usan las acciones. Funciona como un mando de verdad, así que un juego hecho para teclado y mando se juega en el móvil sin cambiar nada. Se esconde si se conecta un mando de verdad, por Bluetooth o por cable.

Aquí sale en el ordenador porque se pide con "siempre". juego.mando_tactil(falso) lo quita, para un juego con controles propios.

importar "juego"

juego.abrir("Móvil", 480, 270)
juego.definir_accion("saltar", ["espacio", "mando:a"])
juego.definir_accion("zarpazo", ["z", "mando:b"])
juego.definir_accion("izquierda", ["izquierda", "mando:izq_x-"])
juego.mando_tactil({"siempre": verdadero})
mientras juego.abierto() {
    juego.limpiar([60, 110, 170])
    juego.rectangulo(0, 200, 480, 70, [80, 150, 80])
    juego.presentar()
}
Captura de «El mando de la pantalla táctil»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/710_mando_tactil.lince

Dedos, vibrar e inclinar

juego.toques() da los dedos que hay en la pantalla (en el ordenador, el botón izquierdo del ratón cuenta como un dedo). juego.vibrar(segundos) hace vibrar el móvil y los mandos, y juego.inclinacion() dice hacia dónde está inclinado el móvil (en el ordenador, [0, 0]). Una acción puede usar la inclinación: "inclinacion:x-".

Para ponerlo en el móvil: lince repartir juego.lince --android (ver Compartir el juego).

importar "juego"

juego.abrir("Dedos", 480, 270)
mientras juego.abierto() {
    juego.limpiar([25, 25, 35])
    para cada diccionario dedo en juego.toques() {
        juego.circulo(dedo["x"], dedo["y"], 30, [255, 255, 255, 120])
        juego.texto("DEDO {dedo["id"]}", dedo["x"] + 36, dedo["y"] - 6, 2)
    }
    juego.texto("TÁCTIL: {juego.tactil()}   INCLINACIÓN: {juego.inclinacion()}", 20, 20, 1)
    juego.presentar()
}
Captura de «Dedos, vibrar e inclinar»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/720_toques.lince

Sonido

Efectos sin archivos

juego.efecto("moneda") genera y hace sonar un efecto al estilo de los juegos de 8 bits: "moneda", "salto", "disparo", "explosion", "golpe", "subir", "clic" o "perder". Cada vez suena un poco distinto. juego.crear_efecto() lo da como sonido, para juego.reproducir() o para guardarlo con juego.guardar_sonido().

importar "juego"

juego.abrir("Sonido", 320, 180)
juego.efecto("moneda")
sea salto = juego.crear_efecto("salto", {"tono": 1.2})
juego.reproducir(salto, {"volumen": 0.8, "lado": -0.5})      # algo a la izquierda
escribir("el salto dura {salto["duracion"]} s")
juego.pitido(440, 0.1)
salida verificada
el salto dura 0.241338 s
docs/juego/ejemplos/810_sonido.lince

Sonidos y música de archivos

juego.sonido(ruta) carga un .wav o un .ogg, y juego.reproducir() lo hace sonar (se mezclan hasta 32 a la vez), con volumen, lado (de −1, izquierda, a 1, derecha) y tono. juego.musica("tema.ogg") suena en bucle y se va leyendo poco a poco, así una canción entera apenas ocupa memoria.

importar "juego"
importar "matematica"

fijo PASO = juego.sonido("paso.wav")
juego.reproducir(PASO, {"volumen": 0.6, "tono": 0.9 + matematica.aleatorio() * 0.2})
juego.musica("musica/bosque.ogg", 0.5)          # en bucle
juego.musica(nulo)                              # la para
juego.volumen(0.8)                              # el volumen general

Movimiento

Movimiento suave

juego.suavizar(desde, hasta, segundos, inicio, curva) da el valor que toca ahora en un cambio que empezó en inicio (un juego.tiempo()): para que un menú entre desde un lado o un título caiga y rebote. Las curvas: "lineal", "suave", "entrar", "salir", "rebote", "elastico" y "atras". juego.curva(nombre, t) da la curva tal cual, como en esta gráfica.

juego.acercar(actual, objetivo, rapidez) da un paso hacia el objetivo, más largo cuanto más lejos: una cámara que sigue al jugador sin tirones.

importar "juego"

juego.abrir("Curvas", 480, 270)
fijo CURVAS = ["lineal", "suave", "entrar", "salir", "rebote", "elastico", "atras"]
mientras juego.abierto() {
    juego.limpiar([20, 22, 32])
    para (i = 0; i < CURVAS.longitud(); i++) {
        sea x0 = 12 + i * 66
        juego.contorno(x0, 60, 56, 150, [60, 60, 80])
        para (k = 0; k <= 50; k++) {
            sea v = juego.curva(CURVAS[i], k / 50)
            juego.rectangulo(x0 + 3 + k, 190 - v * 110, 2, 2, [255, 200, 70])
        }
        juego.texto(CURVAS[i].mayusculas(), x0, 222, 1)
    }
    juego.texto("CURVA(NOMBRE, T)", 12, 20, 2)
    juego.presentar()
}
Captura de «Movimiento suave»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/910_suavizar.lince

Cámara

Cámara y temblor

juego.camara(x, y) desplaza todo lo que se dibuja después, para mundos más grandes que la pantalla; juego.camara() vuelve a (0, 0), para el marcador. juego.temblor(fuerza, segundos) sacude la pantalla, para un golpe o una explosión, y el marcador no tiembla.

importar "juego"
importar "matematica"

juego.abrir("Cámara", 480, 270)
sea jugador = 300
sea cam = 0
mientras juego.abierto() {
    jugador = jugador + 250 * juego.dt()
    cam = juego.acercar(cam, jugador - 240, 6)                  # la cámara va detrás, sin tirones
    juego.limpiar([150, 200, 240])
    juego.camara(cam, 0)
    para (i = 0; i < 40; i++) {                                   # el mundo
        juego.rectangulo(i * 120, 200, 110, 70, [90, 150 + (i % 2) * 20, 80])
        juego.circulo(i * 120 + 60, 170, 18, [60, 120, 60])
    }
    juego.rectangulo(jugador, 170, 30, 30, [230, 140, 60])
    juego.camara()                                                # el marcador, quieto
    juego.texto("CÁMARA X: {matematica.redondear(cam)}", 20, 20, 2)
    juego.presentar()
}
Captura de «Cámara y temblor»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/1010_camara.lince

Choques

¿Se tocan?

juego.colision(x1, y1, an1, al1, x2, y2, an2, al2) dice si se solapan dos rectángulos; juego.colision_circulos y juego.colision_circulo_rect, lo mismo con círculos (cada rectángulo puede ir en una lista [x, y, ancho, alto] y cada círculo en [x, y, radio]); juego.dentro(px, py, x, y, an, al), si un punto (un clic) está dentro de un rectángulo (un botón).

juego.caja(cosa, x, y) da el rectángulo de lo que se ve de verdad al dibujar una imagen, una hoja o un animador, sin el hueco transparente de alrededor: para que los choques sean justos.

importar "juego"

juego.abrir("Choques", 480, 270)
fijo LINCE = juego.hoja("recursos/lince.png", 16)
mientras juego.abierto() {
    juego.limpiar([25, 25, 35])
    sea toca = juego.colision(60, 60, 100, 60, 130, 90, 80, 80)
    juego.rectangulo(60, 60, 100, 60, [230, 100, 90, 200])
    juego.rectangulo(130, 90, 80, 80, [90, 160, 230, 200])
    juego.texto("COLISIÓN: {toca}", 60, 190, 2)
    juego.dibujar(LINCE, 300, 60, {"escala": 6})
    sea c = juego.caja(LINCE, 300, 60, {"escala": 6})
    juego.contorno(300, 60, 96, 96, [90, 90, 110])                # la celda entera
    juego.contorno(c[0], c[1], c[2], c[3], [255, 220, 80])        # lo que se ve
    juego.texto("CAJA: {c}", 300, 190, 1)
    juego.presentar()
}
Captura de «¿Se tocan?»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/1110_choques.lince

Mapas

Un nivel escrito con texto

juego.mapa(filas, tamaño) hace un mapa de casillas a partir de texto, un carácter por casilla. juego.mover_en_mapa(mapa, cosa) mueve algo según su vx y su vy sin atravesar las paredes ("#", o las que se digan) y deja en en_suelo, techo y pared con qué chocó; la gravedad la sumas tú. juego.dibujar_mapa lo pinta, con un color o un tile para cada carácter.

importar "juego"

juego.abrir("Mapa", 480, 270)
juego.definir_accion("izquierda", ["izquierda", "a", "mando:izq_x-"])
juego.definir_accion("derecha", ["derecha", "d", "mando:izq_x+"])
fijo NIVEL = juego.mapa([
    "###############",
    "#.............#",
    "#.............#",
    "#......====...#",
    "#.............#",
    "#...##........#",
    "#...##....#...#",
    "###############"
], 32, {"plataformas": "="})
sea yo = {"x": 40, "y": 100, "ancho": 20, "alto": 28, "vx": 0, "vy": 0}
mientras juego.abierto() {
    yo["vx"] = juego.direccion("izquierda", "derecha") * 160
    yo["vy"] = yo["vy"] + 1000 * juego.dt()
    juego.mover_en_mapa(NIVEL, yo)
    juego.limpiar([30, 30, 50])
    juego.dibujar_mapa(NIVEL, {"#": [90, 80, 120], "=": [180, 140, 80]})
    juego.rectangulo(yo["x"], yo["y"], yo["ancho"], yo["alto"], [240, 150, 60])
    juego.texto("EN SUELO: {yo["en_suelo"]}  PARED: {yo["pared"]}", 40, 8, 1)
    juego.presentar()
}
Captura de «Un nivel escrito con texto»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/1210_mapa.lince

Tiles y tiles animados

Lo normal es pintar el nivel con una hoja de tiles: a cada carácter, [hoja, cuadro], o [hoja, [cuadros], segundos] para uno animado (el agua, las monedas). Los tiles se ajustan al tamaño de la casilla. juego.casilla y juego.poner_casilla leen y cambian una casilla (coger una moneda, abrir una puerta), y juego.buscar_en_mapa dice dónde están.

importar "juego"

juego.abrir("Tiles", 480, 270)
fijo TILES = juego.hoja("recursos/tiles.png", 16)
fijo NIVEL = juego.mapa([
    "...............",
    "..c.......c....",
    "...............",
    "......$$$......",
    "....-----..x...",
    "............xx.",
    "hhhhhh~~~hhhhhh",
    "tttttt~~~tttttt"
], 32)
mientras juego.abierto() {
    juego.limpiar([120, 190, 240])
    juego.dibujar_mapa(NIVEL, {
        "h": [TILES, 0], "t": [TILES, 1], "x": [TILES, 5], "-": [TILES, 12], "c": [TILES, 10],
        "~": [TILES, [3, 4], 0.4],                 # agua: dos cuadros
        "$": [TILES, [6, 7, 8, 9], 0.12]           # monedas que giran
    })
    juego.texto("MONEDAS: {juego.buscar_en_mapa(NIVEL, "$").longitud()}", 10, 8, 2)
    juego.presentar()
}
Captura de «Tiles y tiles animados»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/1220_tiles.lince

Buscar el camino

juego.camino(mapa, [c1, f1], [c2, f2]) da el camino más corto entre dos casillas sin atravesar paredes, como lista de [columna, fila]: para enemigos que persiguen al jugador por un laberinto. Con un cuarto argumento verdadero también va en diagonal.

importar "juego"

juego.abrir("Camino", 480, 270)
fijo LAB = juego.mapa([
    "###############",
    "#.....#.......#",
    "#.###.#.#####.#",
    "#.#...#.....#.#",
    "#.#.#######.#.#",
    "#...#.......#.#",
    "#####.#######.#",
    "###############"
], 32)
mientras juego.abierto() {
    juego.limpiar([20, 20, 30])
    juego.dibujar_mapa(LAB, {"#": [70, 70, 100]})
    para cada lista c en juego.camino(LAB, [1, 1], [13, 6]) {
        juego.circulo(c[0] * 32 + 16, c[1] * 32 + 16, 5, [255, 210, 70])
    }
    juego.circulo(48, 48, 12, [240, 90, 90])
    juego.circulo(13 * 32 + 16, 6 * 32 + 16, 12, [90, 200, 120])
    juego.presentar()
}
Captura de «Buscar el camino»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/1230_camino.lince

Mapas de Tiled

Niveles hechos con Tiled

Tiled es el editor de mapas gratuito más usado. juego.mapa_tiled(ruta) lee sus mapas (.tmx o .tmj) con sus tilesets, y da un mapa de Lince normal: mover_en_mapa, camino y las demás funciones sirven igual. Lo sólido se marca en Tiled: una capa llamada Colisiones, o la propiedad solido en una capa o en un tile del tileset (o plataforma).

juego.dibujar_tiled(mapa, capas) lo dibuja con los tiles animados, girados y volteados, la opacidad y el paralaje de cada capa: el fondo antes del personaje y lo de delante después. Los objetos que se pusieron en Tiled (dónde empieza el jugador, las monedas) vienen en mapa["objetos"].

importar "juego"

juego.abrir("Tiled", 480, 240)
juego.definir_accion("izquierda", ["izquierda", "a", "mando:izq_x-"])
juego.definir_accion("derecha", ["derecha", "d", "mando:izq_x+"])
fijo NIVEL = juego.mapa_tiled("recursos/nivel.tmx")
fijo LINCE = juego.hoja("recursos/lince.png", 16)
sea yo = {"x": 0, "y": 0, "ancho": 14, "alto": 12, "vx": 0, "vy": 0}
sea monedas = []
para cada diccionario cosa en NIVEL["objetos"] {
    si cosa["tipo"] == "jugador" {
        yo["x"] = cosa["x"]
        yo["y"] = cosa["y"]
    }
    si cosa["tipo"] == "moneda" { monedas.agregar(cosa) }
}
mientras juego.abierto() {
    yo["vx"] = juego.direccion("izquierda", "derecha") * 120
    yo["vy"] = yo["vy"] + 900 * juego.dt()
    juego.mover_en_mapa(NIVEL, yo)
    juego.camara(yo["x"] - 120, 0)
    juego.dibujar_tiled(NIVEL)
    para cada diccionario m en monedas {
        juego.circulo(m["x"] + 8, m["y"] + 8, 5, [250, 210, 60])
    }
    juego.dibujar(LINCE, yo["x"] - 1, yo["y"] - 4, {"fila": 1, "cuadro": juego.cuadro(4, 10)})
    juego.camara()
    juego.texto("{NIVEL["propiedades"]["nombre"]}  CAPAS: {NIVEL["capas"]}", 8, 8, 1)
    juego.presentar()
}
Captura de «Niveles hechos con Tiled»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/1310_tiled.lince

Física

Cajas, pelotas y ascensores

juego.fisica() hace un mundo con gravedad (y, si se le da, un mapa con el que chocar), y juego.cuerpo(mundo, datos) le añade cuerpos: cajas o círculos (radio) con masa, rebote y friccion. juego.avanzar_fisica(mundo) lo mueve todo en cada fotograma: caen, rebotan, se empujan según su masa y se apilan.

Un cuerpo fijo no lo mueve nada; con vx o vy es una plataforma o un ascensor que lleva lo que tiene encima. Uno sensor no choca, solo avisa de lo que toca (juego.tocando), para monedas o zonas. Los cuerpos son diccionarios normales: el personaje puede ser uno más.

importar "juego"

juego.abrir("Física", 480, 270)
fijo NIVEL = juego.mapa([
    "###############",
    "#.............#",
    "#.............#",
    "#.............#",
    "#.............#",
    "#.............#",
    "#.............#",
    "#.............#",
    "###############"
], 30)
sea mundo = juego.fisica({"gravedad": 900, "mapa": NIVEL})
para (i = 0; i < 4; i++) {
    juego.cuerpo(mundo, {"x": 60 + i * 6, "y": 40 + i * 45, "ancho": 30, "alto": 30})       # una torre que cae
}
juego.cuerpo(mundo, {"x": 220, "y": 40, "radio": 12, "rebote": 0.8})
juego.cuerpo(mundo, {"x": 280, "y": 60, "radio": 8, "rebote": 0.9, "masa": 0.5})
juego.cuerpo(mundo, {"x": 340, "y": 190, "ancho": 70, "alto": 8, "fijo": verdadero, "vy": -40})
juego.cuerpo(mundo, {"x": 360, "y": 150, "ancho": 24, "alto": 24})
mientras juego.abierto() {
    juego.avanzar_fisica(mundo)
    juego.limpiar([25, 30, 45])
    juego.dibujar_mapa(NIVEL, {"#": [70, 80, 110]})
    para cada diccionario c en mundo["cuerpos"] {
        si c.contiene("radio") {
            juego.circulo(c["x"] + c["radio"], c["y"] + c["radio"], c["radio"], [240, 120, 90])
        } sino {
            juego.rectangulo(c["x"], c["y"], c["ancho"], c["alto"], si c["fijo"] { [120, 200, 140] } sino { [200, 150, 80] })
            juego.contorno(c["x"], c["y"], c["ancho"], c["alto"], [60, 40, 20])
        }
    }
    juego.presentar()
}
Captura de «Cajas, pelotas y ascensores»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/1410_fisica.lince

Partículas

Chispas, polvo y explosiones

juego.particulas(x, y, opciones) lanza un puñado de partículas y juego.dibujar_particulas() las mueve y las dibuja en cada fotograma. Opciones: cantidad, color y color_final, velocidad, direccion y apertura (en grados), vida, tamaño y tamaño_final, gravedad y mezcla ("sumar" para que brillen). Las que admiten [mínimo, máximo] toman un valor al azar para cada una.

importar "juego"

juego.abrir("Partículas", 480, 270)
juego.particulas(110, 150, {"cantidad": 60, "color": [255, 220, 80], "color_final": [255, 60, 20, 0],
                            "velocidad": [60, 220], "vida": [0.4, 0.9], "mezcla": "sumar"})
juego.particulas(250, 200, {"cantidad": 30, "color": [200, 190, 170], "direccion": -90, "apertura": 70,
                            "velocidad": [40, 120], "tamaño": 6, "tamaño_final": 14, "gravedad": -30})
juego.particulas(380, 80, {"cantidad": 40, "color": [120, 200, 255], "velocidad": [80, 160],
                           "gravedad": 500, "vida": 1})
mientras juego.abierto() {
    juego.limpiar([15, 15, 25])
    juego.dibujar_particulas()
    juego.texto("CHISPAS", 70, 240, 2)
    juego.texto("POLVO", 215, 240, 2)
    juego.texto("AGUA", 350, 240, 2)
    juego.presentar()
}
Captura de «Chispas, polvo y explosiones»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/1510_particulas.lince

Escenas

Portada, partida y final

Un juego tiene partes: la portada, la partida, la pantalla final... juego.cambiar_escena("partida", 0.6) pasa a otra con un fundido a negro de 0,6 segundos; juego.escena() dice en cuál se está y juego.escena_nueva() es verdadero en su primer fotograma, para prepararla. La captura es de la mitad del fundido.

importar "juego"

juego.abrir("Escenas", 480, 270)
juego.cambiar_escena("portada")
mientras juego.abierto() {
    si juego.escena() == "portada" {
        juego.limpiar([40, 30, 70])
        juego.texto("LA PORTADA", 240, 120, {"escala": 4, "alinear": "centro"})
        si juego.tiempo_escena() > 0.2 { juego.cambiar_escena("partida", 0.6) }
    } sino {
        juego.limpiar([60, 140, 80])
        juego.texto("LA PARTIDA", 240, 120, {"escala": 4, "alinear": "centro"})
    }
    juego.presentar()
}
Captura de «Portada, partida y final»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/1610_escenas.lince

Controles

Que el jugador elija sus teclas

juego.menu_controles() enseña cada acción con sus teclas y sus botones, y deja cambiarlos: se elige una y se pulsa lo nuevo. Si otra acción ya lo usaba, se intercambian. Lo cambiado se guarda solo y se aplica solo la próxima vez que el juego defina esa acción, sin hacer nada más. Devuelve "volver" al salir. juego.restablecer_controles() vuelve a los del juego.

importar "juego"

juego.abrir("Controles", 480, 270)
juego.definir_accion("izquierda", ["izquierda", "a", "mando:izq_x-"])
juego.definir_accion("derecha", ["derecha", "d", "mando:izq_x+"])
juego.definir_accion("saltar", ["espacio", "mando:a"])
juego.definir_accion("disparar", ["z", "mando:x"])
mientras juego.abierto() {
    juego.limpiar([20, 22, 36])
    si juego.menu_controles({"nombres": {"izquierda": "Izquierda", "derecha": "Derecha", "saltar": "Saltar", "disparar": "Disparar"}}) == "volver" {
        juego.salir()
    }
    juego.presentar()
}
Captura de «Que el jugador elija sus teclas»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/1810_controles.lince

Partidas guardadas

Guardar y cargar

juego.guardar("partida", datos) guarda números, textos, listas y diccionarios, y juego.cargar("partida", por_defecto) los recupera (o da por_defecto si no hay nada). Van a la carpeta del usuario de cada sistema (en el móvil, la de la app), una por juego, sin pensar en rutas. Si el juego se cierra mientras guarda, lo de antes sigue entero. juego.olvidar("partida") lo borra.

Con un diccionario por defecto, lo que falte en lo guardado se toma de ahí: una opción nueva de una versión posterior del juego tiene valor desde el principio.

importar "juego"

sea opciones = juego.cargar("opciones", {"musica": 0.8, "idioma": "es"})
escribir("la primera vez: {opciones}")
juego.guardar("opciones", {"musica": 0.3})
juego.guardar("partida", {"nivel": 4, "vidas": 2, "llaves": ["roja", "azul"]})
escribir("después: {juego.cargar("opciones", {"musica": 0.8, "idioma": "es"})}")
escribir("la partida: {juego.cargar("partida")}")
juego.olvidar("partida")
escribir("olvidada: {juego.cargar("partida")}")
salida verificada
la primera vez: {"musica": 0.8, "idioma": es}
después: {"musica": 0.3, "idioma": es}
la partida: {"nivel": 4, "vidas": 2, "llaves": ["roja", "azul"]}
olvidada: nulo
docs/juego/ejemplos/1910_partidas.lince

Tareas

Esperar sin parar el juego

Una tarea se escribe como una función, pero puede esperar(segundos) sin parar el juego: avanza sola en cada juego.presentar(). Va muy bien para lo que pasa poco a poco: un parpadeo, un enemigo que patrulla, una cuenta atrás, una cinemática. t.terminada() dice si acabó y t.parar() la para.

importar "juego"

juego.abrir("Tareas", 480, 270)
sea mensaje = ""
sea brillo = falso

tarea cuenta_atras() {
    para cada texto t en ["3", "2", "1", "¡YA!"] {
        mensaje = t
        esperar(0.4)
    }
}

tarea parpadear() {
    mientras verdadero {
        brillo = no brillo
        esperar(0.15)
    }
}

cuenta_atras()
parpadear()
mientras juego.abierto() {
    juego.limpiar([20, 20, 35])
    juego.circulo(240, 170, 30, si brillo { [255, 230, 120] } sino { [120, 90, 40] })
    juego.texto(mensaje, 240, 60, {"escala": 6, "alinear": "centro"})
    juego.presentar()
}
Captura de «Esperar sin parar el juego»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/2010_tareas.lince

Peleas

Golpes que se notan

Para juegos de lucha, cada animación del animador puede decir con qué golpea cada cuadro ("golpes": [cuadro, x, y, ancho, alto]), y juego.golpe(pj, x, y) da esa caja colocada donde se dibuja, o nulo si en ese cuadro no golpea. Con "cancelar", desde ese cuadro se puede encadenar otro golpe, y juego.libre(pj) dice si ya se puede.

Lo que hace que un golpe pese: juego.congelar(0.05) (un parón de unas centésimas), juego.temblor(3) y el destello blanco en quien lo recibe. Además, juego.combo(["abajo", "derecha", "zarpazo"]) reconoce combinaciones, juego.mantenida() los golpes cargados y juego.pulsaciones() machacar un botón.

importar "juego"

juego.abrir("Peleas", 480, 270)
juego.definir_accion("zarpazo", ["z", "mando:x"])
fijo LINCE = juego.hoja("recursos/lince.png", 16)
sea pj = juego.animador(LINCE, {
    "quieto":  {"fila": 0, "por_segundo": 3},
    "zarpazo": {"fila": 3, "por_segundo": 12, "repetir": falso, "siguiente": "quieto",
                "golpes": [[1, 13, 8, 3, 4], [2, 13, 8, 3, 4]], "cancelar": 2}
})
sea destello = -1
mientras juego.abierto() {
    si juego.libre(pj) y juego.accion_pulsada("zarpazo") { juego.animacion(pj, "zarpazo", verdadero) }
    sea g = juego.golpe(pj, 150, 120, {"escala": 5})
    sea enemigo = [222, 130, 50, 70]
    si g != nulo y juego.colision(g, enemigo) y destello < 0 {
        destello = juego.tiempo()
        juego.congelar(0.05)
        juego.temblor(3)
        juego.particulas(224, 165, {"cantidad": 20, "color": [255, 240, 150], "mezcla": "sumar"})
    }
    juego.limpiar([45, 35, 60])
    juego.dibujar(pj, 150, 120, {"escala": 5})
    sea blanco = si destello >= 0 { 1 - (juego.tiempo() - destello) * 4 } sino { 0 }
    juego.rectangulo(enemigo[0], enemigo[1], enemigo[2], enemigo[3], [120, 160, 220])
    si blanco > 0 { juego.rectangulo(enemigo[0], enemigo[1], enemigo[2], enemigo[3], [255, 255, 255, blanco * 255]) }
    si g != nulo { juego.contorno(g[0], g[1], g[2], g[3], [255, 80, 80]) }
    juego.dibujar_particulas()
    juego.presentar()
}
Captura de «Golpes que se notan»
Captura real, sacada al generar esta página.
docs/juego/ejemplos/2110_peleas.lince

Compartir el juego

Un ejecutable, un .zip, un .apk

lince compilar hace un ejecutable con el juego dentro (el código, lo que importa y las imágenes, sonidos, fuentes y mapas que nombra), que funciona sin instalar Lince. lince repartir hace una carpeta y un .zip listos para compartir, con SDL2 dentro.

Para el móvil, --android hace el .apk para instalarlo y --aab el paquete que pide Google Play, firmados por Lince sin Java ni el SDK de Android. Cada versión nueva lleva un --version mayor.

lince compilar mi_juego.lince -o mi_juego
lince repartir mi_juego.lince                       # mi_juego-linux.zip o mi_juego-windows.zip
lince repartir mi_juego.lince --incluir niveles     # con una carpeta que el código no nombra
lince repartir mi_juego.lince --android --nombre "Mi juego" --id org.tunombre.mijuego
lince repartir mi_juego.lince --aab --version 2