Saltar al contenido principal

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.

Captura del módulo Sprite de JARU para cargar sprites, animar fotogramas y detectar colisiones

use Display
use Bitmap
use Sprite

Funciones​

FunciónQué haceDevuelve
Sprite.load(ruta)Carga un sprite: su .json y sus imágenesUn sprite
Sprite.new(imagen) / Sprite.new(x, y, imagen)Crea un sprite con un bitmap, o una copia de otro spriteUn 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
}
CampoDescripción
frameWidthAncho de cada fotograma en píxeles
frameHeightAlto de cada fotograma en píxeles
totalFramesNúmero total de fotogramas
fpsFotogramas por segundo de la animación: el sprite empieza con frameRate = 1000 / fps milisegundos. Con 0, o sin el campo, no se anima solo
suffixDígitos en el sufijo numérico (ej: 2 = _00, _01...)
backgroundcolorEl 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.

ErrorCuándo
ERR_FILE_NOT_FOUNDNo 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_MEMORYNo hay memoria para cargar una de las imágenes.
ERR_INVALID_VALUELa imagen de la hoja no cuadra con el .json (ver abajo).
ERR_INVALID_ARG_NUMBERNo 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.

CampoSi faltaDescripción
pivotX pivotYcentro del fotogramaPunto de rotación y posición, en coordenadas del fotograma (equivale a sprite.pivot)
loop10 para que la animación se reproduzca una sola vez; al acabar, el sprite pasa a active = false (equivale a sprite.loop)
yoyo01 para que la animación vaya y vuelva (equivale a sprite.yoyo)
colliderTypesin collider1 rectángulo, 2 círculo. Sin este campo el collider es el fotograma entero
colliderX colliderY0Desplazamiento del collider dentro del fotograma. Puede ser negativo
colliderW colliderHfotograma enteroTamaño del collider rectangular
colliderRmitad del lado menorRadio del collider circular
sheet01: 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
}
El pivote se paga una vez

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.

ErrorCuándo
ERR_INVALID_ARG_NUMBERNo le pasas uno o tres argumentos.
ERR_TYPE_MISMATCHEl último no es un bitmap ni un sprite, o x o y no son números.
ERR_INVALID_VALUEEl 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)
Compartir gráficos

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​

ConstanteValorDescripción
Sprite.VISUAL0Sin física, solo visual. El de serie
Sprite.DYNAMIC1La escena lo mueve: gravedad, velocidad, choques y giros
Sprite.STATIC2No se mueve, pero los dinámicos chocan con él: suelos y paredes
Sprite.KINEMATIC3Lo 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​

ConstanteValorDescripción
Sprite.RECTANGLE1Collider rectangular
Sprite.CIRCLE2Collider 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
PropiedadTipoDe serieLectura / escritura
x, ydecimal0, o los de Sprite.new(x, y, …)Las dos
poslista [x, y]—Solo escritura
prevX, prevYdecimalLos mismos que x e yLas dos
angledecimal, grados0Las dos
spindecimal, grados por segundo0Las dos
pivotlista [x, y]El centro del fotogramaLas dos
width, heightenteroEl tamaño del fotogramaSolo lectura
activesí/notrueLas dos
flipX, flipYsí/nofalseLas dos
frameentero0Las dos
framesenteroEl número de fotogramasSolo lectura
frameRateentero, milisegundos25 con Sprite.new; con Sprite.load, 1000 / fpsLas dos
loopsí/notrueLas dos
yoyosí/nofalseLas dos
dir1 o -11Las dos
colliderlistaEl fotograma enteroLas dos
colliderTypeenteroSprite.RECTANGLESolo 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.

Tras una pausa, gira de golpe

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).

En una sala del editor de escenas, active = false es para siempre

En 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.

Cuerpos físicos

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 collider sin tamaño se ignora sin avisar

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.

PropiedadTipoDe serieQué es
bodyTypeconstanteSprite.VISUALEl tipo de cuerpo
velocitylista [vx, vy][0, 0]La velocidad, en píxeles por segundo
vx, vydecimal0Cada componente de la velocidad
forcelista [fx, fy][0, 0]La fuerza de este paso
massdecimal1La masa
frictiondecimal, de 0 a 10El rozamiento
elasticitydecimal, de 0 a 10.5El rebote
gravityScaledecimal1Cuánta gravedad de la escena recibe
dampingdecimal, 0 o más0El frenado del movimiento
categoryentero, de 0 a 655351Las categorías del cuerpo
collideWithentero, de 0 a 6553565535Con qué categorías choca
rotatessí/notrueSi los choques lo hacen girar
angularVeldecimal, grados por segundo0La velocidad de giro física
angularDampingdecimal, 0 o más0El frenado del giro
bulletsí/nofalseLa 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
Sprites muy rápidos

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
Alias acceleration

sprite.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.

Colliders diminutos

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.

Cuándo activarlo

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
Un cuerpo bala nunca se duerme

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étodoQué haceDevuelve
hits(otro)Con qué sprites chocaUna lista de sprites, o nil
contacts(otro)Como hits, con la dirección y la profundidad del choqueUna lista de contactos, o nil
hitsGrid(mapa [, tiles])Con qué celdas de un GridMap chocaUna lista de celdas, o nil
onGround()Si está apoyado en algotrue o false
onCollision(funcion [, minImpulso])Avisa cuando empieza a chocar con otro cuerpotrue
applyForce(fx, fy)Empuja durante un paso de físicatrue
applyImpulse(ix, iy)Cambia la velocidad de golpetrue
applyTorque(t)Hace girar durante un paso de físicatrue
applyForceAt(fx, fy, px, py)Empuja desde un punto: desplaza y hace girartrue
nextFrame()Pasa al fotograma siguientenil
addImage(bitmap)Añade un fotogramatrue
mem()La memoria de sus fotogramasUn 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ónContenido
[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.

Sentido de la normal

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, lanza ERR_INVALID_VALUE.
  • Qué celdas chocan: sin segundo argumento, las de mapa.solids y mapa.platforms; si el mapa no tiene ninguna, devuelve nil.
  • 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.

Recorrido del fotograma

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.

Plataformas de un solo sentido

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).

Máximo 8 tiles por llamada

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 a true si el mapa lo sostiene. Pregúntalo después de grid.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ámetroTipoDescripción
callbackfunción/cadenaReferencia directa (miFuncion), método de un objeto (objeto.metodo) o nombre en una cadena. nil desactiva el callback
minImpulsoentero/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)
Filtrar el ruido con minImpulso

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.

Firma exacta

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ámetroTipoDescripción
fxnúmeroComponente X de la fuerza
fynúmeroComponente 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ámetroTipoDescripción
ixnúmeroComponente X del impulso
iynúmeroComponente 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ámetroTipoDescripción
tnúmeroPar 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ámetroTipoDescripción
fxnúmeroComponente X de la fuerza
fynúmeroComponente Y de la fuerza
pxnúmeroCoordenada X del punto de aplicación (mundo)
pynúmeroCoordenada 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.

Sprites de hoja

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 = false saca el sprite de la escena para siempre (ver active).
  • spin gira al pintar, y en cada llamada: pinta cada sprite una sola vez por fotograma (ver spin).
  • spin gira 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 a true no vuelve a false (ver onGround).
  • hits y contacts no miran category ni collideWith: esas dos solo filtran la física de la escena.
  • Rendimiento: los sprites VISUAL no 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 con Sprite.new(otro) en vez de cargar el mismo fichero varias veces.

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