Saltar al contenido principal

Tu primer juego con salas

Un juego con salas tiene varias pantallas o niveles, y un bucle que decide cuál se juega. En JARU IDE cada sala se dibuja en el editor de escenas y el IDE escribe su código. Tú solo escribes el bucle del juego: la pantalla de título, el paso de una sala a otra y el final.

Esta guía usa la plantilla StarterPlatformer: un juego de plataformas con título, dos salas (Level1 y Level2) y fin. Cada pieza enlaza con la página del manual que la cuenta entera.

Crear el proyecto desde la plantilla​

  1. Abre el asistente de proyectos, Project Wizard. Sale solo al arrancar el IDE. Si ya tienes un proyecto abierto, Home › Project › New lo cierra, después de preguntarte por lo que tengas sin guardar, y abre el asistente.
  2. En la lista Example Projects, haz doble clic en StarterPlatformer.
  3. Se abre Create Project from Example, que pide el nombre del proyecto nuevo, con StarterPlatformer puesto. Cámbialo si quieres, sin espacios, y acepta.
  4. El IDE copia la plantilla en Documentos\JARUProjects\<nombre> y la abre. Si ya hay una carpeta con ese nombre, te lo dice: elige otro.
  5. Pulsa Build › Run. El IDE compila el juego y lo abre en la VM de Windows.

En el título, Intro empieza la partida. Las flechas izquierda y derecha mueven al jugador, Espacio salta y Esc cierra el juego.

La otra plantilla, StarterTopDown, es un juego visto desde arriba, con título, una sala y fin. Se crea igual. El asistente entero está en Proyectos.

Las piezas​

Un juego con salas tiene cuatro piezas. Las dos primeras se hacen en el editor de escenas; la tercera la escribe el IDE, y la cuarta es tu código.

1. Las salas​

El editor de escenas con la sala Level1 de StarterPlatformer y la zona exit elegida, con Show in game encendido

Cada sala es un fichero .scn de la carpeta Scenes: en StarterPlatformer, Level1.scn y Level2.scn. Se abre con doble clic en el explorador. Una sala tiene:

  • Una capa de tiles, Tiles, que pinta el mapa de la sala (Maps/Level1.map). Los mapas se hacen en el editor de mapas, y las capas las cuenta Capas.
  • Una capa de objetos, Objects: el jugador (player), las monedas y los enemigos. Cada objeto puede llevar comportamientos: player lleva Platform control (andar y saltar con los controles), y los enemigos, Patrol (ir y volver). Ver Objetos y Comportamientos.
  • Zonas: rectángulos que no se ven y que vigilan las reglas, como pit (caerse al foso) y exit (la salida). La salida lleva Show in game: el juego la pinta como un recuadro verde, para que el jugador vea dónde está. Ver Zonas.
  • El mapa de colisión, que dice qué es suelo y qué es pared. No es la capa de tiles: se elige en Room › Collision map. En StarterPlatformer es el mismo Level1.map que pinta la capa, leído por número de tile (Tile numbers): los tiles 0, 1 y 2 son sólidos, y el 3 es una plataforma que se atraviesa saltando desde abajo. Ver Mapa de colisión.
  • Reglas (botón Logic): cuando pasa algo, haz algo. Ver Reglas y estados.

Las reglas de Level1 son estas:

Regla (WHEN)Acciones (DO)
When the room startsPlay or stop music: GameMusic.
When two things touch: player y coinDestroy (other, la moneda), Play a sound effect (coin) y Change a variable: game.score, add, 10.
When two things touch: player y enemy, con Not again for (s) a 1Play a sound effect (hit), Lose a life y Ask for another room: (this room).
When it enters a zone: player en pitLose a life y Ask for another room: (this room).
When it enters a zone: player en exitAsk for another room: Level2.

Level2 tiene las mismas, salvo la de exit: allí la salida suma 1 a la variable del juego won (ver Título y fin).

Play, en la barra del editor de escenas, prueba la sala sola, sin tu bucle: sin título ni fin. Sirve para probarla mientras la dibujas. Esc cierra la vista previa. Lo cuenta Guardar, generar, probar y compilar.

Play con recursos renombrados

El Play solo reconstruye los recursos (imágenes, sprites, mapas y música) cuando alguno ha cambiado. Si renombras, mueves o borras uno, no se entera: no los reconstruye, y la vista previa sigue con lo que había en Build. Para evitarlo, borra Build/assets.stamp antes de pulsar Play, o compila con Build › Compile, que los reconstruye siempre.

2. El juego​

La pestaña Game de StarterPlatformer: Rate (fps) a 30, las variables del juego con won y los seis controles

La pestaña Game del editor de escenas guarda lo que es de todo el juego y no de una sala, en Scenes/Game.json:

  • FRAME RATE: Rate (fps) es cuántos fotogramas por segundo pinta como mucho el juego, igual en Windows y en todas las placas. StarterPlatformer va a 30. El código generado lo pone una vez, al arrancar, con Display.fps. Con 0, lo decide tu código. Con física, 20 o más: por debajo, la sala va a cámara lenta.
  • VARIABLES: las variables del juego. score, lives, health y maxHealth están siempre. StarterPlatformer añade won: la salida de la última sala le suma 1. El código las lee como sceneGame.score o sceneGame.lives. Lo que una sala deja en ellas es lo que encuentra la siguiente.
  • CONTROLS: los controles del juego, con su tecla en Windows: left (LEFT), right (RIGHT), up (UP), down (DOWN), jump (SPACE) y start (ENTER). El código los mapea con SceneInputMap(), una vez, al arrancar. En una placa van con los GPIO de sus botones o con el mando I2C: lo cuenta En la placa.

Todo lo de esta pestaña está en La pestaña Game.

3. El código que escribe el IDE​

Al guardar una sala, el IDE escribe su código en la carpeta Code: Level1.gen.aru para la sala Level1, con estas funciones:

FunciónCuándoQué hace
Level1Load(nil)Al entrar en la salaCarga la sala y devuelve room, con todo lo que hay en ella.
Level1FollowCamera(room)Al entrar en la salaHace que la cámara siga a player.
Level1Update(room)En cada fotogramaLee el mando I2C, mueve los comportamientos y aplica las reglas.
room.scene.update()En cada fotogramaMueve la física, la cámara y las animaciones.
Level1Resolve(room)En cada fotogramaSaca los cuerpos dinámicos de los tiles sólidos del mapa de colisión.
Level1Draw(room)En cada fotogramaPinta la sala.

Dos de ellas no salen siempre:

  • Level1Update solo existe si la sala tiene comportamientos, reglas, estados, plantillas, variables de sala o código en algún objeto. El código de creación de la sala, solo, no basta. En una sala sin nada de eso, como una pantalla final, no hay que llamarla.
  • Level1FollowCamera solo existe si la cámara de la sala sigue a un objeto (Room › CAMERA › Follow).

Si llamas a una que no existe, el programa compila, pero se para al llegar a esa línea con el error Undefined variable. Todas las funciones de una sala están en El código que genera.

El nombre del fichero y el de las funciones salen del nombre de la sala (su campo Name, en la pestaña Room), pero solo con letras sin tilde, números y _: los espacios y los guiones pasan a ser _, las tildes y la ñ se pierden, y si el nombre empieza por un número se le pone una S delante. La sala «Canción» da Cancin.gen.aru y CancinLoad; la sala «Level 3», Level_3.gen.aru y Level_3Load. Para no llevarte sorpresas, pon a las salas nombres sin espacios ni tildes.

Todo esto lo hace mientras Room › BUILD › Generate .aru esté encendido, que es como viene. Apagado, ni guardar ni compilar escriben el .gen.aru de esa sala (si ya tenía uno, se queda como estaba), y el Play no la juega. Sirve para una sala a medio hacer que tu juego todavía no importa. En una sala que tu juego importa, déjalo encendido: ver los problemas conocidos.

SceneRuntime.gen.aru tiene lo que comparten todas las salas, como sceneGame, SceneInputMap() y SceneGameReset() (ver El runtime compartido). Los ficheros .gen.aru no se editan a mano: el IDE los vuelve a escribir al guardar y al compilar.

4. El bucle del juego​

Es el fichero principal del proyecto, Code/StarterPlatformer.aru. Importa las salas; al entrar en una, la carga (Entrar), y en cada fotograma la juega (Paso):

import "Screen.aru"        // abre la pantalla: va el primero
import "Level1.gen.aru"
import "Level2.gen.aru"

use Display, Input, Sound

var draw = Display.draw

var sala = ""
var room = nil

func Entrar(nombre)
sala = nombre
if (nombre == "Level1") then
room = Level1Load(nil)
Level1FollowCamera(room)
else
room = Level2Load(nil)
Level2FollowCamera(room)
end
end

func Paso()
if (sala == "Level1") then
Level1Update(room)
room.scene.update()
Level1Resolve(room)
Level1Draw(room)
else
Level2Update(room)
room.scene.update()
Level2Resolve(room)
Level2Draw(room)
end
end

Screen.aru solo abre la pantalla:

use Display

Display.open(320, 240)

Va el primero porque un import se ejecuta antes que la primera línea del fichero que lo importa, y los import se ejecutan en el orden en que aparecen. Así la pantalla coge su memoria antes de que las salas construyan sus tablas. Su tamaño, 320 × 240, es el de Room › Screen (px) en las dos salas.

Por qué cada fotograma va en ese orden lo explica Con tu código.

Cambiar de sala​

La ventana Room logic de Level1, con la regla de la zona exit y su acción Ask for another room, que pide Level2

La acción Ask for another room de una regla (Acciones) no cambia de sala: escribe el nombre de la sala pedida en room.nextScene. En Level1, la regla de la zona exit pide Level2. Quien cambia es el bucle del juego, cuando lo ve. Lo esencial es esto; en StarterPlatformer va dentro del estado JUGANDO (ver Título y fin):

Paso()
if (room.nextScene != "") then
Entrar(room.nextScene)
end

Así el juego decide cuándo y cómo se cambia: con una pantalla de «Fase 2» entre medias, guardando la partida… Pedir (this room), la misma sala, sirve para empezarla otra vez después de perder una vida: es lo que hacen las reglas de pit y de los enemigos.

Título y fin​

El título y el final no son salas: son estados del bucle. StarterPlatformer usa tres: TITULO, JUGANDO y FIN. Este es su bucle, con las constantes que usa (en el fichero va con sus comentarios):

const ACT_START = 5             // el control start de la pestaña Game: Intro en Windows
const ACT_SALIR = 9 // salir: Esc en Windows

const TITULO = 0
const JUGANDO = 1
const FIN = 2

var estado = TITULO
var mensaje = ""

SceneInputMap() // los controles de la pestaña Game
if (platform() == HW_WIN32) then Input.map(ACT_SALIR, "ESC") end

var antes = false
while (not Input.pressed(ACT_SALIR))
var pulsa = Input.pressed(ACT_START)
var empieza = pulsa and (not antes)
antes = pulsa

if (estado == TITULO) then
draw.cls(0x000000)
draw.text(96, 90, "MY PLATFORMER", 0xFFFF00)
draw.text(100, 140, "Press START", 0xFFFFFF)
if (empieza) then
SceneGameReset() // las variables del juego, como al principio
Entrar("Level1")
estado = JUGANDO
end
elsif (estado == JUGANDO) then
Paso()
Marcador() // las vidas y los puntos, arriba
if (sceneGame.lives <= 0) then
mensaje = "GAME OVER"
estado = FIN
Sound.stopMusic()
elsif (sceneGame.won > 0) then
mensaje = "YOU WIN!"
estado = FIN
Sound.stopMusic()
elsif (room.nextScene != "") then
Entrar(room.nextScene)
end
else
draw.cls(0x000000)
draw.text(112, 90, mensaje, 0xFFFF00)
draw.text(100, 120, "SCORE " + sceneGame.score.toString(), 0xFFFFFF)
draw.text(100, 160, "Press START", 0xFFFFFF)
if (empieza) then estado = TITULO end
end
Display.update()
end

Display.close()
  • SceneInputMap() mapea los controles de la pestaña Game. Nadie la llama por ti: el juego la llama una vez, antes del bucle (ver Cómo llegan los controles al juego). En Windows, el juego mapea además Esc para salir.
  • empieza vale true una sola vez por cada pulsación de start. El bucle lo calcula con antes, en vez de usar Input.justPressed.
  • En el título, SceneGameReset() devuelve todas las variables del juego a su valor de salida, won incluida.
  • Las reglas de las salas cambian las variables, y el bucle decide. Lose a life resta una vida, y la regla de la zona exit de Level2 suma 1 a won (Change a variable). Sin vidas, el juego dice «GAME OVER»; con won por encima de 0, «YOU WIN!».

Añadir una sala​

Así se añade Level3, detrás de Level2:

  1. Crea la sala. Abre Level2 en el editor de escenas. En el explorador, haz clic derecho en Scenes, elige New Scene y escribe Level3. La sala nueva copia los ajustes de Room de Level2: los tamaños, la pantalla, la música, la cámara, la física, los valores de colisión con Tile numbers y Generate .aru. No copia el mapa de colisión, a quién sigue la cámara, las variables de la sala ni el código de creación. Nace con una capa Tiles sin mapa, una capa Objects vacía y sin reglas. Ver Crear y abrir una sala.

  2. Dibújala.

    • Haz su mapa con New TileMap, con el tileset Images/Tiles.bmp (ver el editor de mapas). Elígelo en la capa Tiles (Tilemap), y la sala toma su tamaño. Elígelo también en Room › Collision map: sin mapa de colisión, el jugador atraviesa el suelo.
    • Copia los objetos de Level2 con Ctrl+C y pégalos en la capa Objects de Level3 con Ctrl+V: llegan al mismo sitio, con sus comportamientos. Después, muévelos a donde quieras.
    • En Room › CAMERA › Follow, elige player.
    • Copia las reglas de Level2, una a una: en su Room logic, haz clic en la lista WHEN, elige una regla y pulsa Ctrl+C; en la de Level3, Ctrl+V. Ver Copiar reglas entre salas.
    • Guárdala con Ctrl+S: el IDE escribe Code/Level3.gen.aru.
  3. Llámala desde el bucle. En StarterPlatformer.aru, los tres sitios están marcados con un comentario ROOMS. Añade import "Level3.gen.aru" junto a los otros dos, y una rama en Entrar() y otra en Paso(). La de Level2 es el else: pásala a un elsif y deja el else para Level3.

    func Entrar(nombre)
    sala = nombre
    if (nombre == "Level1") then
    room = Level1Load(nil)
    Level1FollowCamera(room)
    elsif (nombre == "Level2") then
    room = Level2Load(nil)
    Level2FollowCamera(room)
    else
    room = Level3Load(nil)
    Level3FollowCamera(room)
    end
    end

    En Paso(), lo mismo: elsif (sala == "Level2") then y un else con las cuatro llamadas de Level3. Llama solo a las funciones que tiene la sala: ver El código que escribe el IDE.

  4. Pide la sala nueva. En la Room logic de Level2, la regla de la zona exit suma 1 a won, y con eso se gana. Elígela, borra su acción con Delete y añade con + Action... la acción Ask for another room, con Level3 en Room. La regla de exit que copiaste en Level3 es ahora la que suma won.

En la placa​

El mismo juego funciona en una placa ESP32. Los pasos de siempre están en Tu programa en una placa: añadir tu placa al proyecto con Manage boards..., en el menú de Target; grabarle el firmware de la VM de esta versión con Flash Firmware; Run, y Open Serial para ver lo que dice. Aquí va lo que cambia en este juego.

Los botones de la placa​

En Windows, cada control es una tecla. En una placa, cada control necesita el GPIO de un botón o el mando I2C; el ESP32-S31 lee además un teclado USB. Los controles de StarterPlatformer no traen ningún GPIO: traen el mando (ver abajo). Para jugar con los botones de tu placa:

  1. Añade tu placa al proyecto y elígela en Target.
  2. En la pestaña Game del editor de escenas, pulsa Edit controls.... Se abre la ventana Game controls.
  3. Elige cada control y escribe el GPIO de su botón en BUTTONS ON THE BOARD (GPIO), en la fila de tu familia de placas. Hacen falta por lo menos left, right, jump y start.
  4. Si no tienes el mando, apágalo: pulsa Gamepad on, en la misma ventana, y pasa a Gamepad off.
  5. Guarda con Ctrl+S en el editor de escenas y pulsa Run.
La ventana Game controls con el control start: la tecla ENTER, el botón Start del mando y la fila ESP32-S3 sin GPIO

Qué GPIO es cada botón depende de tu placa. La ventana entera está en La ventana Game controls.

Sin GPIO ni mando, el juego arranca sin entrada y se queda en el título. Con Open Serial, el Output lo dice: Controls: no gamepad and no GPIO buttons for this board in Game > Controls, so no input. Con tu placa en Target, el panel de problemas del editor de escenas también lo avisa.

En la placa no hay tecla para salir: Esc solo se mapea en Windows, y el bucle no acaba nunca.

El mando I2C​

StarterPlatformer trae encendido el mando I2C de Adafruit (Mini I2C Gamepad), en la dirección 0x50, con su driver en Code/Drivers/DRV_SEESAW_GAMEPAD.aru. El joystick mueve, A es jump y Start es start.

El mando arranca en las placas cuyo perfil tiene los pines del bus I2C (Config › Platform › ESP32, grupo I2C Bus). La plantilla trae el perfil JARU JaruCade: un ESP32-S3, con SDA en el GPIO 11 y SCL en el 10. Si en el proyecto no hay ningún perfil del chip de tu placa con pines I2C, la placa lo dice al arrancar: Gamepad: no I2C pins for this board in Config > ESP32, so no gamepad. Todo sobre el mando está en El mando I2C.

En el título y en el fin no se lee el mando

Cada sala lee el mando en su <Sala>Update, pero el título y el fin no son salas. En una placa que solo tiene el mando, Start no hace nada en el título, y el juego no empieza. Léelo tú fuera de las salas, con SceneInputRead() al principio del bucle:

while (not Input.pressed(ACT_SALIR))
if (estado != JUGANDO) then SceneInputRead() end // fuera de las salas
var pulsa = Input.pressed(ACT_START)

SceneInputRead() solo existe si tu código la nombra: el siguiente Build la escribe en SceneRuntime.gen.aru.

Si no usas el mando, apágalo

Encendido, el juego abre el bus I2C de la placa al arrancar, y en el ESP32-S31 eso tiene una pega: si el mando no contesta y ningún control tiene GPIO, el juego no usa las teclas del teclado USB y se queda sin entrada.

El botón Gamepad on de la ventana Game controls lo apaga para todo el juego: pasa a Gamepad off.

Todos los problemas conocidos de JARU, juntos, están en Problemas conocidos.

El sonido​

El sonido solo sale después de Sound.init(), también en Windows. Cada sala que suena lo llama al cargarse, y StarterPlatformer lo llama además al principio del fichero principal: así suena también lo que tu código toque fuera de las salas, como en el título. Llamarlo otra vez no hace nada. Ver Música.