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ón | Qué hace | Devuelve |
|---|---|---|
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ámara | En (0, 0), sin seguir a nadie y sin límites |
| Modo de la cámara | CLAMP en los dos ejes |
| Sprites y mapa de tiles | Ninguno |
Constantes
Los modos de la cámara, para cameraMode:
| Constante | Valor | Qué hace la cámara al llegar a los límites |
|---|---|---|
Scene.CLAMP | 0 | Se para en ellos. |
Scene.FREE | 1 | No los mira: se mueve sin límites. |
Scene.WRAP | 2 | Da 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.
| Propiedad | Tipo | De serie | Qué es |
|---|---|---|---|
physics | booleano | true | Si update() mueve los cuerpos. |
gravity | número | 0 | La gravedad, hacia abajo, en píxeles por segundo al cuadrado. |
iters | entero | 3 | Las pasadas del solver en cada paso de física. |
stepRate | número | 0 | Los pasos de física por segundo. Con 0, uno en cada update(). |
maxDelta | número | 0.05 | Lo más que avanza la física en un update(), en segundos. |
sleeping | booleano | false | Si 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, cadaupdate()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 / stepRatesegundos: 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. SistepRatepasa 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 unstepRatemenor que1 / maxDelta(20, con el valor de serie), los pasos son demaxDelta. - Un valor negativo cuenta como
0. CambiarstepRatetira 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.
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étodo | Qué hace | Devuelve |
|---|---|---|
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,STATICoKINEMATICentra en la física. UnoVISUAL(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.scenepinta 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 ydraw.scenelo 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
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.scenelo pinta debajo de los sprites, desde la cámara de la escena, con sutileset. Si el mapa no tienetileset,draw.scenelanzaERR_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ámetro | Tipo | Qué es |
|---|---|---|
sprite | Sprite | El sprite que sigue la cámara. |
suavizado | número | Opcional, 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 entreminymax. Para no enseñar nada fuera del mundo,maxes el tamaño del mundo menos el de lo que se ve.WRAP: la posición da la vuelta;max − mines 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ámetro | Tipo | Qué es |
|---|---|---|
minX, minY | número | La posición más pequeña de la cámara. |
maxX, maxY | número | La 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:
FREEen el eje en que el mapa da la vuelta (wrapXowrapY) yCLAMPen el otro. Si quieres otro modo, llama acameraMode()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()osetBoundsFromGrid(). - 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.
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:
- Guarda la posición de cada sprite en
prevXyprevY. - Si
physicsestá encendida, mueve los cuerpos: la gravedad, las velocidades, los choques y los rebotes. - Mueve la cámara hacia el sprite que sigue (
follow). - Aplica los límites de la cámara, según su modo.
- Calcula qué parte del mundo se ve:
draw.sceneno 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 › Enabled | physics |
| PHYSICS › Gravity | gravity |
| PHYSICS › Iters | iters |
| PHYSICS › Step rate (Hz) | stepRate (si no es 0) |
| CAMERA › Bounds, Use bounds | setBounds(...) |
| CAMERA › Mode X / Y | cameraMode([...]) |
| 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.
- Cómo se monta un juego con salas, paso a paso: Tu primer juego con salas.
- Cada campo de Room: La sala y el juego.
- El código de cada sala y dónde va cada llamada: El código que genera.
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.sceneno pinta los sprites. Esupdate()quien calcula qué parte del mundo se ve; hasta la primera llamada,draw.sceneda 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 mideDisplay.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
stepRatemuy alto para tus fotogramas, también cámara lenta. Cadaupdate()hace como mucho 4 pasos. - El mapa de tiles no choca solo.
setGridMapes para pintarlo y para los límites de la cámara; los choques con el mapa los resuelvemapa.hits(sprite), después deupdate(). setBoundsFromGrid()cambia el modo de la cámara. Llama acameraMode()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.