Skip to main content

Scene module

A scene brings together the sprites of a level. In each frame, update() moves its physics (gravity, velocities, collisions and bounces between sprites), moves the camera and works out which sprites are in view. Then draw.scene(scene) paints the scene's tilemap and those sprites, from the camera.

Each room of the scene editor is a scene: see The rooms of the scene editor.

use Scene

The module works the same on Windows and on every board.

Functions​

FunctionWhat it doesReturns
Scene.new()Creates an empty scene.A scene

Scene.new()​

Creates an empty scene. It takes no arguments: with any, it throws ERR_INVALID_ARG_NUMBER.

use Scene

var scene = Scene.new()
println(scene.physics, " ", scene.gravity, " ", scene.iters) // true 0 3

This is what a new scene comes with:

Default
Physics (physics)On
Gravity (gravity)0
Solver iterations (iters)3
Fixed steps (stepRate)0: one step in each update()
Maximum time per update() (maxDelta)0.05 s
Sleeping (sleeping)Off
CameraAt (0, 0), following nobody, with no bounds
Camera modeCLAMP on both axes
Sprites and tilemapNone

Constants​

The camera modes, for cameraMode:

ConstantValueWhat the camera does when it reaches the bounds
Scene.CLAMP0It stops at them.
Scene.FREE1It ignores them: it moves with no bounds.
Scene.WRAP2It wraps around: max − min is the distance after which it repeats.

Objects it returns​

Scene.new() returns a scene, with these properties and methods. All their errors are thrown, so try catches them: one argument too many or too few gives ERR_INVALID_ARG_NUMBER, and one of the wrong type, ERR_TYPE_MISMATCH.

Scene properties​

You read and change them like a field: scene.gravity = 500.

PropertyTypeDefaultWhat it is
physicsbooleantrueWhether update() moves the bodies.
gravitynumber0The gravity, downwards, in pixels per second squared.
itersinteger3The solver iterations in each physics step.
stepRatenumber0The physics steps per second. With 0, one in each update().
maxDeltanumber0.05The most the physics advances in one update(), in seconds.
sleepingbooleanfalseWhether bodies at rest fall asleep.

physics​

With true, update() simulates the bodies of the scene: gravity, velocities, collisions and bounces between sprites. With false it does not move them: a sprite only changes place if your code moves it. The camera works the same.

Without physics, update() still saves prevX and prevY of each sprite, which is what hitsGrid needs so as not to go through walls (see Sprite).

use Scene

var scene = Scene.new()
scene.physics = false // camera only: your code moves the sprites

It has to be true or false: any other value throws ERR_TYPE_MISMATCH.

gravity​

The gravity that pulls the DYNAMIC bodies, in pixels per second squared. A positive value pulls down, and a negative one, up. It takes decimals.

use Scene

var scene = Scene.new()
scene.gravity = 500 // a platform game
scene.gravity = 0 // a space game, with no gravity

Changing it wakes up the sleeping bodies (see sleeping).

iters​

The solver iterations in each physics step, from 1 to 255. More iterations make stacks and collisions firmer, and take more CPU time. The default 3 is fine for almost everything.

A value out of that range is adjusted: 0 becomes 1, and 999, 255. It has to be an integer: 2.5 throws ERR_TYPE_MISMATCH.

stepRate​

The physics steps per second.

  • With 0, each update() is one step with the time of the last frame. What the physics does then depends on the frames per second: on a PC that draws hundreds of them, onGround() flickers and jumps are lost.
  • With a number, the physics takes fixed steps of 1 / stepRate seconds: as many as fit in the frame time, and the time left over carries to the next one. That way it does the same at any rate. The scene editor sets 60.
use Scene

var scene = Scene.new()
scene.stepRate = 60 // 60 steps per second, whether the game runs at 30 or 200 frames
  • Each update() takes at most 4 steps. If stepRate is more than 4 times the frames per second, the scene runs in slow motion: with 240 at 30 frames per second, at half speed.
  • A step cannot be longer than maxDelta. With a stepRate lower than 1 / maxDelta (20, with the default value), the steps are maxDelta long.
  • A negative value counts as 0. Changing stepRate drops the time left over.

maxDelta​

The most the physics advances in one update(), in seconds. If a frame takes longer, the scene only simulates maxDelta and runs in slow motion: with the default 0.05, below 20 frames per second.

use Scene

var scene = Scene.new()
scene.maxDelta = 0.1 // a game at 10 frames per second does not run in slow motion

It keeps a hitch (loading a file, the garbage collector) from making the sprites jump all at once. Raise it if your game runs slower than 20 frames per second. A value of 0 or below changes nothing.

It is stored with the precision of a short decimal: when you read it, the default 0.05 comes out as 0.05000000074505806.

sleeping​

With sleeping on, the bodies that have not moved for half a second fall asleep: they stop using CPU and are not computed again until something touches them. It is off by default.

use Scene

var scene = Scene.new()
scene.sleeping = true // turn sleeping on
var asleep = scene.sleeping // read it

A sleeping body wakes up when another body hits it, when a moving platform pushes it, or when your program changes any of its physics properties. Changing the gravity of the scene, or turning sleeping off, wakes them all. STATIC and KINEMATIC bodies never sleep, because they do not use any simulation either.

It has to be true or false: any other value throws ERR_TYPE_MISMATCH.

Savings on small boards

In a scene with objects that end up at rest (boxes, rubble, fallen fruit), sleeping removes almost all the cost of the physics as soon as they settle. On an ESP32 it makes a big difference.

Scene methods​

MethodWhat it doesReturns
addSprite(sprite)Adds a sprite.true, or false if there is no memory left
removeSprite(sprite)Removes a sprite.true, or false if it was not there
clearSprites()Removes all the sprites.nil
spriteCount()Counts the sprites.An integer
setGridMap(map)Sets the tilemap of the scene, or removes it with nil.nil
getGridMap()Reads the tilemap.A GridMap, or nil
setCamera(x, y)Puts the camera somewhere and stops following.nil
getCamera()Reads the position of the camera.A list [x, y]
follow(sprite, smoothing)Makes the camera follow a sprite.nil
unfollow()Stops following it.nil
setBounds(minX, minY, maxX, maxY)Sets the bounds of the camera.nil
setBoundsFromGrid()Sets the bounds from the tilemap.nil
clearBounds()Removes the bounds.nil
cameraMode()Reads or changes the camera mode on each axis.[modeX, modeY], or true when changing it
sleepAll()Puts every body to sleep in the next step.nil
update()One frame of the scene: physics, camera and what is in view.nil

addSprite​

addSprite(sprite) adds the sprite at the end of the scene's list. It throws ERR_TYPE_MISMATCH if you do not give it a sprite.

use Scene, Sprite, Bitmap

var scene = Scene.new()
var player = Sprite.new(Bitmap.load("Images/Player.bmp"))
player.bodyType = Sprite.DYNAMIC
scene.addSprite(player)
  • A DYNAMIC, STATIC or KINEMATIC sprite takes part in the physics. A VISUAL one (the default type) does not collide with anything: it is drawn, and it moves if you move it. The body types are in Sprite.
  • draw.scene paints the sprites in the order of the list: the last one, in front.
  • It does not check whether the sprite was already there. If you add it twice, it is there twice: spriteCount() counts it twice and draw.scene paints it twice.

removeSprite​

removeSprite(sprite) removes the sprite from the scene: it stops moving with the physics and being painted by draw.scene. It returns true, or false if the sprite was not there. If it was there twice, it removes one.

use Scene, Sprite, Bitmap

var scene = Scene.new()
var enemy = Sprite.new(Bitmap.load("Images/Enemy.bmp"))
scene.addSprite(enemy)
println(scene.removeSprite(enemy)) // true
println(scene.removeSprite(enemy)) // false
Removing a sprite changes the drawing order

When you remove a sprite, the last one in the list takes its place. From then on, that sprite is painted behind the ones it used to have behind it. If a sprite always has to be seen in front, do not remove others: turn them off with sprite.active = false, which stops drawing them and colliding with them without moving the rest.

clearSprites​

clearSprites() removes all the sprites from the scene. The sprites still exist: you can add them again.

spriteCount​

spriteCount() returns how many sprites the scene has. A sprite added twice counts twice.

use Scene

var scene = Scene.new()
println("Sprites in the scene: ", scene.spriteCount()) // Sprites in the scene: 0

setGridMap​

setGridMap(map) sets the tilemap of the scene; with nil, it removes it. It throws ERR_TYPE_MISMATCH if you give it neither a GridMap nor nil.

use Scene, GridMap

var scene = Scene.new()
var map = GridMap.new(60, 15)
map.tSize(16, 16)
scene.setGridMap(map)

The scene uses the map for two things:

  • draw.scene paints it under the sprites, from the scene's camera, with its tileset. If the map has no tileset, draw.scene throws ERR_INVALID_VALUE.
  • setBoundsFromGrid() takes the camera bounds from it.

The sprites do not collide with the map by themselves. For that, call map.hits(sprite) in each frame, after update(): see GridMap.

use Scene, Sprite, Bitmap, GridMap

var map = GridMap.new(60, 15)
map.solids = [1]
var player = Sprite.new(Bitmap.load("Images/Player.bmp"))
player.bodyType = Sprite.DYNAMIC
var scene = Scene.new()
scene.addSprite(player)
scene.setGridMap(map)

// In each frame:
scene.update()
map.hits(player) // pushes the player out of the solid tiles

getGridMap​

getGridMap() returns the tilemap of the scene, or nil if it has none.

use Scene

var scene = Scene.new()
if (scene.getGridMap() == nil) then
println("The scene has no map") // The scene has no map
end

setCamera​

setCamera(x, y) puts the camera somewhere. x and y are the left and top edges of what is in view, in world coordinates. If the camera was following a sprite, it stops following it.

use Scene

var scene = Scene.new()
scene.setCamera(100, 50)

If the scene has bounds (setBounds), they are not applied until the next update(): until then, getCamera() returns what you set.

getCamera​

getCamera() returns the position of the camera in a list [x, y], with decimals.

use Scene

var scene = Scene.new()
scene.setCamera(100, 50)
var camera = scene.getCamera()
println("Camera at ", camera[0], ", ", camera[1]) // Camera at 100, 50

follow​

follow(sprite, smoothing) makes the camera follow a sprite: in each update(), it moves it to leave the sprite (its x and y) in the middle of what is in view.

use Scene, Sprite, Bitmap

var scene = Scene.new()
var player = Sprite.new(Bitmap.load("Images/Player.bmp"))

scene.follow(player) // glued to the player
scene.follow(player, 0.1) // lags behind and catches up little by little
ParameterTypeWhat it is
spriteSpriteThe sprite the camera follows.
smoothingnumberOptional, from 0 to 1 (out of that range it is adjusted). With 1, the default, it stays glued. With less, it covers that part of the remaining distance every 1/60 of a second, so it moves the same at any frame rate. With 0 it does not move.

The middle of what is in view comes from Display.viewWidth and Display.viewHeight. The smoothing uses the frame time that Display.update() measures: without Display.update(), the camera does not move.

unfollow​

unfollow() stops following the sprite. The camera stays where it is.

setBounds​

setBounds(minX, minY, maxX, maxY) sets by hand how far the camera can move. The four values are not the world rectangle: they limit the position of the camera, which is the top-left corner of what is in view (the one of setCamera() and getCamera()). What is done with them depends on the mode of each axis (cameraMode):

  • CLAMP: the position stays between min and max. To show nothing outside the world, max is the size of the world minus the size of what is in view.
  • WRAP: the position wraps around; max − min is the distance after which it repeats.
  • FREE: the bounds do not count.
use Display, Scene

Display.open(320, 240)
var scene = Scene.new()

// A 1280 x 240 level: the camera goes from X = 0 to 1280 minus the width of the view
// (960 on a 320 x 240 screen)
scene.setBounds(0, 0, 1280 - Display.viewWidth, 240 - Display.viewHeight)

// A background that repeats every 1280 px horizontally
scene.cameraMode([Scene.WRAP, Scene.CLAMP])
scene.setBounds(0, 0, 1280, 0)
ParameterTypeWhat it is
minX, minYnumberThe smallest position of the camera.
maxX, maxYnumberThe largest. With CLAMP: the size of the world minus the size of what is in view. With WRAP: the end of the period.

If the scene has a tilemap, setBoundsFromGrid() does this sum for you. The bounds are applied inside update(), after moving the camera with follow().

setBoundsFromGrid​

setBoundsFromGrid() sets the bounds from the scene's tilemap, so that the camera shows nothing outside it. It takes into account the map's x and y and its tile size.

use Scene, GridMap

var scene = Scene.new()
var map = GridMap.new(60, 15)
map.tSize(16, 16)
scene.setGridMap(map)
scene.setBoundsFromGrid()
  • If the map is smaller than what is in view, the camera stays centred on it.
  • It also changes the camera mode: FREE on the axis where the map wraps (wrapX or wrapY) and CLAMP on the other. If you want another mode, call cameraMode() afterwards.
  • With no tilemap, it throws ERR_INVALID_VALUE.
  • Calling it once, when you load the level, is enough.

clearBounds​

clearBounds() removes the bounds: the camera moves with no restrictions, whatever its mode.

cameraMode​

cameraMode() returns the camera mode on each axis, in a list [modeX, modeY]. cameraMode([modeX, modeY]) changes it and returns true. The modes are the constants CLAMP, FREE and WRAP.

use Scene

var scene = Scene.new()
scene.cameraMode([Scene.WRAP, Scene.CLAMP]) // wraps horizontally
var mode = scene.cameraMode()
println("Mode X: ", mode[0], " Mode Y: ", mode[1]) // Mode X: 2 Mode Y: 0
  • The modes only do something with bounds: with setBounds() or setBoundsFromGrid().
  • The list has to hold two integers from 0 to 2. If it is not a list, or an element is not an integer, it throws ERR_TYPE_MISMATCH. With another size or another number, ERR_INVALID_VALUE.

sleepAll​

sleepAll() puts every body of the scene to sleep at once in the next physics step. It only does something with sleeping on.

use Scene

var scene = Scene.new()
scene.physics = true
scene.sleeping = true

// ...here you place the boxes of the tower...

scene.sleepAll() // the tower stays still until something hits it

It is useful to build a structure and leave it still without using CPU. The bodies wake up as usual as soon as they are hit.

It is not needed for the structure to hold: without putting it to sleep, a stack of bodies holds by itself, although it settles a few pixels at the start. With sleepAll() it stays exactly where you placed it and uses no CPU from the first frame.

It applies only once

sleepAll() puts to sleep what there is in the next physics step, not in the later ones. If you add bodies afterwards and want them asleep too, call it again.

update​

update() does one frame of the scene. Call it once in each frame, before drawing:

  1. It saves the position of each sprite in prevX and prevY.
  2. If physics is on, it moves the bodies: gravity, velocities, collisions and bounces.
  3. It moves the camera towards the sprite it follows (follow).
  4. It applies the camera bounds, according to its mode.
  5. It works out which part of the world is in view: draw.scene does not paint the sprites that are outside it.

The physics advances the time of the last frame, the time between the last two Display.update() calls (at most, maxDelta). So:

  • Without Display.update() in the loop, the physics and the camera smoothing do not advance.
  • If you call update() twice in the same frame, the scene runs twice as fast.
use Display, Scene

Display.open(320, 240)
Display.fps = 30
var draw = Display.draw
var scene = Scene.new()

for (var i = 0; i < 300; i++) // 10 seconds
scene.update() // physics, camera and what is in view
draw.scene(scene) // map and sprites, from the camera
Display.update() // shows the frame and measures its time
end
Display.close()

The rooms of the scene editor​

Each room of the scene editor has its scene: room.scene. The code the editor generates creates it, in <Room>Load, with what you set in the Room tab:

In the editor (Room)In the scene
PHYSICS › Enabledphysics
PHYSICS › Gravitygravity
PHYSICS › Itersiters
PHYSICS › Step rate (Hz)stepRate (if it is not 0)
CAMERA › Bounds, Use boundssetBounds(...)
CAMERA › Mode X / YcameraMode([...])
CAMERA › Follow, Smooth %follow(...), inside <Room>FollowCamera(room)
The tile layer marked with Scene map (Layers)setGridMap(...)

The editor does not change maxDelta: with physics, the game rate (Rate (fps), in the Game tab) has to be 20 or more.

Your loop calls room.scene.update() once in each frame, between <Room>Update(room) and <Room>Resolve(room), and draws with <Room>Draw(room) instead of draw.scene. You can read and change room.scene like any scene: room.scene.gravity = 300.

Full example​

A player who runs and jumps on a floor of tiles, with the camera behind. It needs two images in the project: Images/Tiles.bmp, with two 16 x 16 tiles (the second one is the floor), and Images/Player.bmp.

use Display, Bitmap, Sprite, Scene, GridMap, Input

const LEFT = 0
const RIGHT = 1
const JUMP = 2
const QUIT = 3

if (platform() == HW_WIN32) then
Input.map(LEFT, "LEFT")
Input.map(RIGHT, "RIGHT")
Input.map(JUMP, "SPACE")
Input.map(QUIT, "ESC")
else
// Example pins: use the ones of your board's buttons
Input.map(LEFT, 32)
Input.map(RIGHT, 33)
Input.map(JUMP, 25)
Input.map(QUIT, 26)
end

Display.open(320, 240)
Display.mode(2)
Display.fps = 30
var draw = Display.draw

// A level of 60 x 15 tiles of 16 px: value 0 is air and 1 is floor
var map = GridMap.new(60, 15)
map.tSize(16, 16)
map.tEmpty(0)
map.tileset = Bitmap.load("Images/Tiles.bmp")
for (var x = 0; x < 60; x++)
map.set(x, 14, 1)
end
map.solids = [1]

var player = Sprite.new(Bitmap.load("Images/Player.bmp"))
player.bodyType = Sprite.DYNAMIC
player.x = 40
player.y = 100

var scene = Scene.new()
scene.gravity = 500
scene.stepRate = 60
scene.addSprite(player)
scene.setGridMap(map)
scene.setBoundsFromGrid()
scene.follow(player, 0.1)

var quit = false
while (not quit)
var vel = player.velocity
vel[0] = 0
if (Input.pressed(LEFT)) then
vel[0] = -120
elsif (Input.pressed(RIGHT)) then
vel[0] = 120
end
if (Input.pressed(JUMP) and player.onGround()) then
vel[1] = -250
end
player.velocity = vel

scene.update() // physics, camera and what is in view
map.hits(player) // the floor

draw.scene(scene)
Display.update()
quit = Input.pressed(QUIT)
end
Display.close()

Pitfalls and known issues​

  • Without update(), draw.scene does not paint the sprites. It is update() that works out which part of the world is in view; until the first call, draw.scene takes every sprite that is not near the (0, 0) corner as out of view.
  • Without Display.update(), the scene does not move. The physics advances the time that Display.update() measures between frames.
  • One call to update() per frame. With two, the physics runs twice as fast.
  • Below 20 frames per second, slow motion. It is because of maxDelta: raise it if your game runs slower.
  • A stepRate too high for your frames, slow motion too. Each update() takes at most 4 steps.
  • The tilemap does not collide by itself. setGridMap is for painting it and for the camera bounds; the collisions with the map are resolved by map.hits(sprite), after update().
  • setBoundsFromGrid() changes the camera mode. Call cameraMode() after it, not before.
  • Removing a sprite changes the drawing order. See removeSprite.
  • More iters, more CPU. Use as few as work for you; the default 3 is fine for almost everything.

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