Skip to main content

Your first game with rooms

A game with rooms has several screens or levels, and a loop that decides which one is played. In JARU IDE each room is drawn in the scene editor and the IDE writes its code. You only write the game loop: the title screen, going from one room to the next, and the end.

This guide uses the StarterPlatformer template: a platformer with a title screen, two rooms (Level1 and Level2) and an end screen. Each piece links to the page of the manual that tells all about it.

Create the project from the template​

  1. Open the project wizard, Project Wizard. It shows up by itself when the IDE starts. If you already have a project open, Home › Project › New closes it, after asking you about anything unsaved, and opens the wizard.
  2. In the Example Projects list, double-click StarterPlatformer.
  3. Create Project from Example opens and asks for the name of the new project, with StarterPlatformer filled in. Change it if you like, with no spaces, and accept.
  4. The IDE copies the template to Documents\JARUProjects\<name> and opens it. If there is already a folder with that name, it tells you: pick another one.
  5. Press Build › Run. The IDE builds the game and opens it in the Windows VM.

On the title screen, Enter starts the game. The left and right arrows move the player, Space jumps and Esc closes the game.

The other template, StarterTopDown, is a game seen from above, with a title screen, one room and an end screen. It is created the same way. The whole wizard is in Projects.

The pieces​

A game with rooms has four pieces. The first two are made in the scene editor; the IDE writes the third, and the fourth is your code.

1. The rooms​

The scene editor with StarterPlatformer's Level1 room and the exit zone selected, with Show in game on

Each room is a .scn file in the Scenes folder: in StarterPlatformer, Level1.scn and Level2.scn. Double-click it in the explorer to open it. A room has:

  • A tile layer, Tiles, that draws the room's tilemap (Maps/Level1.map). Tilemaps are made in the tilemap editor, and Layers tells all about layers.
  • An object layer, Objects: the player (player), the coins and the enemies. Each object can carry behaviours: player carries Platform control (walk and jump with the controls), and the enemies, Patrol (go back and forth). See Objects and Behaviours.
  • Zones: invisible rectangles that the rules watch, such as pit (falling into the pit) and exit (the way out). The exit has Show in game on: the game draws it as a green frame, so that the player can see where it is. See Zones.
  • The collision map, which tells what is floor and what is wall. It is not the tile layer: you choose it in Room › Collision map. In StarterPlatformer it is the same Level1.map the layer draws, read by tile number (Tile numbers): tiles 0, 1 and 2 are solid, and 3 is a platform you can jump up through. See Collision map.
  • Rules (Logic button): when something happens, do something. See Rules and states.

These are the rules of Level1:

Rule (WHEN)Actions (DO)
When the room startsPlay or stop music: GameMusic.
When two things touch: player and coinDestroy (other, the coin), Play a sound effect (coin) and Change a variable: game.score, add, 10.
When two things touch: player and enemy, with Not again for (s) at 1Play a sound effect (hit), Lose a life and Ask for another room: (this room).
When it enters a zone: player in pitLose a life and Ask for another room: (this room).
When it enters a zone: player in exitAsk for another room: Level2.

Level2 has the same ones, except the exit one: there the exit adds 1 to the game variable won (see Title and end).

Play, in the bar of the scene editor, tries the room alone, without your loop: no title screen and no end. Use it to try the room while you draw it. Esc closes the preview. Save, generate, play and compile tells all about it.

Play and renamed assets

Play only rebuilds the assets (images, sprites, tilemaps and music) when one of them has changed. If you rename, move or delete one, it does not notice: it does not rebuild them, and the preview keeps what was in Build. To avoid it, delete Build/assets.stamp before pressing Play, or build with Build › Compile, which always rebuilds them.

2. The game​

StarterPlatformer's Game tab: Rate (fps) at 30, the game variables with won, and the six controls

The Game tab of the scene editor keeps what belongs to the whole game and not to one room, in Scenes/Game.json:

  • FRAME RATE: Rate (fps) is how many frames per second the game draws at most, the same on Windows and on every board. StarterPlatformer runs at 30. The generated code sets it once, at start-up, with Display.fps. With 0, your code decides. With physics, 20 or more: below that, the room runs in slow motion.
  • VARIABLES: the game variables. score, lives, health and maxHealth are always there. StarterPlatformer adds won: the exit of the last room adds 1 to it. The code reads them as sceneGame.score or sceneGame.lives. What one room leaves in them is what the next one finds.
  • CONTROLS: the controls of the game, with their key on Windows: left (LEFT), right (RIGHT), up (UP), down (DOWN), jump (SPACE) and start (ENTER). The code maps them with SceneInputMap(), once, at start-up. On a board they go with the GPIO of their buttons or with the I2C gamepad: see On the board.

Everything in this tab is in The Game tab.

3. The code the IDE writes​

When you save a room, the IDE writes its code in the Code folder: Level1.gen.aru for room Level1, with these functions:

FunctionWhenWhat it does
Level1Load(nil)When the room startsLoads the room and returns room, with everything in it.
Level1FollowCamera(room)When the room startsMakes the camera follow player.
Level1Update(room)Every frameReads the I2C gamepad, runs the behaviours and applies the rules.
room.scene.update()Every frameMoves the physics, the camera and the animations.
Level1Resolve(room)Every framePushes the dynamic bodies out of the solid tiles of the collision map.
Level1Draw(room)Every frameDraws the room.

Two of them are not always there:

  • Level1Update only exists if the room has behaviours, rules, states, templates, room variables or code in some object. The creation code of the room alone is not enough. In a room with none of that, such as an ending screen, do not call it.
  • Level1FollowCamera only exists if the camera of the room follows an object (Room › CAMERA › Follow).

If you call one that does not exist, the program builds, but it stops when it reaches that line, with the error Undefined variable. All the functions of a room are in The code it generates.

The name of the file and of the functions come from the name of the room (its Name field, in the Room tab), but only with plain letters (no accents), digits and _: spaces and hyphens become _, accented letters and ñ are dropped, and if the name starts with a digit an S goes in front. A room called "Canción" gives Cancin.gen.aru and CancinLoad; a room called "Level 3", Level_3.gen.aru and Level_3Load. To avoid surprises, give your rooms names without spaces or accents.

All of this happens while Room › BUILD › Generate .aru is on, which is how it comes. When it is off, neither saving nor building writes the .gen.aru of that room (if it already had one, it stays as it was), and Play does not play it. It is meant for a half-done room that your game does not import yet. In a room your game imports, leave it on: see the known issues.

SceneRuntime.gen.aru holds what all the rooms share, such as sceneGame, SceneInputMap() and SceneGameReset() (see The shared runtime). The .gen.aru files are not edited by hand: the IDE writes them again when you save and when you build.

4. The game loop​

It is the main file of the project, Code/StarterPlatformer.aru. It imports the rooms; when it enters one it loads it (Entrar), and every frame it plays it (Paso):

import "Screen.aru"        // opens the screen: it goes first
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 only opens the screen:

use Display

Display.open(320, 240)

It goes first because an import runs before the first line of the file that imports it, and the imports run in the order they appear. This way the screen gets its memory before the rooms build their tables. Its size, 320 × 240, is the Room › Screen (px) of both rooms.

Why each frame goes in that order is explained in With your own code.

Switching rooms​

The Room logic window of Level1, with the exit zone rule and its Ask for another room action, which asks for Level2

The Ask for another room action of a rule (Actions) does not switch rooms: it writes the name of the room it asks for in room.nextScene. In Level1, the rule of the exit zone asks for Level2. The game loop is the one that switches, when it sees it. This is the gist; in StarterPlatformer it goes inside the JUGANDO state (see Title and end):

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

That way the game decides when and how to switch: with a "Level 2" screen in between, saving the game… Asking for (this room), the same room, starts it again after losing a life: that is what the rules of pit and of the enemies do.

Title and end​

The title and the end are not rooms: they are states of the loop. StarterPlatformer uses three: TITULO, JUGANDO and FIN. This is its loop, with the constants it uses (in the file it comes with its comments):

const ACT_START = 5             // the start control of the Game tab: Enter on Windows
const ACT_SALIR = 9 // quit: Esc on Windows

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

var estado = TITULO
var mensaje = ""

SceneInputMap() // the controls of the Game tab
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() // the game variables, as at the start
Entrar("Level1")
estado = JUGANDO
end
elsif (estado == JUGANDO) then
Paso()
Marcador() // the lives and the score, at the top
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() maps the controls of the Game tab. Nobody calls it for you: the game calls it once, before the loop (see How the controls reach the game). On Windows, the game also maps Esc to quit.
  • empieza is true only once for each press of start. The loop works it out with antes, instead of using Input.justPressed.
  • On the title screen, SceneGameReset() puts every game variable back to its start value, won included.
  • The rules of the rooms change the variables, and the loop decides. Lose a life takes one life away, and the rule of the exit zone of Level2 adds 1 to won (Change a variable). With no lives left, the game says "GAME OVER"; with won above 0, "YOU WIN!".

Adding a room​

This is how you add Level3, after Level2:

  1. Create the room. Open Level2 in the scene editor. In the explorer, right-click Scenes, choose New Scene and type Level3. The new room copies the Room settings of Level2: the sizes, the screen, the music, the camera, the physics, the collision values with Tile numbers, and Generate .aru. It does not copy the collision map, whom the camera follows, the room variables or the creation code. It starts with a Tiles layer with no map, an empty Objects layer and no rules. See Creating and opening a room.

  2. Draw it.

    • Make its map with New TileMap, with the tileset Images/Tiles.bmp (see the tilemap editor). Choose it in the Tiles layer (Tilemap), and the room takes its size. Choose it also in Room › Collision map: without a collision map, the player falls through the floor.
    • Copy the objects of Level2 with Ctrl+C and paste them in the Objects layer of Level3 with Ctrl+V: they land in the same place, with their behaviours. Then move them wherever you want.
    • In Room › CAMERA › Follow, choose player.
    • Copy the rules of Level2, one by one: in its Room logic, click the WHEN list, choose a rule and press Ctrl+C; in the one of Level3, Ctrl+V. See Copying rules between rooms.
    • Save it with Ctrl+S: the IDE writes Code/Level3.gen.aru.
  3. Call it from the loop. In StarterPlatformer.aru, the three places are marked with a ROOMS comment. Add import "Level3.gen.aru" next to the other two, and a branch in Entrar() and another in Paso(). The one of Level2 is the else: turn it into an elsif and leave the else for 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

    In Paso(), the same: elsif (sala == "Level2") then and an else with the four calls of Level3. Only call the functions the room has: see The code the IDE writes.

  4. Ask for the new room. In the Room logic of Level2, the rule of the exit zone adds 1 to won, and that wins the game. Choose it, delete its action with Delete and, with + Action..., add the Ask for another room action, with Level3 in Room. The exit rule you copied into Level3 is now the one that adds to won.

On the board​

The same game runs on an ESP32 board. The usual steps are in Your program on a board: add your board to the project with Manage boards..., in the Target menu; flash the VM firmware of this version with Flash Firmware; Run, and Open Serial to see what it says. Here is what changes for this game.

The buttons of the board​

On Windows, each control is a key. On a board, each control needs the GPIO of a button or the I2C gamepad; the ESP32-S31 also reads a USB keyboard. The controls of StarterPlatformer bring no GPIO: they bring the gamepad (see below). To play with the buttons of your board:

  1. Add your board to the project and choose it in Target.
  2. In the Game tab of the scene editor, press Edit controls.... The Game controls window opens.
  3. Choose each control and type the GPIO of its button in BUTTONS ON THE BOARD (GPIO), in the row of your board family. You need at least left, right, jump and start.
  4. If you do not have the gamepad, turn it off: press Gamepad on, in the same window, and it becomes Gamepad off.
  5. Save with Ctrl+S in the scene editor and press Run.
The Game controls window with the start control: the ENTER key, the gamepad's Start button and the ESP32-S3 row with no GPIO

Which GPIO each button is depends on your board. The whole window is in The Game controls window.

With no GPIO and no gamepad, the game starts with no input and stays on the title screen. With Open Serial, the Output says so: Controls: no gamepad and no GPIO buttons for this board in Game > Controls, so no input. With your board in Target, the problems panel of the scene editor warns about it too.

On the board there is no key to quit: Esc is only mapped on Windows, and the loop never ends.

The I2C gamepad​

StarterPlatformer comes with the Adafruit I2C gamepad (Mini I2C Gamepad) on, at address 0x50, with its driver in Code/Drivers/DRV_SEESAW_GAMEPAD.aru. The joystick moves, A is jump and Start is start.

The gamepad starts on the boards whose profile has the pins of the I2C bus (Config › Platform › ESP32, I2C Bus group). The template brings the JARU JaruCade profile: an ESP32-S3, with SDA on GPIO 11 and SCL on 10. If the project has no profile of the chip of your board with I2C pins, the board says so at start-up: Gamepad: no I2C pins for this board in Config > ESP32, so no gamepad. Everything about the gamepad is in The I2C gamepad.

On the title and end screens, nobody reads the gamepad

Each room reads the gamepad in its <Room>Update, but the title and the end are not rooms. On a board that only has the gamepad, Start does nothing on the title screen, and the game does not start. Read it yourself outside the rooms, with SceneInputRead() at the beginning of the loop:

while (not Input.pressed(ACT_SALIR))
if (estado != JUGANDO) then SceneInputRead() end // outside the rooms
var pulsa = Input.pressed(ACT_START)

SceneInputRead() only exists if your code names it: the next Build writes it in SceneRuntime.gen.aru.

If you do not use the gamepad, turn it off

When it is on, the game opens the I2C bus of the board at start-up, and on the ESP32-S31 that has a drawback: if the gamepad does not answer and no control has a GPIO, the game does not use the keys of the USB keyboard and is left with no input.

The Gamepad on button of the Game controls window turns it off for the whole game: it becomes Gamepad off.

All the known issues of JARU, together, are in Known issues.

The sound​

The sound only comes out after Sound.init(), on Windows too. Each room that makes sound calls it when it loads, and StarterPlatformer also calls it at the beginning of the main file: that way, what your code plays outside the rooms, such as on the title screen, sounds too. Calling it again does nothing. See Music.