Skip to main content

Sprite module

A sprite is an image that moves around the screen: it has a position, a rotation and one or more frames that animate by themselves. It knows what it hits, thanks to its collider, and it can be a physics body that a scene moves with gravity, velocity and bounces. It is drawn with draw.sprite or with draw.scene.

JARU Sprite module screenshot for loading sprites, animating frames, and detecting collisions

use Display
use Bitmap
use Sprite

Functions​

FunctionWhat it doesReturns
Sprite.load(path)Loads a sprite: its .json and its imagesA sprite
Sprite.new(image) / Sprite.new(x, y, image)Creates a sprite from a bitmap, or a copy of another spriteA sprite

load​

The load(path) function loads a sprite from a JSON definition file along with its associated BMP images. It drops the extension of the path and reads the .json with the same name: Sprite.load("Sprites/character.spr") reads Sprites/character.json.

var player = Sprite.load("Sprites/character.spr")

When you build, the IDE exports each sprite of the sprite editor this way: a .json and its image. The JSON file must have the following structure:

{
"frameWidth": 32,
"frameHeight": 32,
"totalFrames": 8,
"fps": 12,
"suffix": 2,
"backgroundcolor": 0
}
FieldDescription
frameWidthWidth of each frame in pixels
frameHeightHeight of each frame in pixels
totalFramesTotal number of frames
fpsAnimation frames per second: the sprite starts with frameRate = 1000 / fps milliseconds. With 0, or without the field, it does not animate by itself
suffixDigits in the numeric suffix (e.g. 2 = _00, _01...)
backgroundcolorThe transparent color, 0xRRGGBB written in decimal (optional; without it, black)

Image files must follow the naming pattern: basename_00.bmp, basename_01.bmp, etc. All the fields are integers: one that is not is ignored.

ErrorWhen
ERR_FILE_NOT_FOUNDThe .json is not there or cannot be read as JSON, or the image of a frame or of the sheet is missing. The message gives the file.
ERR_OUT_OF_MEMORYThere's no memory to load one of the images.
ERR_INVALID_VALUEThe sheet image does not match the .json (see below).
ERR_INVALID_ARG_NUMBERYou do not pass it a string.

Frame sheet​

With "sheet": 1, all frames go in a single BMP, basename.bmp, one below the other: frame n starts at row n × frameHeight. The image is frameWidth wide and at least frameHeight × totalFrames tall; if it doesn't match the .json, Sprite.load raises an error.

{
"frameWidth": 32, "frameHeight": 32, "totalFrames": 8,
"fps": 12, "suffix": 2, "sheet": 1,
"backgroundcolor": 0
}

This is how the IDE exports sprites on Build: one file and one image per sprite instead of one per frame. On a board it loads much faster, uses less memory and less flash, and draws just as fast. A sprite with more than 255 frames is exported with separate frames, as before.

Resources with separate frames (no "sheet") still load the same. A Build with sheets needs a VM that knows them: if a board has an older firmware, flash the VM firmware first (Flash Firmware).

Optional resource fields​

The IDE's sprite editor can also write these fields. All of them are optional: when one is missing, the sprite behaves exactly as it always did, so a resource created before they existed keeps working without changes.

All of them are integers; the yes/no ones travel as 0 or 1.

FieldIf missingDescription
pivotX pivotYframe centerRotation and position point, in frame coordinates (same as sprite.pivot)
loop10 to play the animation only once; when it ends, the sprite becomes active = false (same as sprite.loop)
yoyo01 to make the animation go back and forth (same as sprite.yoyo)
colliderTypeno collider1 rectangle, 2 circle. Without this field the collider is the whole frame
colliderX colliderY0Offset of the collider inside the frame. Can be negative
colliderW colliderHwhole frameSize of the rectangular collider
colliderRhalf the shorter sideRadius of the circular collider
sheet01: all frames in a single basename.bmp (see Frame sheet)
{
"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
}
The pivot is set once

Defining the pivot in the resource saves you setting it by hand every time you create the sprite. Remember that a sprite's x/y is its pivot, not its top-left corner: by default, the center of the frame.

new​

The new() function creates a new sprite from a bitmap or by copying the frames of another existing sprite. With three arguments, the first two are its x, y position.

ErrorWhen
ERR_INVALID_ARG_NUMBERYou do not pass it one or three arguments.
ERR_TYPE_MISMATCHThe last one is neither a bitmap nor a sprite, or x or y are not numbers.
ERR_INVALID_VALUEThe sprite you copy has no frames.

Create from bitmap​

var image = Bitmap.load("Images/ship.bmp")

// Create at position (0, 0)
var ship = Sprite.new(image)

// Create at a specific position
var ship2 = Sprite.new(100, 50, image)

The sprite has one frame, the image, and its collider is the whole image, with the pivot at the center. It starts with frameRate = 25.

Clone from another sprite​

// Create original sprite
var enemy = Sprite.load("Sprites/enemy.spr")

// Create a copy at (0, 0) that shares the same graphics
var enemy2 = Sprite.new(enemy)

// Create copies at specific positions
var enemy3 = Sprite.new(200, 100, enemy)
var enemy4 = Sprite.new(300, 100, enemy)
Sharing graphics

When you create a sprite from another sprite, the frames are shared in memory. This is very efficient for creating multiple instances of the same character or enemy.

The copy also inherits the pivot, the animation (frameRate, loop, yoyo), the flip (flipX, flipY) and the collider of the original: the one from its .spr or the one you set with sprite.collider. That is how the scene editor creates every object, and why the collider you define in the sprite editor is the one they all use. It does not inherit the position, the rotation or the physics: it starts at frame 0, active and VISUAL.

Constants​

Physics body types​

ConstantValueDescription
Sprite.VISUAL0No physics, visual only. The default
Sprite.DYNAMIC1The scene moves it: gravity, velocity, collisions and rotation
Sprite.STATIC2It does not move, but dynamic bodies collide with it: floors and walls
Sprite.KINEMATIC3You move it, with x/y or with its velocity. Gravity and collisions do not affect it, but it pushes dynamic bodies: moving platforms, paddles

Collider types​

ConstantValueDescription
Sprite.RECTANGLE1Rectangular collider
Sprite.CIRCLE2Circular collider

Sprite properties​

Properties are read without parentheses and assigned with =:

// Read a property
var posX = sprite.x

// Assign a property
sprite.x = 100
PropertyTypeDefaultRead / write
x, ydecimal0, or those of Sprite.new(x, y, …)Both
poslist [x, y]—Write only
prevX, prevYdecimalThe same as x and yBoth
angledecimal, degrees0Both
spindecimal, degrees per second0Both
pivotlist [x, y]The center of the frameBoth
width, heightintegerThe size of the frameRead only
activeyes/notrueBoth
flipX, flipYyes/nofalseBoth
frameinteger0Both
framesintegerThe number of framesRead only
frameRateinteger, milliseconds25 with Sprite.new; with Sprite.load, 1000 / fpsBoth
loopyes/notrueBoth
yoyoyes/nofalseBoth
dir1 or -11Both
colliderlistThe whole frameBoth
colliderTypeintegerSprite.RECTANGLERead only

The physics ones are in Physics. A value of the wrong type does not change the property, and it does not raise an error either. Calling a property like a function (sprite.pos(100, 200)) throws ERR_INVALID_OPERATION.

Position and transformation​

x​

Gets or sets the sprite's X position in pixels: that of its pivot.

// Get X position
var posX = sprite.x

// Set X position
sprite.x = 100

y​

Gets or sets the sprite's Y position in pixels: that of its pivot.

// Get Y position
var posY = sprite.y

// Set Y position
sprite.y = 200

pos​

Sets the sprite's position with an [x, y] list. It is a property: it is assigned, not called (sprite.pos(100, 200) raises an error).

sprite.pos = [100, 200]

It is write-only: reading sprite.pos does not return the position. To read it, use sprite.x and sprite.y.

prevX and prevY​

Get or set where the sprite was at the start of the frame, in pixels. It is the point from which hitsGrid checks the sprite's path and with which it decides whether it came from above a one-way platform.

scene.update() keeps them up to date for the sprites in the scene. Without a scene, or when placing or teleporting a sprite, you set them yourself:

// Without a scene: at the start of each frame, before moving the sprite
player.prevX = player.x
player.prevY = player.y
player.x += vx
player.y += vy
var collisions = player.hitsGrid(map)

// When teleporting it, so it doesn't check the path
player.pos = [400, 120]
player.prevX = 400
player.prevY = 120

A sprite created with Sprite.new(100, 50, image) starts with the previous position at (100, 50); with Sprite.load or Sprite.new(image), at (0, 0). On a sprite in a scene, what you write lasts until the next scene.update(). Assigning them something that is not a number throws ERR_TYPE_MISMATCH.

angle​

Gets or sets the rotation angle in degrees. The sprite rotates around its pivot.

// Get angle
var angle = sprite.angle

// Rotate 45 degrees
sprite.angle = 45

// Rotate continuously
sprite.angle = sprite.angle + 1

spin​

Gets or sets the automatic rotation speed in degrees per second.

// Rotate automatically at 90 degrees per second
sprite.spin = 90

// Stop rotation
sprite.spin = 0

// Rotate in the opposite direction
sprite.spin = -45

spin is the sprite's manual rotation. On DYNAMIC bodies, collisions can add physical rotation as well (see angularVel and rotates in the Physics section); both rotations add up.

The rotation is applied when the sprite is drawn: each time you draw it, angle advances spin times the time elapsed between the last two Display.update() calls. A sprite that is not drawn does not turn, and one you draw twice in the same frame turns twice as much.

After a pause, it turns all at once

After a pause or a long load, the next frame turns for all the time that has elapsed: with spin = 10, after a pause(2000) the sprite jumps 20 degrees. If it bothers you, set spin = 0 before the pause and set it again afterwards.

pivot​

Gets or sets the sprite's pivot point (center of rotation), in frame coordinates.

// Get current pivot
var p = sprite.pivot
println("Pivot: ", p[0], ", ", p[1])

// Set pivot with a list
sprite.pivot = [16, 16]

By default, the pivot is at the center of the sprite. x and y are the position of the pivot: with the pivot at [0, 0], they are the top left corner.

Dimensions​

width​

Gets the sprite's width in pixels (read-only): that of its frames.

var w = sprite.width

height​

Gets the sprite's height in pixels (read-only).

var h = sprite.height

State​

active​

Gets or sets whether the sprite is active. Inactive sprites are not drawn, do not collide (hits, contacts, hitsGrid and physics skip them) and are not animated.

// Deactivate sprite
sprite.active = false

// Activate sprite
sprite.active = true

// Check if active
if (sprite.active) then
println("The sprite is active")
end

An animation without loop also deactivates it when it ends (see loop).

In a scene editor room, active = false is for good

In a room with behaviours, states, rules, templates, variables or code, the code the scene editor generates takes out of the scene, at the end of each frame, the sprites with active = false: that is what the Destroy action does. Setting it back to true does not bring it back. To hide an object for a while, move it out of sight; to make it appear again, make a template and create a copy. Objects, in the scene editor, explains it.

Appearance​

flipX​

Gets or sets whether the sprite is flipped horizontally. The image turns over inside its frame, without moving, and the collider is flipped with it.

// Flip horizontally
sprite.flipX = true

// Restore orientation
sprite.flipX = false

// Check state
if (sprite.flipX) then
println("Sprite flipped on X")
end

With an off-center collider, like a sword's or a nose's, the box moves to the other side:

// 32 px wide frame, with the box in the right 8 px
sword.collider = [24, 8, 8, 16]

sword.flipX = true
// Now the box is in the left 8 px, as if it were [0, 8, 8, 16]

sprite.collider still returns what you wrote: the flip is applied when checking collisions (hits, contacts, hitsGrid and physics). The pivot is not flipped. It only accepts true or false.

Physics bodies

An off-center collider changes side when flipped. If the sprite is a physics body and is against a wall, physics separates it.

flipY​

Gets or sets whether the sprite is flipped vertically. As with flipX, the image turns over inside its frame and the collider is flipped with it.

// Flip vertically
sprite.flipY = true

// Restore orientation
sprite.flipY = false

Animation​

The animation advances when the sprite is drawn, with draw.sprite or draw.scene: each time you draw it, it checks whether frameRate has elapsed since the last frame change. The clock is that of Display.update(). A sprite that is not drawn, or that is outside the camera of a scene, is not animated.

frame​

Gets or sets the current animation frame, counting from 0.

// Get current frame
var f = sprite.frame

// Go to frame 3
sprite.frame = 3

A value outside 0 to frames - 1 does not change the frame. Decimals are rounded down.

frames​

Gets the total number of frames (read-only).

var total = sprite.frames
println("Total frames: ", total)

frameRate​

Gets or sets the animation speed in milliseconds per frame. With 0, the sprite does not animate by itself.

// Get current speed
var speed = sprite.frameRate

// Set faster animation (50 ms per frame = 20 fps)
sprite.frameRate = 50

A sprite from Sprite.new(bitmap) starts at 25 (40 frames per second). One from Sprite.load starts at 1000 / fps from the .json: with "fps": 12, 83. A copy made with Sprite.new(other) inherits the original's.

loop​

Gets or sets whether the animation loops.

// Disable loop (the animation plays once and the sprite deactivates)
sprite.loop = false

// Enable loop
sprite.loop = true

With loop = false, after the last frame the sprite becomes active = false: it stops being drawn and colliding, and frame is left equal to frames. If you activate it again with active = true, the animation starts over from the first frame.

yoyo​

Gets or sets the ping-pong (back-and-forth) animation mode.

// Enable ping-pong animation
sprite.yoyo = true

// Disable
sprite.yoyo = false

It only accepts true or false.

dir​

Gets or sets the animation direction: 1 (forward) or -1 (backward). Any other value does not change it.

// Play animation backwards
sprite.dir = -1

// Play forward
sprite.dir = 1

Collisions​

collider​

Gets or sets the collider parameters, in frame coordinates.

// Get collider data
var data = sprite.collider
// For rectangle: [x, y, width, height]
// For circle: [x, y, radius]

// Set rectangular collider
sprite.collider = [0, 0, 32, 32]

// Set circular collider
sprite.collider = [16, 16, 16] // center at (16,16), radius 16

The coordinates are those of the unflipped frame. With flipX or flipY the collider is flipped with the image (see flipX). Decimals are truncated.

A collider without size is ignored without warning

A width, height or radius of 0 or less does not change the collider, and it raises no error: the sprite keeps the previous one. Check the sizes before assigning them.

colliderType​

Gets the current collider type (read-only).

var colType = sprite.colliderType
if (colType == Sprite.RECTANGLE) then
println("Rectangular collider")
elsif (colType == Sprite.CIRCLE) then
println("Circular collider")
end

Physics​

A sprite is a physics body when its bodyType is DYNAMIC, STATIC or KINEMATIC and it is in a scene with physics turned on (scene.physics = true): each scene.update() moves it. The Scene module explains it.

DYNAMIC bodies don't just move: they also rotate when colliding. A box falling on its edge tips over, a ball rolls down a ramp — also against a GridMap. Physical rotation adds to spin (manual rotation) and can be disabled per sprite with rotates.

PropertyTypeDefaultWhat it is
bodyTypeconstantSprite.VISUALThe body type
velocitylist [vx, vy][0, 0]The velocity, in pixels per second
vx, vydecimal0Each component of the velocity
forcelist [fx, fy][0, 0]The force for this step
massdecimal1The mass
frictiondecimal, from 0 to 10The friction
elasticitydecimal, from 0 to 10.5The bounce
gravityScaledecimal1How much of the scene's gravity it gets
dampingdecimal, 0 or more0The braking of the movement
categoryinteger, from 0 to 655351The body's categories
collideWithinteger, from 0 to 6553565535Which categories it collides with
rotatesyes/notrueWhether collisions make it rotate
angularVeldecimal, degrees per second0The physical rotation speed
angularDampingdecimal, 0 or more0The braking of the rotation
bulletyes/nofalseContinuous collision

When you assign any of them to a VISUAL sprite, the value is kept, but the sprite does not move until it is a body in a scene with physics. And when a body becomes VISUAL, it loses all these values: they go back to the defaults.

bodyType​

Gets or sets the physics body type.

// Convert to dynamic body (affected by physics)
sprite.bodyType = Sprite.DYNAMIC

// Convert to static (immovable but collides)
sprite.bodyType = Sprite.STATIC

// Back to visual only (no physics)
sprite.bodyType = Sprite.VISUAL

A value outside 0–3 does not change it. When it becomes STATIC, its velocity is set to 0.

A KINEMATIC is moved by you: with x/y every frame, or with velocity (the scene moves it at that speed). To rotate it, change angle or set angularVel: it rotates around its pivot, and the bodies it hits leave with the speed of the point that touches them (the tip of a paddle pushes harder than its base). An angle you change stays as you leave it; with angularVel, the scene changes angle by itself.

// A lift that rises 30 pixels per second and carries what is on it
lift.bodyType = Sprite.KINEMATIC
lift.vy = -30

// A paddle that turns 90 degrees per second
paddle.bodyType = Sprite.KINEMATIC
paddle.angularVel = 90

velocity​

Gets or sets the velocity as a [vx, vy] list, in pixels per second.

// Get current velocity
var vel = sprite.velocity
println("Velocity: ", vel[0], ", ", vel[1])

// Set velocity
sprite.velocity = [100, -50] // 100 px/s right, 50 px/s up
Very fast sprites

Two sprites moving very fast can pass through each other without colliding, because between one frame and the next they jump from one side of the other body to the other.

Against the tile map this does not happen: hitsGrid checks the sprite's whole path from its previous position (prevX and prevY), as long as that position is up to date.

vx and vy​

Get or set each component of the velocity, in pixels per second: vx horizontally (positive, to the right) and vy vertically (positive, downwards).

player.vx = 120               // to the right, without touching vy
if (player.onGround()) then
player.vy = -300 // jump
end

They are the same as velocity[0] and velocity[1], without creating a list.

force​

Gets or sets the pending force as an [fx, fy] list. It is a single-step force: on the next scene.update() the engine integrates it (Δv = force / mass · dt) and resets it to zero. For sustained thrust (engine, wind, booster) you must reassign it every frame — or call applyForce() inside the loop, which is equivalent (assignment overwrites the frame's accumulated force; applyForce() adds to it).

// Sustained thrust: reassign every frame, before scene.update()
while (true)
rocket.force = [0, -300] // upward thrust
scene.update()
Display.update()
end
acceleration alias

sprite.acceleration is another name for force, from earlier versions: use force. The old name was misleading: the value is divided by the mass (with mass = 4, force = [200, 0] produces an effective acceleration of 50 px/s², not 200) and it is consumed after each step — after a scene.update() the property reads as [0, 0].

friction​

Gets or sets the sprite's friction, from 0 to 1. A value outside stays at the limit.

// Get friction
var f = sprite.friction

// Set friction
sprite.friction = 0.5

Friction also governs collision-induced spin: it's what makes a ball roll instead of sliding. A sprite with friction = 0 (the default) is a perfect bearing — nothing slows its rotation once it starts spinning.

mass​

Gets or sets the sprite's mass. With 0 or less, it is 1.

// Heavy sprite
sprite.mass = 10

// Light sprite
sprite.mass = 0.5

elasticity​

Gets or sets the sprite's elasticity (bounciness), from 0 to 1. By default, 0.5.

// Very elastic (bounces a lot)
sprite.elasticity = 0.9

// No bounce
sprite.elasticity = 0

gravityScale​

Gets or sets how much of the scene's gravity this body gets: the scene's gravity times this number. 1 (the default) is normal, 0 makes it float and a negative number makes it fall upwards.

balloon.gravityScale = 0       // floats
feather.gravityScale = 0.2 // falls slowly
bubble.gravityScale = -0.5 // rises

damping​

Gets or sets how much the body's movement is braked, also in the air and without touching anything: on each physics step, the velocity is divided by 1 + dt × damping. 0 (the default) does not brake. A negative value counts as 0.

// A puck that stops by itself in a couple of seconds
puck.vx = 100
puck.damping = 2

To brake the rotation there is angularDamping.

category and collideWith​

They decide which bodies collide with each other. Each one is a 16-bit number, from 0 to 65535: each bit is a category.

  • category: the body's categories. By default, 1.
  • collideWith: which categories it collides with. By default, 65535: all of them.

Two bodies collide if each one has in collideWith some category of the other. A value outside 0–65535, or one that is not an integer, does not change them.

// The game's categories: 1 the level, 2 the player, 4 its bullets
ground.category = 1
player.category = 2
bullet.category = 4
bullet.collideWith = 1 // bullets hit the level, not the player
player.collideWith = 1 | 2 // the player does not hit its bullets either

They only count in the scene's physics: hits, contacts, hitsGrid and grid.hits do not look at them. The collisions they filter out do not call onCollision either.

rotates​

Gets or sets whether collisions can make the body rotate (only affects DYNAMIC bodies). Enabled by default.

// Platformer character: always upright
player.rotates = false

// Box that tips and rolls (default behavior)
box.rotates = true

With rotates = false the sprite goes back to the classic behavior: physics moves the body but never changes its angle. It's the recommended setting for platformer heroes and in general for any sprite that must stay upright.

Tiny colliders

A collider of very few pixels that can also rotate becomes unstable when resting on other bodies. If that happens, disable rotation with rotates = false.

angularVel​

Gets or sets the physical angular velocity in degrees per second: the spin that collisions impart to the body. It is independent of spin (both rotations add up). There is a safety cap of 720 degrees per second.

// Read the current spin speed
var w = sprite.angularVel

// Launch the sprite spinning
sprite.angularVel = 180

// Stop the spin (for example, when recycling a body)
sprite.angularVel = 0

angularDamping​

Gets or sets the body's angular damping (0 by default, no damping). It gradually slows the spin, even without contact with other surfaces.

The physical way to slow a spin is contact friction (friction), but when a body rolls without slipping friction can no longer stop it: that's what angularDamping is for.

// Ball that gradually stops spinning
ball.angularDamping = 0.5

// Almost no spin loss
top.angularDamping = 0.05

bullet​

Gets or sets the body's continuous collision detection (CCD): disabled by default. With bullet = true the engine doesn't only check where the body is at the end of each step, but sweeps the whole path it travelled during the step, so a fast projectile cannot pass through another body because the impact falls right between two steps.

// Shooter bullet: at 6000 px/s it travels 100 px per step, much more
// than the thickness of a wall
var bullet = Sprite.new(0, 80, bulletTemplate)
bullet.bodyType = Sprite.DYNAMIC
bullet.bullet = true
bullet.velocity = [6000, 0]

Without bullet, a body that moves farther than its own size in a single step can appear on the other side of a thin obstacle without ever touching it. It's classic tunneling, and it only shows with very fast bodies or very thin colliders: at normal speeds discrete detection is enough and cheaper.

When to enable it

Enable it only on the sprites that need it (bullets, pinball balls, bodies falling from very high). It is not a global quality switch: each marked body pays for a sweep against the rest of the bodies on every step, so marking the whole scene is expensive and adds nothing.

It only takes effect on DYNAMIC bodies. On STATIC, KINEMATIC or VISUAL the value is stored and ignored, so you can assign bullet before or after bodyType without worrying about the order.

// Both ways are equivalent
a.bodyType = Sprite.DYNAMIC
a.bullet = true

b.bullet = true
b.bodyType = Sprite.DYNAMIC
A bullet body never sleeps

The scene's auto-sleep (scene.sleeping) excludes bodies marked as bullets. If you mark a permanent sprite of the level, you take it out of the CPU savings for good. For projectiles, which live briefly, this doesn't matter.

Collision against a GridMap already sweeps for all bodies, marked or not: bullet only affects body-to-body collision.

The engine resolves at most two impacts per step per bullet. From the second one on, the bullet stays at the last safe position and continues on the next step: it never passes through anything, but it may seem to slow down for an instant in a very tight corner.

Sprite methods​

Methods are always called with parentheses:

sprite.nextFrame()
var result = sprite.hits(other)
MethodWhat it doesReturns
hits(other)Which sprites it collides withA list of sprites, or nil
contacts(other)Like hits, with the direction and depth of the collisionA list of contacts, or nil
hitsGrid(map [, tiles])Which cells of a GridMap it collides withA list of cells, or nil
onGround()Whether it is resting on somethingtrue or false
onCollision(function [, minImpulse])Warns you when it starts colliding with another bodytrue
applyForce(fx, fy)Pushes during one physics steptrue
applyImpulse(ix, iy)Changes the velocity at oncetrue
applyTorque(t)Makes it rotate during one physics steptrue
applyForceAt(fx, fy, px, py)Pushes from a point: moves and rotatestrue
nextFrame()Moves to the next framenil
addImage(bitmap)Adds a frametrue
mem()The memory of its framesAn integer, in bytes

hits​

Checks the sprite for collisions against another sprite, a list or an array. Returns a list with the sprites it collides with, or nil if there is no collision.

// Against a single sprite
var result = player.hits(enemy)
if (result != nil) then
println("Collision detected!")
end

// Against a list of sprites
var enemies = [enemy1, enemy2, enemy3]
var collisions = player.hits(enemies)
if (collisions != nil) then
foreach (var e in collisions)
e.active = false
end
end

The collision system uses a broadphase with bounding circles before the precise detection (narrowphase). It supports:

  • Rectangle vs Rectangle (including rotation with SAT)
  • Circle vs Circle
  • Circle vs Rectangle

It is pure geometry: it needs no scene or physics, and it does not look at category or collideWith. It skips itself, inactive sprites and anything that is not a sprite; if the sprite that asks is inactive, it returns nil. To check against many sprites, pass it a list: it is faster than calling it with each one. With something that is not a sprite, a list or an array, it throws ERR_TYPE_MISMATCH.

contacts​

Like hits, but it also tells where and how much they overlap. It returns a list with one contact per sprite it touches, or nil if it touches none. Each contact is a list [other, nx, ny, depth]:

PositionContent
[0]The other sprite
[1], [2]nx, ny: unit normal in the direction in which this sprite has to move to get out of the other
[3]depth: depth of the overlap in pixels (always greater than 0)
// Push the player out of the obstacles it is touching
var contacts = player.contacts(obstacles)
if (contacts != nil) then
foreach (var c in contacts)
var nx = c[1], ny = c[2], depth = c[3]
player.x += nx * depth
player.y += ny * depth
end
end

It is a pure overlap test with the current position, angle and collider of both sprites: it needs no scene or physics, so it works with any bodyType, including VISUAL, and with rotated rectangles and circles. It is meant for games that resolve their own collisions, such as an arcade racing game with its own impact response.

It accepts the same as hits (a sprite, a list or an array), skips itself, anything that is not a sprite and inactive sprites, and returns the contacts in list order. A contact right at the edge (overlap 0) does not count, just like in hits.

Direction of the normal

The contacts normal follows the same convention as hitsGrid: it points to where this sprite has to move to get out. The onCollision one goes the other way, from this sprite towards the other.

hitsGrid​

Checks the sprite for collisions against a GridMap. Returns a list of results [tileX, tileY, tileValue, normalX, normalY] for each colliding tile, or nil if there is no collision.

It is a query: it does not move the sprite, and it works with any bodyType, without a scene. It needs:

  • The tile size in the map (map.tSize(width, height), or the one a .gmap brings). Without it, it throws ERR_INVALID_VALUE.
  • Which cells collide: without a second argument, those in map.solids and map.platforms; if the map has none, it returns nil.
  • The previous position up to date (prevX, prevY): see "Path of the frame", below.

tileX and tileY are the column and row of the cell, tileValue its value, and normalX, normalY, where the sprite has to move to get out (on a floor, 0, -1). An inactive sprite returns nil.

Without second argument (solid tiles and platforms from the GridMap)​

var collisions = player.hitsGrid(map)
if (collisions != nil) then
foreach (var info in collisions)
println("Tile at (", info[0], ",", info[1], ") value=", info[2])
println("Normal: ", info[3], ", ", info[4])
end
end

With custom tiles​

// Check against a specific tile (by value)
var collisions = player.hitsGrid(map, 5)

// Check against a list of tile values
collisions = player.hitsGrid(map, [1, 2, 5])

With a second argument there are no one-way platforms: it returns every cell it touches with those values. A value outside 0–255 throws ERR_INVALID_VALUE.

Path of the frame

hitsGrid doesn't only look at where the sprite is: it checks the whole path from its previous position (prevX, prevY) to the current one, so that a fast sprite doesn't pass through a thin wall. If the sprite is in a scene that is updated every frame, that is automatic. Without a scene, or right after placing or teleporting the sprite, set the previous position yourself; otherwise, hitsGrid also returns the tiles along the whole path from where it was.

One-way platforms

When used without a second argument, hitsGrid automatically handles the platforms defined in the GridMap as one-way platforms: it only registers a collision if the sprite came from above (its previous bottom edge was above the tile).

At most 8 tiles per call

hitsGrid returns at most 8 tiles. It is plenty for normal-sized sprites, but a collider that covers many cells at once will not get all of them.

For the map to push the sprite out of the walls, use grid.hits.

onGround​

Returns true if the body is resting on something: on another body or on the map.

if (player.onGround()) then
println("On the ground")
// Allow jumping
if (GPIO.read(15) == HIGH) then
player.vy = -300
end
end

It only works with a DYNAMIC body of a scene with physics:

  • each scene.update() works it out again, depending on whether it touches another body;
  • grid.hits(sprite) sets it to true if the map holds it up. Ask after grid.hits.

A sprite without physics always returns false. Without a scene with physics, nothing sets it back to false: once grid.hits sets it to true, it stays like that.

onCollision​

Registers a callback that the physics engine runs automatically when the sprite starts touching another sprite. It's the reactive way of handling impacts: instead of querying hits() every frame, the engine notifies you with the strength of the hit.

sprite.onCollision(callback)
sprite.onCollision(callback, minImpulse)
sprite.onCollision(nil) // disable
ParameterTypeDescription
callbackfunction/stringDirect reference (myFunction), an object's method (object.method) or name as a string. nil disables the callback
minImpulseinteger/float(Optional) Minimum contact impulse to fire. Default 0 (every new contact fires)

The callback receives five parameters:

func onHit(me, other, impulse, nx, ny)
// me: the sprite that registered the callback
// other: the sprite it collided with
// impulse: strength of the hit (accumulated normal impulse from the solver)
// nx, ny: contact normal, oriented from 'me' towards 'other'
end

Requires a physics scene (scene.physics = true) and both sprites participating in the simulation (DYNAMIC, STATIC or KINEMATIC). Events fire only when contact begins: a box resting on the ground generates no events while at rest, and if the bodies separate and collide again (a bounce) a new event is generated. If both sprites have callbacks, each one receives its own. With a callback that is not a function, a method or a string, it throws ERR_TYPE_MISMATCH.

Example: projectile that destroys above a force threshold​

use Bitmap, Sprite, Scene

class Pig
def init(spr)
this.spr = spr
this.alive = true
// Only strong hits deal damage
spr.onCollision(this.takeHit, 300)
end

def takeHit(me, other, impulse, nx, ny)
this.alive = false
me.active = false
println("Hit with force ", impulse, "!")
end
end

var scene = Scene.new()
scene.physics = true
scene.gravity = 400

var tpl = Bitmap.load("Images/box.bmp")
var rock = Sprite.new(20, 100, tpl)
rock.bodyType = Sprite.DYNAMIC
rock.mass = 4
scene.addSprite(rock)

var target = Sprite.new(200, 100, tpl)
target.bodyType = Sprite.DYNAMIC
scene.addSprite(target)

var pig = Pig(target) // registers target.onCollision(pig.takeHit, 300)

rock.vx = 300 // launch!

With a method as callback (this.takeHit), the sprite keeps the instance alive while the callback is registered.

For simple reactions, an inline lambda avoids defining a separate function:

box.onCollision(fn(me, other, impulse, nx, ny):
println("hit with force ", impulse, "!")
end, 100)
Filter the noise with minImpulse

When settling after a fall, a body may generate several consecutive low-force contacts (solver micro-bounces). With a minImpulse suited to your game, only the hits that matter fire the callback.

Exact signature

The callback must accept exactly 5 parameters (me, other, impulse, nx, ny). If the signature doesn't match, the event is silently discarded. Collisions against GridMap tiles do not generate events (use hitsGrid for that); if the "ground" needs to notify, create it as a STATIC sprite.

applyForce​

Pushes the body with a force that is consumed on the next physics step (Δv = F / mass · dt) and then discarded. It is equivalent to adding onto the force property. For sustained thrust, call it every frame inside the loop.

// Booster: continuous thrust while the button is held
while (true)
if (Input.pressed(ACT_THRUST)) then
ship.applyForce(0, -500)
end
scene.update()
Display.update()
end
ParameterTypeDescription
fxnumberX component of the force
fynumberY component of the force

Requires the sprite to take part in physics (for example, bodyType = Sprite.DYNAMIC). On a sprite without physics, the four apply… methods return an error value and do nothing.

applyImpulse​

Applies an instant impulse: the velocity changes immediately (Δv = impulse / mass), without waiting for the physics step. It's the natural tool for jumps, shots and explosions.

// Jump: immediate velocity change, proportional to mass
player.applyImpulse(0, -300 * player.mass)

// Sideways push from an explosion
box.applyImpulse(150, -80)
ParameterTypeDescription
ixnumberX component of the impulse
iynumberY component of the impulse

Requires the sprite to take part in physics (for example, bodyType = Sprite.DYNAMIC).

applyTorque​

Applies a torque that spins the body against its inertia. Like applyForce, it is consumed on the next physics step and then discarded: for sustained spin, call it every frame. The resulting angular acceleration is t / inertia in degrees/s².

// Spinning top: speed up the spin while the button is held
while (true)
if (Input.pressed(ACT_SPIN)) then
top.applyTorque(4000)
end
scene.update()
Display.update()
end
ParameterTypeDescription
tnumberApplied torque. Positive = clockwise (with Y pointing down)

Inertia depends on the collider's shape and mass: m·(width² + height²)/12 for rectangles and m·radius²/2 for circles. A body with rotates = false ignores torque, and the resulting spin respects the ±720 °/s safety cap and angularDamping.

applyForceAt​

Applies a force at a specific point in the world: besides the linear push (identical to applyForce), it generates the torque r × F around the collider's center. It's the classic off-center shove: hitting a box near a corner moves it and makes it spin.

// Off-center hit: pushes and tips the box
// (with the default pivot, at the center, the point is 8 px below
// the center of the 16x16 collider)
box.applyForceAt(300, 0, box.x, box.y + 8)
ParameterTypeDescription
fxnumberX component of the force
fynumberY component of the force
pxnumberX coordinate of the application point (world)
pynumberY coordinate of the application point (world)

If the point matches the collider's center, the result is a pure force (no spin), and the longer the lever arm, the more it spins. Like any force, it's consumed on the next step: for a sustained push, call it every frame.

nextFrame​

Manually advances to the next frame.

sprite.nextFrame()

On the last frame, it goes back to the first.

addImage​

Adds a bitmap as a new frame to the 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 frames: ", sprite.frames) // 3

The new frame changes width and height to its size. If the collider and the pivot were the default ones (the whole frame and its center), they adjust to the new size; if you had set others, they stay.

Sheet sprites

addImage() is not available on a sprite loaded as a frame sheet (nor on its copies): it raises an error. To keep adding frames, start from a sprite created with Sprite.new(bitmap).

mem​

Returns the memory usage in bytes of all the sprite's frames.

var memory = sprite.mem()
println("Memory used: ", memory, " bytes")

Copies made with Sprite.new(other) share the frames: mem() counts in each one what they take, even though they are in memory only once.

Complete example​

A ship bounces around the screen spinning and knocks out three rocks: when it touches one, it deactivates it. The rocks are copies that share the ship's image.

use Display, Bitmap, Sprite

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

var image = Bitmap.load("Images/ship.bmp") // an image of your project
var ship = Sprite.new(160, 120, image)
ship.spin = 90 // 90 degrees per second

// Three rocks: copies that share the image
var rocks = []
for (var i = 0; i < 3; i++)
rocks.append(Sprite.new(60 + i * 100, 40, image))
end

var vx = 120.0 // pixels per second
var vy = 80.0
var halfX = ship.width / 2 // x/y is the pivot: the center
var halfY = ship.height / 2
for (var f = 0; f < 600; f++) // 20 seconds
ship.x = ship.x + vx * Display.dt
ship.y = ship.y + vy * Display.dt
if (ship.x < halfX) then
ship.x = halfX
vx = abs(vx)
elsif (ship.x > Display.width - halfX) then
ship.x = Display.width - halfX
vx = 0 - abs(vx)
end
if (ship.y < halfY) then
ship.y = halfY
vy = abs(vy)
elsif (ship.y > Display.height - halfY) then
ship.y = Display.height - halfY
vy = 0 - abs(vy)
end
ship.flipX = (vx < 0)

var touched = ship.hits(rocks) // nil if it touches none
if (touched != nil) then
foreach (var r in touched)
r.active = false // stops being drawn and colliding
end
end

draw.sprite(rocks)
draw.sprite(ship)
Display.update()
end
Display.close()

Pitfalls and known issues​

  • In a scene editor room, active = false takes the sprite out of the scene for good (see active).
  • spin turns when drawing, and on every call: draw each sprite only once per frame (see spin).
  • spin turns all at once after a pause (see spin).
  • A collider with width, height or radius 0 is ignored without an error (see collider).
  • When a body becomes VISUAL, it loses its physics: mass, velocity, friction and the rest go back to the defaults (see Physics).
  • onGround() needs a scene with physics; without it, once true it does not go back to false (see onGround).
  • hits and contacts do not look at category or collideWith: those two only filter the scene's physics.
  • Performance: VISUAL sprites do not take part in physics calculations; check collisions with a list (hits(list)) rather than one by one, and share images by making copies with Sprite.new(other) instead of loading the same file several times.

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