Saltar al contenido principal

Módulo Scene

Una escena reúne los sprites de un nivel. En cada fotograma, update() mueve su física (gravedad, velocidades, choques y rebotes entre sprites), mueve la cámara y calcula qué sprites se ven. draw.scene(escena) pinta después el mapa de tiles de la escena y esos sprites, desde la cámara.

Cada sala del editor de escenas es una escena: ver Las salas del editor de escenas.

use Scene

El módulo funciona igual en Windows y en todas las placas.

Funciones​

FunciónQué haceDevuelve
Scene.new()Crea una escena vacía.Una escena

Scene.new()​

Crea una escena vacía. No lleva argumentos: con alguno, lanza ERR_INVALID_ARG_NUMBER.

use Scene

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

Así viene una escena nueva:

De serie
Física (physics)Encendida
Gravedad (gravity)0
Pasadas del solver (iters)3
Pasos fijos (stepRate)0: un paso en cada update()
Tiempo máximo por update() (maxDelta)0.05 s
Reposo (sleeping)Apagado
CámaraEn (0, 0), sin seguir a nadie y sin límites
Modo de la cámaraCLAMP en los dos ejes
Sprites y mapa de tilesNinguno

Constantes​

Los modos de la cámara, para cameraMode:

ConstanteValorQué hace la cámara al llegar a los límites
Scene.CLAMP0Se para en ellos.
Scene.FREE1No los mira: se mueve sin límites.
Scene.WRAP2Da la vuelta: max − min es la distancia tras la que se repite.

Objetos que devuelve​

Scene.new() devuelve una escena, con estas propiedades y métodos. Todos sus errores se lanzan, así que se capturan con try: un argumento de más o de menos da ERR_INVALID_ARG_NUMBER, y uno de otro tipo, ERR_TYPE_MISMATCH.

Propiedades de la escena​

Se leen y se cambian como un campo: escena.gravity = 500.

PropiedadTipoDe serieQué es
physicsbooleanotrueSi update() mueve los cuerpos.
gravitynúmero0La gravedad, hacia abajo, en píxeles por segundo al cuadrado.
itersentero3Las pasadas del solver en cada paso de física.
stepRatenúmero0Los pasos de física por segundo. Con 0, uno en cada update().
maxDeltanúmero0.05Lo más que avanza la física en un update(), en segundos.
sleepingbooleanofalseSi los cuerpos quietos se duermen.

physics​

Con true, update() simula los cuerpos de la escena: la gravedad, las velocidades, los choques y los rebotes entre sprites. Con false no los mueve: un sprite solo cambia de sitio si lo mueve tu código. La cámara sigue funcionando igual.

Sin física, update() sigue guardando prevX y prevY de cada sprite, que es lo que necesita hitsGrid para no atravesar paredes (ver Sprite).

use Scene

var escena = Scene.new()
escena.physics = false // solo cámara: los sprites los mueve tu código

Tiene que ser true o false: otro valor lanza ERR_TYPE_MISMATCH.

gravity​

La gravedad que tira de los cuerpos DYNAMIC, en píxeles por segundo al cuadrado. Un valor positivo tira hacia abajo y uno negativo, hacia arriba. Admite decimales.

use Scene

var escena = Scene.new()
escena.gravity = 500 // un juego de plataformas
escena.gravity = 0 // un juego espacial, sin gravedad

Cambiarla despierta a los cuerpos dormidos (ver sleeping).

iters​

Las pasadas del solver en cada paso de física, de 1 a 255. Más pasadas hacen las pilas y los choques más firmes, y gastan más tiempo de CPU. Las 3 de serie valen para casi todo.

Un valor fuera de ese rango se ajusta: 0 se queda en 1, y 999, en 255. Tiene que ser un entero: 2.5 lanza ERR_TYPE_MISMATCH.

stepRate​

Los pasos de física por segundo.

  • Con 0, cada update() es un paso con el tiempo del último fotograma. Lo que hace la física depende entonces de los fotogramas por segundo: en un PC que pinta cientos, onGround() parpadea y se pierden saltos.
  • Con un número, la física da pasos fijos de 1 / stepRate segundos: los que quepan en el tiempo del fotograma, y el tiempo que sobra pasa al siguiente. Así hace lo mismo a cualquier ritmo. El editor de escenas pone 60.
use Scene

var escena = Scene.new()
escena.stepRate = 60 // 60 pasos por segundo, vaya el juego a 30 o a 200 fotogramas
  • En cada update() hace como mucho 4 pasos. Si stepRate pasa de 4 veces los fotogramas por segundo, la escena va a cámara lenta: con 240 a 30 fotogramas por segundo, a la mitad.
  • Un paso no puede ser más largo que maxDelta. Con un stepRate menor que 1 / maxDelta (20, con el valor de serie), los pasos son de maxDelta.
  • Un valor negativo cuenta como 0. Cambiar stepRate tira el tiempo que sobraba.

maxDelta​

Lo más que avanza la física en un update(), en segundos. Si un fotograma tarda más, la escena solo simula maxDelta y va a cámara lenta: con los 0,05 de serie, por debajo de 20 fotogramas por segundo.

use Scene

var escena = Scene.new()
escena.maxDelta = 0.1 // un juego a 10 fotogramas por segundo no va a cámara lenta

Sirve para que un tirón (cargar un fichero, el recolector de basura) no haga saltar a los sprites de golpe. Súbelo si tu juego va más despacio de 20 fotogramas por segundo. Un valor de 0 o negativo no cambia nada.

Se guarda con la precisión de un decimal corto: al leerlo, los 0,05 de serie salen como 0.05000000074505806.

sleeping​

Con el reposo encendido, los cuerpos que llevan medio segundo sin moverse se duermen: dejan de gastar CPU y no se recalculan hasta que algo los toca. Viene apagado.

use Scene

var escena = Scene.new()
escena.sleeping = true // encender el reposo
var duermen = escena.sleeping // consultarlo

Un cuerpo dormido se despierta cuando lo golpea otro cuerpo, cuando lo empuja una plataforma que se mueve o cuando tu programa cambia alguna de sus propiedades físicas. Cambiar la gravedad de la escena, o apagar el reposo, los despierta a todos. Los cuerpos STATIC y KINEMATIC nunca se duermen, porque tampoco gastan simulación.

Tiene que ser true o false: otro valor lanza ERR_TYPE_MISMATCH.

Ahorro en placas pequeñas

En una escena con objetos que acaban quietos (cajas, escombros, fruta caída), el reposo quita casi todo el coste de la física en cuanto se asientan. En un ESP32 se nota mucho.

Métodos de la escena​

MétodoQué haceDevuelve
addSprite(sprite)Añade un sprite.true, o false si no queda memoria
removeSprite(sprite)Quita un sprite.true, o false si no estaba
clearSprites()Quita todos los sprites.nil
spriteCount()Cuenta los sprites.Un entero
setGridMap(mapa)Pone el mapa de tiles de la escena, o lo quita con nil.nil
getGridMap()Lee el mapa de tiles.Un GridMap, o nil
setCamera(x, y)Pone la cámara en un sitio y deja de seguir.nil
getCamera()Lee la posición de la cámara.Una lista [x, y]
follow(sprite, suavizado)Hace que la cámara siga a un sprite.nil
unfollow()Deja de seguirlo.nil
setBounds(minX, minY, maxX, maxY)Pone los límites de la cámara.nil
setBoundsFromGrid()Pone los límites a partir del mapa de tiles.nil
clearBounds()Quita los límites.nil
cameraMode()Lee o cambia el modo de la cámara en cada eje.[modoX, modoY], o true al cambiarlo
sleepAll()Duerme todos los cuerpos en el siguiente paso.nil
update()Un fotograma de la escena: física, cámara y lo que se ve.nil

addSprite​

addSprite(sprite) añade el sprite al final de la lista de la escena. Lanza ERR_TYPE_MISMATCH si no le das un sprite.

use Scene, Sprite, Bitmap

var escena = Scene.new()
var jugador = Sprite.new(Bitmap.load("Images/Jugador.bmp"))
jugador.bodyType = Sprite.DYNAMIC
escena.addSprite(jugador)
  • Un sprite DYNAMIC, STATIC o KINEMATIC entra en la física. Uno VISUAL (el tipo de serie) no choca con nada: se pinta, y se mueve si lo mueves tú. Los tipos de cuerpo están en Sprite.
  • draw.scene pinta los sprites en el orden de la lista: el último, delante.
  • No mira si el sprite ya estaba. Si lo añades dos veces, está dos veces: spriteCount() lo cuenta dos veces y draw.scene lo pinta dos veces.

removeSprite​

removeSprite(sprite) quita el sprite de la escena: deja de moverse con la física y de pintarse con draw.scene. Devuelve true, o false si el sprite no estaba. Si estaba dos veces, quita una.

use Scene, Sprite, Bitmap

var escena = Scene.new()
var enemigo = Sprite.new(Bitmap.load("Images/Enemigo.bmp"))
escena.addSprite(enemigo)
println(escena.removeSprite(enemigo)) // true
println(escena.removeSprite(enemigo)) // false
Quitar un sprite cambia el orden de dibujo

Al quitar un sprite, el último de la lista ocupa su sitio. Desde ese momento, ese sprite se pinta detrás de los que antes tenía detrás. Si un sprite tiene que verse siempre delante, no quites otros: apágalos con sprite.active = false, que deja de pintarse y de chocar sin mover a los demás.

clearSprites​

clearSprites() quita todos los sprites de la escena. Los sprites siguen existiendo: puedes volver a añadirlos.

spriteCount​

spriteCount() devuelve cuántos sprites tiene la escena. Un sprite añadido dos veces cuenta dos.

use Scene

var escena = Scene.new()
println("Sprites en la escena: ", escena.spriteCount()) // Sprites en la escena: 0

setGridMap​

setGridMap(mapa) pone el mapa de tiles de la escena; con nil, lo quita. Lanza ERR_TYPE_MISMATCH si no le das un GridMap ni nil.

use Scene, GridMap

var escena = Scene.new()
var mapa = GridMap.new(60, 15)
mapa.tSize(16, 16)
escena.setGridMap(mapa)

La escena usa el mapa para dos cosas:

  • draw.scene lo pinta debajo de los sprites, desde la cámara de la escena, con su tileset. Si el mapa no tiene tileset, draw.scene lanza ERR_INVALID_VALUE.
  • setBoundsFromGrid() saca de él los límites de la cámara.

Los sprites no chocan solos con el mapa. Para eso, llama a mapa.hits(sprite) en cada fotograma, después de update(): ver GridMap.

use Scene, Sprite, Bitmap, GridMap

var mapa = GridMap.new(60, 15)
mapa.solids = [1]
var jugador = Sprite.new(Bitmap.load("Images/Jugador.bmp"))
jugador.bodyType = Sprite.DYNAMIC
var escena = Scene.new()
escena.addSprite(jugador)
escena.setGridMap(mapa)

// En cada fotograma:
escena.update()
mapa.hits(jugador) // saca al jugador de los tiles sólidos

getGridMap​

getGridMap() devuelve el mapa de tiles de la escena, o nil si no tiene.

use Scene

var escena = Scene.new()
if (escena.getGridMap() == nil) then
println("La escena no tiene mapa") // La escena no tiene mapa
end

setCamera​

setCamera(x, y) pone la cámara en un sitio. x e y son el borde de la izquierda y el de arriba de lo que se ve, en coordenadas del mundo. Si la cámara seguía a un sprite, deja de seguirlo.

use Scene

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

Si la escena tiene límites (setBounds), no se aplican hasta el siguiente update(): hasta entonces, getCamera() devuelve lo que pusiste.

getCamera​

getCamera() devuelve la posición de la cámara en una lista [x, y], con decimales.

use Scene

var escena = Scene.new()
escena.setCamera(100, 50)
var camara = escena.getCamera()
println("Cámara en ", camara[0], ", ", camara[1]) // Cámara en 100, 50

follow​

follow(sprite, suavizado) hace que la cámara siga a un sprite: en cada update(), la mueve para dejar el sprite (su x y su y) en el centro de lo que se ve.

use Scene, Sprite, Bitmap

var escena = Scene.new()
var jugador = Sprite.new(Bitmap.load("Images/Jugador.bmp"))

escena.follow(jugador) // pegada al jugador
escena.follow(jugador, 0.1) // se queda atrás y lo alcanza poco a poco
ParámetroTipoQué es
spriteSpriteEl sprite que sigue la cámara.
suavizadonúmeroOpcional, de 0 a 1 (fuera de ese rango se ajusta). Con 1, que es lo de serie, va pegada. Con menos, recorre esa parte de la distancia que le falta en cada 1/60 de segundo, así que se mueve igual a cualquier ritmo de fotogramas. Con 0 no se mueve.

El centro de lo que se ve sale de Display.viewWidth y Display.viewHeight. El suavizado usa el tiempo del fotograma, que mide Display.update(): sin Display.update(), la cámara no se mueve.

unfollow​

unfollow() deja de seguir al sprite. La cámara se queda donde está.

setBounds​

setBounds(minX, minY, maxX, maxY) pone a mano hasta dónde puede moverse la cámara. Los cuatro valores no son el rectángulo del mundo: limitan la posición de la cámara, que es la esquina de arriba a la izquierda de lo que se ve (la de setCamera() y getCamera()). Qué se hace con ellos depende del modo de cada eje (cameraMode):

  • CLAMP: la posición se queda entre min y max. Para no enseñar nada fuera del mundo, max es el tamaño del mundo menos el de lo que se ve.
  • WRAP: la posición da la vuelta; max − min es la distancia tras la que se repite.
  • FREE: los límites no cuentan.
use Display, Scene

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

// Un nivel de 1280 x 240: la cámara va de X = 0 a 1280 menos el ancho de la vista
// (960 en una pantalla de 320 x 240)
escena.setBounds(0, 0, 1280 - Display.viewWidth, 240 - Display.viewHeight)

// Un fondo que se repite cada 1280 px en horizontal
escena.cameraMode([Scene.WRAP, Scene.CLAMP])
escena.setBounds(0, 0, 1280, 0)
ParámetroTipoQué es
minX, minYnúmeroLa posición más pequeña de la cámara.
maxX, maxYnúmeroLa más grande. Con CLAMP: el tamaño del mundo menos el de lo que se ve. Con WRAP: el final del periodo.

Si la escena tiene un mapa de tiles, setBoundsFromGrid() hace esta cuenta por ti. Los límites se aplican dentro de update(), después de mover la cámara con follow().

setBoundsFromGrid​

setBoundsFromGrid() pone los límites a partir del mapa de tiles de la escena, para que la cámara no enseñe nada fuera de él. Tiene en cuenta la x y la y del mapa y su tamaño de tile.

use Scene, GridMap

var escena = Scene.new()
var mapa = GridMap.new(60, 15)
mapa.tSize(16, 16)
escena.setGridMap(mapa)
escena.setBoundsFromGrid()
  • Si el mapa es más pequeño que lo que se ve, la cámara se queda centrada en él.
  • Cambia también el modo de la cámara: FREE en el eje en que el mapa da la vuelta (wrapX o wrapY) y CLAMP en el otro. Si quieres otro modo, llama a cameraMode() después.
  • Sin mapa de tiles, lanza ERR_INVALID_VALUE.
  • Basta con llamarla una vez, al cargar el nivel.

clearBounds​

clearBounds() quita los límites: la cámara se mueve sin restricciones, sea cual sea su modo.

cameraMode​

cameraMode() devuelve el modo de la cámara en cada eje, en una lista [modoX, modoY]. cameraMode([modoX, modoY]) lo cambia y devuelve true. Los modos son las constantes CLAMP, FREE y WRAP.

use Scene

var escena = Scene.new()
escena.cameraMode([Scene.WRAP, Scene.CLAMP]) // da la vuelta en horizontal
var modo = escena.cameraMode()
println("Modo X: ", modo[0], " Modo Y: ", modo[1]) // Modo X: 2 Modo Y: 0
  • Los modos solo hacen algo con límites: con setBounds() o setBoundsFromGrid().
  • La lista tiene que tener dos enteros de 0 a 2. Si no es una lista, o algún elemento no es un entero, lanza ERR_TYPE_MISMATCH. Con otro tamaño u otro número, ERR_INVALID_VALUE.

sleepAll​

sleepAll() duerme de golpe todos los cuerpos de la escena en el siguiente paso de física. Solo hace algo con sleeping encendido.

use Scene

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

// ...aquí colocas las cajas de la torre...

escena.sleepAll() // la torre se queda quieta hasta que algo la golpee

Sirve para montar una estructura y dejarla quieta sin gastar CPU. Los cuerpos se despiertan como siempre en cuanto reciben un golpe.

No hace falta para que la estructura se sostenga: sin dormirla, una pila de cuerpos aguanta sola, aunque al empezar se asienta unos píxeles. Con sleepAll() se queda justo donde la colocaste y no gasta CPU desde el primer fotograma.

Se aplica una sola vez

sleepAll() duerme lo que hay en el siguiente paso de física, no en los demás. Si añades cuerpos después y también quieres dormirlos, vuelve a llamarla.

update​

update() hace un fotograma de la escena. Llámala una vez en cada fotograma, antes de pintar:

  1. Guarda la posición de cada sprite en prevX y prevY.
  2. Si physics está encendida, mueve los cuerpos: la gravedad, las velocidades, los choques y los rebotes.
  3. Mueve la cámara hacia el sprite que sigue (follow).
  4. Aplica los límites de la cámara, según su modo.
  5. Calcula qué parte del mundo se ve: draw.scene no pinta los sprites que quedan fuera.

La física avanza el tiempo del último fotograma, el que hay entre los dos últimos Display.update() (como mucho, maxDelta). Por eso:

  • Sin Display.update() en el bucle, la física y el suavizado de la cámara no avanzan.
  • Si llamas a update() dos veces en el mismo fotograma, la escena va al doble de rápido.
use Display, Scene

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

for (var i = 0; i < 300; i++) // 10 segundos
escena.update() // física, cámara y lo que se ve
draw.scene(escena) // mapa y sprites, desde la cámara
Display.update() // muestra el fotograma y mide su tiempo
end
Display.close()

Las salas del editor de escenas​

Cada sala del editor de escenas tiene su escena: room.scene. La crea el código que genera el editor, en <Sala>Load, con lo que pusiste en la pestaña Room:

En el editor (Room)En la escena
PHYSICS › Enabledphysics
PHYSICS › Gravitygravity
PHYSICS › Itersiters
PHYSICS › Step rate (Hz)stepRate (si no es 0)
CAMERA › Bounds, Use boundssetBounds(...)
CAMERA › Mode X / YcameraMode([...])
CAMERA › Follow, Smooth %follow(...), dentro de <Sala>FollowCamera(room)
La capa de tiles marcada con Scene map (Capas)setGridMap(...)

El editor no cambia maxDelta: con física, el ritmo del juego (Rate (fps), en la pestaña Game) tiene que ser de 20 o más.

Tu bucle llama a room.scene.update() una vez en cada fotograma, entre <Sala>Update(room) y <Sala>Resolve(room), y pinta con <Sala>Draw(room) en vez de draw.scene. Puedes leer y cambiar room.scene como cualquier escena: room.scene.gravity = 300.

Ejemplo completo​

Un jugador que corre y salta por un suelo de tiles, con la cámara detrás. Necesita dos imágenes en el proyecto: Images/Tiles.bmp, con dos tiles de 16 x 16 (el segundo es el suelo), y Images/Jugador.bmp.

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

const IZQUIERDA = 0
const DERECHA = 1
const SALTAR = 2
const SALIR = 3

if (platform() == HW_WIN32) then
Input.map(IZQUIERDA, "LEFT")
Input.map(DERECHA, "RIGHT")
Input.map(SALTAR, "SPACE")
Input.map(SALIR, "ESC")
else
// Pines de ejemplo: pon los de los botones de tu placa
Input.map(IZQUIERDA, 32)
Input.map(DERECHA, 33)
Input.map(SALTAR, 25)
Input.map(SALIR, 26)
end

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

// Un nivel de 60 x 15 tiles de 16 px: el valor 0 es aire y el 1, suelo
var mapa = GridMap.new(60, 15)
mapa.tSize(16, 16)
mapa.tEmpty(0)
mapa.tileset = Bitmap.load("Images/Tiles.bmp")
for (var x = 0; x < 60; x++)
mapa.set(x, 14, 1)
end
mapa.solids = [1]

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

var escena = Scene.new()
escena.gravity = 500
escena.stepRate = 60
escena.addSprite(jugador)
escena.setGridMap(mapa)
escena.setBoundsFromGrid()
escena.follow(jugador, 0.1)

var salir = false
while (not salir)
var vel = jugador.velocity
vel[0] = 0
if (Input.pressed(IZQUIERDA)) then
vel[0] = -120
elsif (Input.pressed(DERECHA)) then
vel[0] = 120
end
if (Input.pressed(SALTAR) and jugador.onGround()) then
vel[1] = -250
end
jugador.velocity = vel

escena.update() // física, cámara y lo que se ve
mapa.hits(jugador) // el suelo

draw.scene(escena)
Display.update()
salir = Input.pressed(SALIR)
end
Display.close()

Trampas y problemas conocidos​

  • Sin update(), draw.scene no pinta los sprites. Es update() quien calcula qué parte del mundo se ve; hasta la primera llamada, draw.scene da por fuera de la vista a todos los sprites que no están junto a la esquina (0, 0).
  • Sin Display.update(), la escena no se mueve. La física avanza el tiempo que mide Display.update() entre fotogramas.
  • Una llamada a update() por fotograma. Con dos, la física va al doble de rápido.
  • A menos de 20 fotogramas por segundo, cámara lenta. Es por maxDelta: súbelo si tu juego va más despacio.
  • Un stepRate muy alto para tus fotogramas, también cámara lenta. Cada update() hace como mucho 4 pasos.
  • El mapa de tiles no choca solo. setGridMap es para pintarlo y para los límites de la cámara; los choques con el mapa los resuelve mapa.hits(sprite), después de update().
  • setBoundsFromGrid() cambia el modo de la cámara. Llama a cameraMode() después, no antes.
  • Quitar un sprite cambia el orden de dibujo. Ver removeSprite.
  • Más iters, más CPU. Usa las menos que te valgan; las 3 de serie valen para casi todo.

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