Módulo Sprite
Un sprite es una imagen que se mueve por la pantalla: tiene posición, giro y uno o varios fotogramas que se animan solos. Sabe con qué choca, gracias a su collider, y puede ser un cuerpo físico al que una escena mueve con gravedad, velocidad y rebotes. Se pinta con draw.sprite o con draw.scene.
![]()
use Display
use Bitmap
use Sprite
Funciones
| Función | Qué hace | Devuelve |
|---|---|---|
Sprite.load(ruta) | Carga un sprite: su .json y sus imágenes | Un sprite |
Sprite.new(imagen) / Sprite.new(x, y, imagen) | Crea un sprite con un bitmap, o una copia de otro sprite | Un sprite |
load
La función load(ruta) carga un sprite desde un archivo de definición JSON junto con sus imágenes BMP asociadas. Quita la extensión de la ruta y lee el .json del mismo nombre: Sprite.load("Sprites/personaje.spr") lee Sprites/personaje.json.
var jugador = Sprite.load("Sprites/personaje.spr")
Al compilar, el IDE exporta así cada sprite del editor de sprites: un .json y su imagen. El archivo JSON debe tener la siguiente estructura:
{
"frameWidth": 32,
"frameHeight": 32,
"totalFrames": 8,
"fps": 12,
"suffix": 2,
"backgroundcolor": 0
}
| Campo | Descripción |
|---|---|
frameWidth | Ancho de cada fotograma en píxeles |
frameHeight | Alto de cada fotograma en píxeles |
totalFrames | Número total de fotogramas |
fps | Fotogramas por segundo de la animación: el sprite empieza con frameRate = 1000 / fps milisegundos. Con 0, o sin el campo, no se anima solo |
suffix | Dígitos en el sufijo numérico (ej: 2 = _00, _01...) |
backgroundcolor | El color transparente, 0xRRGGBB escrito en decimal (opcional; sin él, el negro) |
Los archivos de imagen deben nombrarse siguiendo el patrón: nombrebase_00.bmp, nombrebase_01.bmp, etc. Todos los campos son enteros: uno que no lo sea se ignora.
| Error | Cuándo |
|---|---|
ERR_FILE_NOT_FOUND | No está el .json o no se puede leer como JSON, o falta la imagen de un fotograma o de la hoja. El mensaje dice el fichero. |
ERR_OUT_OF_MEMORY | No hay memoria para cargar una de las imágenes. |
ERR_INVALID_VALUE | La imagen de la hoja no cuadra con el .json (ver abajo). |
ERR_INVALID_ARG_NUMBER | No le pasas una cadena. |
Hoja de fotogramas
Con "sheet": 1, todos los fotogramas van en un solo BMP, nombrebase.bmp, uno debajo de otro: el fotograma n empieza en la fila n × frameHeight. La imagen mide frameWidth de ancho y al menos frameHeight × totalFrames de alto; si no cuadra con el .json, Sprite.load da error.
{
"frameWidth": 32, "frameHeight": 32, "totalFrames": 8,
"fps": 12, "suffix": 2, "sheet": 1,
"backgroundcolor": 0
}
Así exporta el IDE los sprites al hacer Build: un fichero y una imagen por sprite en lugar de uno por fotograma. En placa carga mucho antes, gasta menos memoria y ocupa menos flash, y se pinta igual de rápido. Un sprite de más de 255 fotogramas se exporta con los fotogramas sueltos, como antes.
Los recursos con fotogramas sueltos (sin "sheet") se siguen cargando igual. Un Build con hojas necesita una VM que las conozca: si una placa lleva un firmware anterior, graba antes el firmware de la VM (Flash Firmware).
Campos opcionales del recurso
El editor de sprites del IDE puede escribir además estos campos. Todos son opcionales: cuando falta uno, el sprite se comporta exactamente igual que siempre, así que un recurso creado antes de que existieran sigue funcionando sin tocar nada.
Todos son enteros; los que representan un sí/no viajan como 0 o 1.
| Campo | Si falta | Descripción |
|---|---|---|
pivotX pivotY | centro del fotograma | Punto de rotación y posición, en coordenadas del fotograma (equivale a sprite.pivot) |
loop | 1 | 0 para que la animación se reproduzca una sola vez; al acabar, el sprite pasa a active = false (equivale a sprite.loop) |
yoyo | 0 | 1 para que la animación vaya y vuelva (equivale a sprite.yoyo) |
colliderType | sin collider | 1 rectángulo, 2 círculo. Sin este campo el collider es el fotograma entero |
colliderX colliderY | 0 | Desplazamiento del collider dentro del fotograma. Puede ser negativo |
colliderW colliderH | fotograma entero | Tamaño del collider rectangular |
colliderR | mitad del lado menor | Radio del collider circular |
sheet | 0 | 1: todos los fotogramas en un solo nombrebase.bmp (ver Hoja de fotogramas) |
{
"frameWidth": 32, "frameHeight": 24, "totalFrames": 4,
"fps": 12, "suffix": 3,
"pivotX": 16, "pivotY": 24,
"loop": 0, "yoyo": 1,
"colliderType": 1, "colliderX": 4, "colliderY": 2,
"colliderW": 24, "colliderH": 20
}
Definir el pivote en el recurso ahorra ponerlo a mano cada vez que se crea el sprite. Recuerda que el x/y de un sprite es su pivote, no su esquina superior izquierda: por defecto, el centro del fotograma.
new
La función new() crea un nuevo sprite a partir de un bitmap o copiando los fotogramas de otro sprite existente. Con tres argumentos, los dos primeros son su posición x, y.
| Error | Cuándo |
|---|---|
ERR_INVALID_ARG_NUMBER | No le pasas uno o tres argumentos. |
ERR_TYPE_MISMATCH | El último no es un bitmap ni un sprite, o x o y no son números. |
ERR_INVALID_VALUE | El sprite que copias no tiene fotogramas. |
Crear desde bitmap
var imagen = Bitmap.load("Images/nave.bmp")
// Crear en posición (0, 0)
var nave = Sprite.new(imagen)
// Crear en posición específica
var nave2 = Sprite.new(100, 50, imagen)
El sprite tiene un fotograma, la imagen, y su collider es la imagen entera, con el pivote en el centro. Empieza con frameRate = 25.
Clonar desde otro sprite
// Crear sprite original
var enemigo = Sprite.load("Sprites/enemigo.spr")
// Crear una copia en (0, 0) que comparte los mismos gráficos
var enemigo2 = Sprite.new(enemigo)
// Crear copias en posición específica
var enemigo3 = Sprite.new(200, 100, enemigo)
var enemigo4 = Sprite.new(300, 100, enemigo)
Cuando creas un sprite desde otro sprite, los fotogramas se comparten en memoria. Esto es muy eficiente para crear múltiples instancias del mismo personaje o enemigo.
La copia hereda también el pivote, la animación (frameRate, loop, yoyo), el volteo (flipX, flipY) y el collider del original: el de su .spr o el que le hayas puesto con sprite.collider. Así crea el editor de escenas cada objeto, y por eso el collider que defines en el editor de sprites es el que usan todos. No hereda la posición, el giro ni la física: empieza en el fotograma 0, activa y VISUAL.
Constantes
Tipos de cuerpo físico
| Constante | Valor | Descripción |
|---|---|---|
Sprite.VISUAL | 0 | Sin física, solo visual. El de serie |
Sprite.DYNAMIC | 1 | La escena lo mueve: gravedad, velocidad, choques y giros |
Sprite.STATIC | 2 | No se mueve, pero los dinámicos chocan con él: suelos y paredes |
Sprite.KINEMATIC | 3 | Lo mueves tú, con x/y o con su velocidad. No le afectan la gravedad ni los choques, pero empuja a los dinámicos: plataformas móviles, paletas |
Tipos de collider
| Constante | Valor | Descripción |
|---|---|---|
Sprite.RECTANGLE | 1 | Collider rectangular |
Sprite.CIRCLE | 2 | Collider circular |
Propiedades del sprite
Las propiedades se leen sin paréntesis y se asignan con =:
// Leer una propiedad
var posX = sprite.x
// Asignar una propiedad
sprite.x = 100
| Propiedad | Tipo | De serie | Lectura / escritura |
|---|---|---|---|
x, y | decimal | 0, o los de Sprite.new(x, y, …) | Las dos |
pos | lista [x, y] | — | Solo escritura |
prevX, prevY | decimal | Los mismos que x e y | Las dos |
angle | decimal, grados | 0 | Las dos |
spin | decimal, grados por segundo | 0 | Las dos |
pivot | lista [x, y] | El centro del fotograma | Las dos |
width, height | entero | El tamaño del fotograma | Solo lectura |
active | sí/no | true | Las dos |
flipX, flipY | sí/no | false | Las dos |
frame | entero | 0 | Las dos |
frames | entero | El número de fotogramas | Solo lectura |
frameRate | entero, milisegundos | 25 con Sprite.new; con Sprite.load, 1000 / fps | Las dos |
loop | sí/no | true | Las dos |
yoyo | sí/no | false | Las dos |
dir | 1 o -1 | 1 | Las dos |
collider | lista | El fotograma entero | Las dos |
colliderType | entero | Sprite.RECTANGLE | Solo lectura |
Las de física están en Física. Un valor del tipo que no toca no cambia la propiedad, y tampoco da error. Llamar a una propiedad como una función (sprite.pos(100, 200)) lanza ERR_INVALID_OPERATION.
Posición y transformación
x
Obtiene o establece la posición X del sprite en píxeles: la de su pivote.
// Obtener posición X
var posX = sprite.x
// Establecer posición X
sprite.x = 100
y
Obtiene o establece la posición Y del sprite en píxeles: la de su pivote.
// Obtener posición Y
var posY = sprite.y
// Establecer posición Y
sprite.y = 200
pos
Establece la posición del sprite con una lista [x, y]. Es una propiedad: se asigna, no se llama (sprite.pos(100, 200) da error).
sprite.pos = [100, 200]
Solo sirve para escribir: leer sprite.pos no devuelve la posición. Para leerla, usa sprite.x y sprite.y.
prevX y prevY
Obtienen o establecen dónde estaba el sprite al empezar el fotograma, en píxeles. Es el punto desde el que hitsGrid comprueba el recorrido del sprite y con el que decide si venía de arriba de una plataforma de un solo sentido.
scene.update() los pone al día para los sprites de la escena. Sin escena, o al colocar o teletransportar un sprite, los pones tú:
// Sin escena: al empezar cada fotograma, antes de mover el sprite
jugador.prevX = jugador.x
jugador.prevY = jugador.y
jugador.x += vx
jugador.y += vy
var colisiones = jugador.hitsGrid(mapa)
// Al teletransportarlo, para que no compruebe el camino
jugador.pos = [400, 120]
jugador.prevX = 400
jugador.prevY = 120
Un sprite creado con Sprite.new(100, 50, imagen) empieza con el previo en (100, 50); con Sprite.load o Sprite.new(imagen), en (0, 0). En un sprite que está en una escena, lo que escribas dura hasta el siguiente scene.update(). Asignarles algo que no es un número lanza ERR_TYPE_MISMATCH.
angle
Obtiene o establece el ángulo de rotación en grados. El sprite gira alrededor de su pivote.
// Obtener ángulo
var angulo = sprite.angle
// Rotar 45 grados
sprite.angle = 45
// Rotar continuamente
sprite.angle = sprite.angle + 1
spin
Obtiene o establece la velocidad de rotación automática en grados por segundo.
// Rotar automáticamente a 90 grados por segundo
sprite.spin = 90
// Detener rotación
sprite.spin = 0
// Rotar en sentido contrario
sprite.spin = -45
spin es la rotación manual del sprite. En cuerpos DYNAMIC, las colisiones pueden añadir además rotación física (ver angularVel y rotates en la sección de Física); ambas rotaciones se suman.
El giro se aplica al pintar el sprite: cada vez que lo pintas, angle avanza spin por el tiempo que ha pasado entre los dos últimos Display.update(). Un sprite que no se pinta no gira, y uno que pintas dos veces en el mismo fotograma gira el doble.
Después de una pausa o una carga larga, el siguiente fotograma gira todo el tiempo que ha pasado: con spin = 10, tras un pause(2000) el sprite salta 20 grados. Si te molesta, pon spin = 0 antes de la pausa y vuelve a ponerlo después.
pivot
Obtiene o establece el punto de pivote (centro de rotación) del sprite, en coordenadas del fotograma.
// Obtener el pivote actual
var p = sprite.pivot
println("Pivote: ", p[0], ", ", p[1])
// Establecer el pivote con una lista
sprite.pivot = [16, 16]
Por defecto, el pivote está en el centro del sprite. x e y son la posición del pivote: con el pivote en [0, 0], son la esquina de arriba a la izquierda.
Dimensiones
width
Obtiene el ancho del sprite en píxeles (solo lectura): el de sus fotogramas.
var ancho = sprite.width
height
Obtiene el alto del sprite en píxeles (solo lectura).
var alto = sprite.height
Estado
active
Obtiene o establece si el sprite está activo. Los sprites inactivos no se pintan, no chocan (hits, contacts, hitsGrid y la física los saltan) y no se animan.
// Desactivar sprite
sprite.active = false
// Activar sprite
sprite.active = true
// Verificar si está activo
if (sprite.active) then
println("El sprite está activo")
end
Una animación sin bucle también lo desactiva al acabar (ver loop).
active = false es para siempreEn una sala con comportamientos, estados, reglas, plantillas, variables o código, el código que genera el editor de escenas saca de la escena, al final de cada fotograma, los sprites con active = false: es lo que hace la acción Destroy. Volver a ponerlo a true no lo devuelve. Para esconder un objeto un rato, apártalo de la vista; para que vuelva a salir, haz una plantilla y crea una copia. Lo cuenta Objetos, del editor de escenas.
Apariencia
flipX
Obtiene o establece si el sprite está volteado horizontalmente. La imagen se da la vuelta dentro de su fotograma, sin cambiar de sitio, y el collider se voltea con ella.
// Voltear horizontalmente
sprite.flipX = true
// Restaurar orientación
sprite.flipX = false
// Consultar estado
if (sprite.flipX) then
println("Sprite volteado en X")
end
Con un collider descentrado, como el de una espada o un morro, la caja pasa al otro lado:
// Fotograma de 32 px de ancho, con la caja en los 8 px de la derecha
espada.collider = [24, 8, 8, 16]
espada.flipX = true
// Ahora la caja está en los 8 px de la izquierda, como si fuera [0, 8, 8, 16]
sprite.collider sigue devolviendo lo que escribiste: el volteo se aplica al comprobar las colisiones (hits, contacts, hitsGrid y la física). El pivote no se voltea. Solo acepta true o false.
Un collider descentrado cambia de lado al voltear. Si el sprite es un cuerpo físico y está pegado a un muro, la física lo separa.
flipY
Obtiene o establece si el sprite está volteado verticalmente. Como con flipX, la imagen se da la vuelta dentro de su fotograma y el collider se voltea con ella.
// Voltear verticalmente
sprite.flipY = true
// Restaurar orientación
sprite.flipY = false
Animación
La animación avanza al pintar el sprite, con draw.sprite o draw.scene: cada vez que lo pintas, mira si ha pasado frameRate desde el último cambio de fotograma. El reloj es el de Display.update(). Un sprite que no se pinta, o que está fuera de la cámara de una escena, no se anima.
frame
Obtiene o establece el fotograma actual de la animación, contando desde 0.
// Obtener el fotograma actual
var f = sprite.frame
// Ir al fotograma 3
sprite.frame = 3
Un valor fuera de 0 a frames - 1 no cambia el fotograma. Los decimales se redondean hacia abajo.
frames
Obtiene el número total de fotogramas (solo lectura).
var total = sprite.frames
println("Total de fotogramas: ", total)
frameRate
Obtiene o establece la velocidad de animación en milisegundos por fotograma. Con 0, el sprite no se anima solo.
// Obtener velocidad actual
var velocidad = sprite.frameRate
// Establecer animación más rápida (50 ms por fotograma = 20 fps)
sprite.frameRate = 50
Un sprite de Sprite.new(bitmap) empieza con 25 (40 fotogramas por segundo). Uno de Sprite.load empieza con 1000 / fps del .json: con "fps": 12, 83. Una copia hecha con Sprite.new(otro) hereda el del original.
loop
Obtiene o establece si la animación se repite en bucle.
// Desactivar bucle (la animación se reproduce una vez y el sprite se desactiva)
sprite.loop = false
// Activar bucle
sprite.loop = true
Con loop = false, al pasar del último fotograma el sprite pasa a active = false: deja de dibujarse y de colisionar, y frame queda igual a frames. Si lo vuelves a activar con active = true, la animación empieza otra vez desde el primer fotograma.
yoyo
Obtiene o establece el modo de animación ping-pong (ida y vuelta).
// Activar animación ping-pong
sprite.yoyo = true
// Desactivar
sprite.yoyo = false
Solo acepta true o false.
dir
Obtiene o establece la dirección de la animación: 1 (adelante) o -1 (atrás). Otro valor no la cambia.
// Reproducir animación hacia atrás
sprite.dir = -1
// Reproducir hacia adelante
sprite.dir = 1
Colisiones
collider
Obtiene o establece los parámetros del collider, en coordenadas del fotograma.
// Obtener datos del collider
var datos = sprite.collider
// Para rectángulo: [x, y, ancho, alto]
// Para círculo: [x, y, radio]
// Establecer collider rectangular
sprite.collider = [0, 0, 32, 32]
// Establecer collider circular
sprite.collider = [16, 16, 16] // centro en (16,16), radio 16
Las coordenadas son las del fotograma sin voltear. Con flipX o flipY el collider se voltea con la imagen (ver flipX). Los decimales se truncan.
Un ancho, un alto o un radio de 0 o negativo no cambia el collider, y no da ningún error: el sprite sigue con el de antes. Comprueba las medidas antes de asignarlas.
colliderType
Obtiene el tipo de collider actual (solo lectura).
var tipo = sprite.colliderType
if (tipo == Sprite.RECTANGLE) then
println("Collider rectangular")
elsif (tipo == Sprite.CIRCLE) then
println("Collider circular")
end
Física
Un sprite es un cuerpo físico cuando su bodyType es DYNAMIC, STATIC o KINEMATIC y está en una escena con la física encendida (scene.physics = true): cada scene.update() lo mueve. Lo cuenta el módulo Scene.
Los cuerpos DYNAMIC no solo se desplazan: también giran al colisionar. Una caja que cae de canto vuelca, una bola rueda por una rampa — también contra un GridMap. La rotación física se suma a spin (la rotación manual) y puede desactivarse por sprite con rotates.
| Propiedad | Tipo | De serie | Qué es |
|---|---|---|---|
bodyType | constante | Sprite.VISUAL | El tipo de cuerpo |
velocity | lista [vx, vy] | [0, 0] | La velocidad, en píxeles por segundo |
vx, vy | decimal | 0 | Cada componente de la velocidad |
force | lista [fx, fy] | [0, 0] | La fuerza de este paso |
mass | decimal | 1 | La masa |
friction | decimal, de 0 a 1 | 0 | El rozamiento |
elasticity | decimal, de 0 a 1 | 0.5 | El rebote |
gravityScale | decimal | 1 | Cuánta gravedad de la escena recibe |
damping | decimal, 0 o más | 0 | El frenado del movimiento |
category | entero, de 0 a 65535 | 1 | Las categorías del cuerpo |
collideWith | entero, de 0 a 65535 | 65535 | Con qué categorías choca |
rotates | sí/no | true | Si los choques lo hacen girar |
angularVel | decimal, grados por segundo | 0 | La velocidad de giro física |
angularDamping | decimal, 0 o más | 0 | El frenado del giro |
bullet | sí/no | false | La colisión continua |
Al asignar cualquiera de ellas a un sprite VISUAL, el valor se guarda, pero el sprite no se mueve hasta que es un cuerpo en una escena con física. Y al pasar un cuerpo a VISUAL, pierde todos estos valores: vuelven a los de serie.
bodyType
Obtiene o establece el tipo de cuerpo físico.
// Convertir a cuerpo dinámico (afectado por física)
sprite.bodyType = Sprite.DYNAMIC
// Convertir a estático (inmóvil pero colisiona)
sprite.bodyType = Sprite.STATIC
// Volver a solo visual (sin física)
sprite.bodyType = Sprite.VISUAL
Un valor fuera de 0–3 no lo cambia. Al pasar a STATIC, su velocidad se pone a 0.
Un KINEMATIC lo mueves tú: con x/y cada fotograma, o con velocity (la escena lo avanza a esa velocidad). Para girarlo, cambia angle o pon angularVel: gira alrededor de su pivote, y los cuerpos que golpea salen con la velocidad del punto que los toca (la punta de una paleta empuja más que la base). Un angle que cambias tú se queda como lo dejas; con angularVel, la escena cambia angle sola.
// Una plataforma que sube 30 píxeles por segundo y lleva encima lo que tenga
plataforma.bodyType = Sprite.KINEMATIC
plataforma.vy = -30
// Una paleta que gira 90 grados por segundo
paleta.bodyType = Sprite.KINEMATIC
paleta.angularVel = 90
velocity
Obtiene o establece la velocidad como lista [vx, vy], en píxeles por segundo.
// Obtener velocidad actual
var vel = sprite.velocity
println("Velocidad: ", vel[0], ", ", vel[1])
// Establecer velocidad
sprite.velocity = [100, -50] // 100 px/s derecha, 50 px/s arriba
Dos sprites que se muevan muy deprisa pueden atravesarse sin llegar a chocar, porque entre un fotograma y el siguiente saltan de un lado al otro del otro cuerpo.
Contra el mapa de tiles esto no ocurre: hitsGrid comprueba todo el recorrido del sprite desde su posición anterior (prevX y prevY), siempre que esta esté al día.
vx y vy
Obtienen o establecen cada componente de la velocidad, en píxeles por segundo: vx en horizontal (positivo, a la derecha) y vy en vertical (positivo, hacia abajo).
jugador.vx = 120 // a la derecha, sin tocar vy
if (jugador.onGround()) then
jugador.vy = -300 // salto
end
Son lo mismo que velocity[0] y velocity[1], sin crear una lista.
force
Obtiene o establece la fuerza pendiente como lista [fx, fy]. Es una fuerza de un solo paso: en el siguiente scene.update() el motor la integra (Δv = fuerza / masa · dt) y la deja a cero. Para un empuje mantenido (motor, viento, propulsor) hay que reasignarla en cada fotograma — o llamar a applyForce() dentro del bucle, que es equivalente (la asignación machaca la fuerza acumulada del fotograma; applyForce() suma sobre ella).
// Empuje mantenido: reasignar en cada fotograma, antes de scene.update()
while (true)
cohete.force = [0, -300] // empuje hacia arriba
scene.update()
Display.update()
end
accelerationsprite.acceleration es otro nombre de force, de versiones anteriores: usa force. El nombre antiguo era engañoso: el valor se divide por la masa (con mass = 4, force = [200, 0] produce una aceleración efectiva de 50 px/s², no 200) y se consume tras cada paso — después de un scene.update() la propiedad se lee como [0, 0].
friction
Obtiene o establece la fricción del sprite, de 0 a 1. Un valor fuera se queda en el límite.
// Obtener fricción
var f = sprite.friction
// Establecer fricción
sprite.friction = 0.5
La fricción también gobierna el giro por colisión: es lo que hace que una bola ruede en vez de deslizar. Un sprite con friction = 0 (el valor por defecto) es un cojinete perfecto — nada frena su rotación una vez empieza a girar.
mass
Obtiene o establece la masa del sprite. Con 0 o menos, vale 1.
// Sprite pesado
sprite.mass = 10
// Sprite ligero
sprite.mass = 0.5
elasticity
Obtiene o establece la elasticidad (rebote) del sprite, de 0 a 1. De serie, 0.5.
// Muy elástico (rebota mucho)
sprite.elasticity = 0.9
// Sin rebote
sprite.elasticity = 0
gravityScale
Obtiene o establece cuánta gravedad de la escena recibe este cuerpo: la gravedad de la escena por este número. 1 (de serie) es la normal, 0 hace que flote y un número negativo, que caiga hacia arriba.
globo.gravityScale = 0 // flota
pluma.gravityScale = 0.2 // cae despacio
burbuja.gravityScale = -0.5 // sube
damping
Obtiene o establece cuánto se frena el movimiento del cuerpo, también en el aire y sin tocar nada: en cada paso de física, la velocidad se divide por 1 + dt × damping. 0 (de serie) no frena. Un valor negativo cuenta como 0.
// Un disco que se para solo en un par de segundos
disco.vx = 100
disco.damping = 2
Para frenar el giro está angularDamping.
category y collideWith
Deciden qué cuerpos chocan entre sí. Cada uno es un número de 16 bits, de 0 a 65535: cada bit es una categoría.
category: las categorías del cuerpo. De serie,1.collideWith: con qué categorías choca. De serie,65535: con todas.
Dos cuerpos chocan si cada uno tiene en collideWith alguna categoría del otro. Un valor fuera de 0–65535, o que no es un entero, no las cambia.
// Categorías del juego: 1 el escenario, 2 el jugador, 4 sus balas
suelo.category = 1
jugador.category = 2
bala.category = 4
bala.collideWith = 1 // las balas chocan con el escenario, no con el jugador
jugador.collideWith = 1 | 2 // el jugador tampoco choca con sus balas
Solo cuentan en la física de la escena: hits, contacts, hitsGrid y grid.hits no las miran. Los choques que filtran tampoco llaman a onCollision.
rotates
Obtiene o establece si las colisiones pueden hacer girar al cuerpo (solo tiene efecto en cuerpos DYNAMIC). Activado por defecto.
// Personaje de plataformas: siempre de pie
jugador.rotates = false
// Caja que vuelca y rueda (comportamiento por defecto)
caja.rotates = true
Con rotates = false el sprite recupera el comportamiento clásico: la física mueve el cuerpo pero nunca cambia su ángulo. Es lo recomendado para héroes de plataformas y en general para cualquier sprite que deba mantenerse de pie.
Un collider de muy pocos píxeles que además pueda girar resulta inestable al apoyarse sobre otros cuerpos. Si te ocurre, desactiva el giro con rotates = false.
angularVel
Obtiene o establece la velocidad angular física en grados por segundo: el giro que las colisiones imprimen al cuerpo. Es independiente de spin (ambas rotaciones se suman). Existe un tope de seguridad de 720 grados por segundo.
// Leer la velocidad de giro actual
var w = sprite.angularVel
// Lanzar el sprite girando
sprite.angularVel = 180
// Parar el giro (por ejemplo, al reciclar un cuerpo)
sprite.angularVel = 0
angularDamping
Obtiene o establece la amortiguación angular del cuerpo (0 por defecto, sin amortiguación). Frena el giro gradualmente, incluso sin contacto con otras superficies.
La forma física de frenar un giro es la fricción de contacto (friction), pero cuando un cuerpo rueda sin deslizar la fricción ya no puede detenerlo: para eso está angularDamping.
// Bola que deja de girar poco a poco
bola.angularDamping = 0.5
// Casi sin pérdida de giro
peonza.angularDamping = 0.05
bullet
Obtiene o establece la colisión continua (CCD) del cuerpo: desactivada por defecto. Con bullet = true el motor no comprueba solo dónde está el cuerpo al final de cada paso, sino que barre el trayecto completo que ha recorrido durante el paso, de modo que un proyectil rápido no puede atravesar a otro cuerpo por quedar el impacto justo entre dos pasos.
// Bala de un juego de disparos: a 6000 px/s recorre 100 px por paso, mucho más
// que el grosor de una pared
var bala = Sprite.new(0, 80, plantillaBala)
bala.bodyType = Sprite.DYNAMIC
bala.bullet = true
bala.velocity = [6000, 0]
Sin bullet, un cuerpo que avanza más que su propio tamaño en un solo paso puede aparecer al otro lado de un obstáculo delgado sin haberlo tocado nunca. Es el clásico tunneling, y solo se nota con cuerpos muy rápidos o colliders muy finos: para velocidades normales la detección discreta basta y sale más barata.
Actívalo únicamente en los sprites que lo necesiten (balas, discos de pinball, cuerpos que caen desde muy alto). No es un interruptor global de calidad: cada cuerpo marcado paga un barrido contra el resto de cuerpos en cada paso, así que marcar la escena entera es caro y no aporta nada.
Solo surte efecto en cuerpos DYNAMIC. En STATIC, KINEMATIC o VISUAL el valor se guarda y se ignora, de modo que puedes asignar bullet antes o después de bodyType sin preocuparte del orden.
// Las dos formas son equivalentes
a.bodyType = Sprite.DYNAMIC
a.bullet = true
b.bullet = true
b.bodyType = Sprite.DYNAMIC
El auto-dormido de la escena (scene.sleeping) excluye a los cuerpos marcados como bala. Si marcas un sprite permanente del escenario, lo estás sacando del ahorro de CPU para siempre. Para proyectiles, que viven poco, esto no importa.
La colisión contra un GridMap ya usa barrido para todos los cuerpos, marcados o no: bullet afecta únicamente a la colisión cuerpo contra cuerpo.
El motor resuelve como mucho dos impactos por paso y por bala. A partir del segundo, la bala se queda en la última posición segura y continúa en el paso siguiente: nunca atraviesa nada, pero puede parecer que se frena un instante en una esquina muy cerrada.
Métodos del sprite
Los métodos se invocan siempre con paréntesis:
sprite.nextFrame()
var resultado = sprite.hits(otro)
| Método | Qué hace | Devuelve |
|---|---|---|
hits(otro) | Con qué sprites choca | Una lista de sprites, o nil |
contacts(otro) | Como hits, con la dirección y la profundidad del choque | Una lista de contactos, o nil |
hitsGrid(mapa [, tiles]) | Con qué celdas de un GridMap choca | Una lista de celdas, o nil |
onGround() | Si está apoyado en algo | true o false |
onCollision(funcion [, minImpulso]) | Avisa cuando empieza a chocar con otro cuerpo | true |
applyForce(fx, fy) | Empuja durante un paso de física | true |
applyImpulse(ix, iy) | Cambia la velocidad de golpe | true |
applyTorque(t) | Hace girar durante un paso de física | true |
applyForceAt(fx, fy, px, py) | Empuja desde un punto: desplaza y hace girar | true |
nextFrame() | Pasa al fotograma siguiente | nil |
addImage(bitmap) | Añade un fotograma | true |
mem() | La memoria de sus fotogramas | Un entero, en bytes |
hits
Comprueba colisiones del sprite contra otro sprite, una lista o un array. Devuelve una lista con los sprites que colisionan, o nil si no hay colisión.
// Contra un solo sprite
var resultado = jugador.hits(enemigo)
if (resultado != nil) then
println("¡Colisión detectada!")
end
// Contra una lista de sprites
var enemigos = [enemigo1, enemigo2, enemigo3]
var colisiones = jugador.hits(enemigos)
if (colisiones != nil) then
foreach (var e in colisiones)
e.active = false
end
end
El sistema de colisiones utiliza una fase amplia (broadphase) con círculos envolventes antes de la detección precisa (narrowphase). Soporta:
- Rectángulo vs Rectángulo (incluyendo rotación con SAT)
- Círculo vs Círculo
- Círculo vs Rectángulo
Es geometría pura: no necesita escena ni física, y no mira category ni collideWith. Se salta a sí mismo, a los inactivos y lo que no es un sprite; si el sprite que pregunta está inactivo, devuelve nil. Para comprobar contra muchos sprites, pásale una lista: es más rápido que llamarlo con cada uno. Con algo que no es un sprite, una lista o un array, lanza ERR_TYPE_MISMATCH.
contacts
Como hits, pero dice además por dónde y cuánto se solapan. Devuelve una lista con un contacto por cada sprite que toca, o nil si no toca ninguno. Cada contacto es una lista [otro, nx, ny, depth]:
| Posición | Contenido |
|---|---|
[0] | El otro sprite |
[1], [2] | nx, ny: normal unitaria en la dirección en la que hay que mover este sprite para salir del otro |
[3] | depth: profundidad del solape en píxeles (siempre mayor que 0) |
// Sacar al jugador de los obstáculos que está tocando
var contactos = jugador.contacts(obstaculos)
if (contactos != nil) then
foreach (var c in contactos)
var nx = c[1], ny = c[2], depth = c[3]
jugador.x += nx * depth
jugador.y += ny * depth
end
end
Es un solape puro con la posición, el ángulo y el collider actuales de los dos sprites: no necesita escena ni física, así que funciona con cualquier bodyType, incluido VISUAL, y con rectángulos y círculos girados. Sirve para los juegos que resuelven sus choques a mano, como un juego de coches arcade con su propia respuesta al impacto.
Acepta lo mismo que hits (un sprite, una lista o un array), se salta a sí mismo, lo que no es un sprite y los sprites inactivos, y devuelve los contactos en el orden de la lista. Un contacto justo en el borde (solape 0) no cuenta, igual que en hits.
La normal de contacts sigue la misma convención que hitsGrid: apunta hacia donde hay que mover este sprite para salir. La de onCollision va al revés, de este sprite hacia el otro.
hitsGrid
Comprueba colisiones del sprite contra un GridMap. Devuelve una lista de resultados [tileX, tileY, tileValue, normalX, normalY] por cada tile que colisiona, o nil si no hay colisión.
Es una consulta: no mueve el sprite, y funciona con cualquier bodyType, sin escena. Necesita:
- El tamaño del tile en el mapa (
mapa.tSize(ancho, alto), o el que trae un.gmap). Sin él, lanzaERR_INVALID_VALUE. - Qué celdas chocan: sin segundo argumento, las de
mapa.solidsymapa.platforms; si el mapa no tiene ninguna, devuelvenil. - La posición anterior al día (
prevX,prevY): ver «Recorrido del fotograma», abajo.
tileX y tileY son la columna y la fila de la celda, tileValue su valor, y normalX, normalY, hacia dónde hay que mover el sprite para salir (en un suelo, 0, -1). Un sprite inactivo devuelve nil.
Sin segundo argumento (tiles sólidos y plataformas del GridMap)
var colisiones = jugador.hitsGrid(mapa)
if (colisiones != nil) then
foreach (var info in colisiones)
println("Tile en (", info[0], ",", info[1], ") valor=", info[2])
println("Normal: ", info[3], ", ", info[4])
end
end
Con tiles personalizados
// Comprobar contra un tile específico (por valor)
var colisiones = jugador.hitsGrid(mapa, 5)
// Comprobar contra una lista de valores de tile
colisiones = jugador.hitsGrid(mapa, [1, 2, 5])
Con un segundo argumento no hay plataformas de un solo sentido: devuelve todas las celdas que toca con esos valores. Un valor fuera de 0–255 lanza ERR_INVALID_VALUE.
hitsGrid no mira solo dónde está el sprite: comprueba todo el camino desde su posición anterior (prevX, prevY) hasta la actual, para que un sprite rápido no atraviese una pared fina. Si el sprite está en una escena que se actualiza en cada fotograma, eso es automático. Sin escena, o justo después de colocar o teletransportar el sprite, pon tú el previo; si no, hitsGrid devuelve también los tiles de todo el camino desde donde estaba.
Cuando se usa sin segundo argumento, hitsGrid maneja automáticamente las plataformas definidas en el GridMap como plataformas de un solo sentido: solo registra colisión si el sprite venía desde arriba (su borde inferior previo estaba por encima del tile).
hitsGrid devuelve como mucho 8 tiles. Es de sobra para sprites de tamaño normal, pero un collider que cubra muchas casillas a la vez no recibirá todas.
Para que el mapa saque al sprite de las paredes, usa grid.hits.
onGround
Devuelve true si el cuerpo está apoyado en algo: en otro cuerpo o en el mapa.
if (jugador.onGround()) then
println("En el suelo")
// Permitir saltar
if (GPIO.read(15) == HIGH) then
jugador.vy = -300
end
end
Solo funciona con un cuerpo DYNAMIC de una escena con física:
- cada
scene.update()lo vuelve a calcular, según toque otro cuerpo; grid.hits(sprite)lo pone atruesi el mapa lo sostiene. Pregúntalo después degrid.hits.
Un sprite sin física devuelve siempre false. Sin una escena con física, nada lo vuelve a poner a false: una vez que grid.hits lo pone a true, se queda así.
onCollision
Registra un callback que el motor de físicas ejecuta automáticamente cuando el sprite empieza a tocar otro sprite. Es la forma reactiva de tratar los impactos: en vez de consultar hits() en cada fotograma, el motor te avisa con la fuerza del golpe.
sprite.onCollision(callback)
sprite.onCollision(callback, minImpulso)
sprite.onCollision(nil) // desactivar
| Parámetro | Tipo | Descripción |
|---|---|---|
callback | función/cadena | Referencia directa (miFuncion), método de un objeto (objeto.metodo) o nombre en una cadena. nil desactiva el callback |
minImpulso | entero/decimal | (Opcional) Impulso mínimo del contacto para disparar. Por defecto 0 (todo contacto nuevo dispara) |
El callback recibe cinco parámetros:
func alChocar(yo, otro, impulso, nx, ny)
// yo: el sprite que registró el callback
// otro: el sprite contra el que ha chocado
// impulso: fuerza del golpe (impulso normal acumulado del solver)
// nx, ny: normal del contacto, orientada de 'yo' hacia 'otro'
end
Requiere una escena con físicas (scene.physics = true) y que ambos sprites participen en la simulación (DYNAMIC, STATIC o KINEMATIC). Los eventos se disparan solo al empezar el contacto: una caja apoyada en el suelo no genera eventos mientras repose, y si los cuerpos se separan y vuelven a chocar (un rebote) se genera un evento nuevo. Si ambos sprites tienen callback, cada uno recibe el suyo. Con un callback que no es una función, un método ni una cadena, lanza ERR_TYPE_MISMATCH.
Ejemplo: proyectil que destruye al superar una fuerza
use Bitmap, Sprite, Scene
class Cerdo
def init(spr)
this.spr = spr
this.vivo = true
// Solo los golpes fuertes hacen daño
spr.onCollision(this.recibir, 300)
end
def recibir(yo, otro, impulso, nx, ny)
this.vivo = false
yo.active = false
println("¡Impacto de fuerza ", impulso, "!")
end
end
var escena = Scene.new()
escena.physics = true
escena.gravity = 400
var tpl = Bitmap.load("Images/caja.bmp")
var piedra = Sprite.new(20, 100, tpl)
piedra.bodyType = Sprite.DYNAMIC
piedra.mass = 4
escena.addSprite(piedra)
var objetivo = Sprite.new(200, 100, tpl)
objetivo.bodyType = Sprite.DYNAMIC
escena.addSprite(objetivo)
var cerdo = Cerdo(objetivo) // registra objetivo.onCollision(cerdo.recibir, 300)
piedra.vx = 300 // ¡lanzar!
Con un método como callback (this.recibir), el sprite mantiene viva la instancia mientras el callback esté registrado.
Para reacciones sencillas, una lambda inline evita definir función aparte:
caja.onCollision(fn(yo, otro, impulso, nx, ny):
println("¡golpe de ", impulso, "!")
end, 100)
Al asentarse tras una caída, un cuerpo puede generar varios contactos consecutivos de poca fuerza (micro-rebotes del solver). Con un minImpulso acorde al juego, solo los golpes que importan disparan el callback.
El callback debe aceptar exactamente 5 parámetros (yo, otro, impulso, nx, ny). Si la firma no coincide, el evento se descarta de forma silenciosa. Las colisiones contra tiles de un GridMap no generan eventos (usa hitsGrid para eso); si el «suelo» debe avisar, créalo como sprite STATIC.
applyForce
Empuja el cuerpo con una fuerza que se consume en el siguiente paso de física (Δv = F / masa · dt) y después se descarta. Equivale a sumar sobre la propiedad force. Para un empuje mantenido, llámalo en cada fotograma del bucle.
// Propulsor: empuje continuo mientras se mantenga pulsado
while (true)
if (Input.pressed(ACT_THRUST)) then
nave.applyForce(0, -500)
end
scene.update()
Display.update()
end
| Parámetro | Tipo | Descripción |
|---|---|---|
fx | número | Componente X de la fuerza |
fy | número | Componente Y de la fuerza |
Requiere que el sprite participe en la física (por ejemplo, bodyType = Sprite.DYNAMIC). En un sprite sin física, los cuatro métodos apply… devuelven un valor de error y no hacen nada.
applyImpulse
Aplica un impulso instantáneo: la velocidad cambia de inmediato (Δv = impulso / masa), sin esperar al paso de física. Es la herramienta natural para saltos, disparos y explosiones.
// Salto: cambio de velocidad inmediato, proporcional a la masa
jugador.applyImpulse(0, -300 * jugador.mass)
// Empujón lateral de una explosión
caja.applyImpulse(150, -80)
| Parámetro | Tipo | Descripción |
|---|---|---|
ix | número | Componente X del impulso |
iy | número | Componente Y del impulso |
Requiere que el sprite participe en la física (por ejemplo, bodyType = Sprite.DYNAMIC).
applyTorque
Aplica un par (torque) que hace girar al cuerpo luchando contra su inercia. Como applyForce, se consume en el siguiente paso de física y se descarta: para un giro sostenido, llámalo en cada fotograma. La aceleración angular resultante es t / inercia en grados/s².
// Peonza: acelerar el giro mientras se mantiene pulsado
while (true)
if (Input.pressed(ACT_SPIN)) then
peonza.applyTorque(4000)
end
scene.update()
Display.update()
end
| Parámetro | Tipo | Descripción |
|---|---|---|
t | número | Par aplicado. Positivo = sentido horario (con Y hacia abajo) |
La inercia depende de la forma y la masa del collider: m·(ancho² + alto²)/12 para rectángulos y m·radio²/2 para círculos. Un cuerpo con rotates = false ignora el torque, y el giro resultante respeta el tope de seguridad de ±720 °/s y el angularDamping.
applyForceAt
Aplica una fuerza en un punto concreto del mundo: además del empuje lineal (idéntico a applyForce), genera el par r × F alrededor del centro del collider. Es el empujón excéntrico clásico: golpear una caja cerca de una esquina la desplaza y la hace girar.
// Golpe excéntrico: empuja y voltea la caja
// (con el pivote de serie, en el centro, el punto está 8 px por debajo
// del centro del collider de 16x16)
caja.applyForceAt(300, 0, caja.x, caja.y + 8)
| Parámetro | Tipo | Descripción |
|---|---|---|
fx | número | Componente X de la fuerza |
fy | número | Componente Y de la fuerza |
px | número | Coordenada X del punto de aplicación (mundo) |
py | número | Coordenada Y del punto de aplicación (mundo) |
Si el punto coincide con el centro del collider el resultado es una fuerza pura (sin giro), y cuanto mayor es el brazo de palanca, más gira. Como toda fuerza, se consume en el siguiente paso: para un empuje sostenido, llámalo en cada fotograma.
nextFrame
Avanza manualmente al siguiente fotograma.
sprite.nextFrame()
En el último fotograma, vuelve al primero.
addImage
Añade un bitmap como nuevo fotograma al sprite.
var sprite = Sprite.new(Bitmap.load("Images/frame0.bmp"))
sprite.addImage(Bitmap.load("Images/frame1.bmp"))
sprite.addImage(Bitmap.load("Images/frame2.bmp"))
println("Total de fotogramas: ", sprite.frames) // 3
El fotograma nuevo cambia width y height a sus medidas. Si el collider y el pivote eran los de serie (el fotograma entero y su centro), se ajustan al tamaño nuevo; si les habías puesto otros, se quedan.
addImage() no está disponible en un sprite cargado como hoja de fotogramas (ni en sus copias): da error. Para ir añadiendo fotogramas, parte de un sprite creado con Sprite.new(bitmap).
mem
Devuelve el uso de memoria en bytes de todos los fotogramas del sprite.
var memoria = sprite.mem()
println("Memoria usada: ", memoria, " bytes")
Las copias hechas con Sprite.new(otro) comparten los fotogramas: mem() cuenta en cada una lo que ocupan, aunque estén una sola vez en memoria.
Ejemplo completo
Una nave rebota por la pantalla girando y se lleva por delante tres rocas: cuando toca una, la desactiva. Las rocas son copias que comparten la imagen de la nave.
use Display, Bitmap, Sprite
Display.open(320, 240)
Display.fps = 30
var draw = Display.draw
var imagen = Bitmap.load("Images/nave.bmp") // una imagen de tu proyecto
var nave = Sprite.new(160, 120, imagen)
nave.spin = 90 // 90 grados por segundo
// Tres rocas: copias que comparten la imagen
var rocas = []
for (var i = 0; i < 3; i++)
rocas.append(Sprite.new(60 + i * 100, 40, imagen))
end
var vx = 120.0 // píxeles por segundo
var vy = 80.0
var mitadX = nave.width / 2 // x/y es el pivote: el centro
var mitadY = nave.height / 2
for (var f = 0; f < 600; f++) // 20 segundos
nave.x = nave.x + vx * Display.dt
nave.y = nave.y + vy * Display.dt
if (nave.x < mitadX) then
nave.x = mitadX
vx = abs(vx)
elsif (nave.x > Display.width - mitadX) then
nave.x = Display.width - mitadX
vx = 0 - abs(vx)
end
if (nave.y < mitadY) then
nave.y = mitadY
vy = abs(vy)
elsif (nave.y > Display.height - mitadY) then
nave.y = Display.height - mitadY
vy = 0 - abs(vy)
end
nave.flipX = (vx < 0)
var tocadas = nave.hits(rocas) // nil si no toca ninguna
if (tocadas != nil) then
foreach (var r in tocadas)
r.active = false // deja de pintarse y de chocar
end
end
draw.sprite(rocas)
draw.sprite(nave)
Display.update()
end
Display.close()
Trampas y problemas conocidos
- En una sala del editor de escenas,
active = falsesaca el sprite de la escena para siempre (ver active). spingira al pintar, y en cada llamada: pinta cada sprite una sola vez por fotograma (ver spin).spingira de golpe tras una pausa (ver spin).- Un collider de ancho, alto o radio 0 se ignora sin error (ver collider).
- Al pasar un cuerpo a
VISUAL, pierde su física: masa, velocidad, rozamiento y demás vuelven a los de serie (ver Física). onGround()necesita una escena con física; sin ella, una vez atrueno vuelve afalse(ver onGround).hitsycontactsno mirancategorynicollideWith: esas dos solo filtran la física de la escena.- Rendimiento: los sprites
VISUALno entran en los cálculos de física; comprueba los choques con una lista (hits(lista)) mejor que uno a uno, y comparte las imágenes haciendo copias conSprite.new(otro)en vez de cargar el mismo fichero varias veces.
Todos los problemas conocidos de JARU, juntos, están en Problemas conocidos.