DEMIENGINEDOCUMENTATION Everything is free
2D & isometric · Game systems

Sprite fields, sheets, clips, and frame events.

A two-player fighting scene: health bars, control hints, and two sprite characters on a platform.
A two-player fighting example built from sprites: characters, a ground platform, HUD health bars, and the collider debug overlay.

Sprites & animation

A sprite is a textured rectangle drawn in the world. This page explains what sprites are, every field you can set, how to stretch panels with nine-slice and clip art with masks, and how to play frame animation.

What a sprite is

Flat art in a game — heroes, tiles, crates, portraits — is drawn as a sprite: a rectangle that samples an image (the texture) and is placed in the world by the entity’s transform. The sprite decides which part of the image shows, how large it is in world units, and when it is drawn relative to other art.

Sprites exist because full 3D models would be wasteful for flat art, and because one texture can carry many frames (a sprite sheet) that animation can page through. In Skyforge, station signs, cargo icons, and 2D drones are all sprites.

Sprite fields

{
  "Sprite": {
    "texture": "asset://sprites/hero",
    "source_position": [0, 0],
    "source_size": [32, 32],
    "size": [1, 1],
    "pivot": [0.5, 0],
    "flip_x": false,
    "flip_y": false,
    "color": [1, 1, 1, 1],
    "layer": "characters",
    "sorting_order": 10,
    "material": "asset://materials/additive",
    "nine_slice": [[2, 2], [2, 2]],
    "mask_offset": [0, 0],
    "mask_size": [1, 1]
  }
}
FieldMeaningTypical value
textureAsset URI of the image to sample.asset://sprites/hero
source_position / source_sizeSub-rectangle of the texture: [x, y] and [w, h]. Use them to pick one frame out of a sheet.[0, 0] / [32, 32]
source_normalizedInterpret the source rectangle in 0…1 coordinates instead of pixels.false
size[w, h] the sprite occupies in world units.[1, 1]
pivotWhere the entity origin sits inside the sprite, [0..1, 0..1] of the sprite extent. [0.5, 0] is bottom-center — ideal for characters standing on the floor.[0.5, 0.5]
flip_x / flip_yMirror the art horizontally or vertically.Flip X for facing left
colorRGBA tint and alpha, each 0 to 1.[1, 1, 1, 1]
layerRender layer name, part of the draw-order chain.characters
sorting_orderInteger order inside the layer. Higher draws later (on top).10
materialOptional material asset for custom blending (additive glow, outline).asset://materials/additive
nine_slice[[left, top], [right, bottom]] border widths for stretchable panels.[[2, 2], [2, 2]]
mask_offset / mask_sizeClip the sprite to a sub-rectangle (see below).[0, 0] / [1, 1]

Transparent art needs alpha in the source image (PNG recommended). Opaque backgrounds look like boxes in play.

Sprites from Lua

local Sprite2D = require("demi.sprite2d")

Sprite2D.set_color(id, { 1, 0.4, 0.4, 1 })
Sprite2D.set_flip(id, true, false)
Sprite2D.set_size(id, 1.5, 1.5)
Sprite2D.set_layer(id, "characters")
Sprite2D.set_sorting_order(id, 20)
Sprite2D.set_material(id, "asset://materials/additive")

Sprite2D.play_animation(id, "run")
Sprite2D.pause_animation(id)
Sprite2D.resume_animation(id)
local clip = Sprite2D.current_animation(id)
CallArgumentsReturns
set_colorid, RGBA table {r, g, b, a} 0 to 1Nothing. Multiplies the sprite tint and alpha.
set_flipid, flip_x, flip_y booleansNothing. Mirrors the art.
set_sizeid, width, height world unitsNothing. Rescales the quad.
set_layerid, layer nameNothing. Moves the sprite in the draw-order chain.
set_sorting_orderid, integerNothing. Changes order within the layer.
set_materialid, material asset URINothing. Applies the material; pass nil to clear.
play_animationid, clip nameNothing. Starts that clip from the beginning.
pause_animation / resume_animationidNothing. Freezes or continues the current clip.
current_animationidCurrent clip name as a string, or nil when no clip is playing.

Nine-slice panels

A dialog frame drawn as one stretched image warps its rounded corners into smears. Nine-slice splits the art into nine regions — four corners kept unscaled, four edges stretched in one direction, one center stretched in both — so borders stay crisp at any size.

{
  "Sprite": {
    "texture": "asset://ui/panel",
    "size": [4, 1.2],
    "nine_slice": [[8, 8], [8, 8]]
  }
}
  • size [4, 1.2] is the stretched world extent of the panel.
  • nine_slice [[8, 8], [8, 8]] gives left/top and right/bottom border widths in texture pixels.
  • Corner patches stay 8×8 pixels; edges and the center stretch to fill the rest. Nothing is returned — this is authored data.

Use nine-slice for Skyforge dialog frames, inventory slots, and any UI panel whose size is not fixed.

Masks

mask_offset and mask_size crop the sprite to a sub-rectangle of its own extent. Shrinking mask_size on X reveals less of a bar from the left — perfect for health bars without a separate shader.

{
  "Sprite": {
    "texture": "asset://ui/health_bar",
    "size": [2, 0.25],
    "mask_offset": [0, 0],
    "mask_size": [1, 1]
  }
}
  • mask_offset [0, 0] anchors the visible window at the sprite’s lower-left.
  • mask_size [1, 1] shows the full sprite; set [0.6, 1] to show 60% of the width.
  • Both values are fractions of the sprite extent (0 to 1). Drive them from gameplay to animate the fill.

Use masks for health and fuel bars, portrait frames, minimap windows, and tutorial progress strips.

Frame animation: clips

SpriteAnimator2D pages through frames of the sprite’s texture. The explicit clips form names each animation and points at a run of frames.

{
  "SpriteAnimator2D": {
    "frame_size": [32, 32],
    "clips": {
      "idle": { "start_frame": 0, "frame_count": 4, "fps": 8, "loop": true },
      "run":  { "start_frame": 4, "frame_count": 6, "fps": 12, "loop": true,
                "events": [{ "time": 0.5, "name": "footstep" }] }
    }
  }
}
FieldMeaning
frame_size[w, h] of one frame in texture pixels.
clipsMap of clip name to clip settings.
start_frameFirst frame index of the clip. Frames count left-to-right across the sheet, row by row.
frame_countHow many frames the clip plays.
fpsPlayback rate in frames per second.
loopOptional. true repeats the clip; omitted clips stop on the last frame.
eventsOptional list of { time, name } markers (see below).

Frame animation: atlas

When the sheet is a strict grid and every row is one animation, the atlas form writes the clip table for you.

{
  "SpriteAnimator2D": {
    "atlas": {
      "columns": 8,
      "rows": 3,
      "row_names": ["idle", "run", "jump"],
      "fps": 12,
      "loop": true
    }
  }
}
  • columns and rows describe the grid of frames in the texture.
  • row_names names each generated clip — row 0 becomes idle, row 1 run, and so on.
  • fps and loop apply to every generated clip.
  • Explicit clips entries override generated rows with the same name when both are present.

Choose clips when frames come from several places or need different rates. Choose atlas when the sheet is a uniform grid of named rows.

Frame events

Footsteps, dust puffs, and hit sparks must fire on the right frame, not on a timer in gameplay code. Clip events mark normalized times inside a clip.

local Events = require("demi.events")

Events.subscribe("sprite_animation", function(e)
    -- e.entity_id  the animated sprite
    -- e.clip       clip name that fired
    -- e.name       event name, for example "footstep"
    -- e.frame      frame index at the time of the event
    if e.name == "footstep" then
        local Audio = require("demi.audio")
        Audio.play("asset://audio/step", { bus = "sfx" })
    end
end)
  • Events.subscribe registers the handler and returns a subscription value; the handler receives one payload table.
  • time in the clip definition is 0 to 1 across the clip. 0.5 on a 6-frame clip fires as playback crosses the middle.
  • The payload’s entity_id, clip, name, and frame let one handler service many markers.

Sorting order and layers

Overlapping sprites need a predictable draw order. DemiEngine sorts in three passes:

  1. sorting_order (integer, higher on top)
  2. layer name
  3. Isometric depth (for iso placement, see Isometric grid)

Practical setup for a 2D game:

LayerContents
backgroundParallax sky, distant stations
worldTilemap tiles and props
charactersHeroes, drones, crates
effectsParticles, flashes
uiBars and markers drawn in world space

Keep layers few and use sorting_order inside a layer for local details (a held item above the character). Debug overlay draw_order shows the resolved chain when something hides behind the wrong art.

Going further

  • Sprites pair naturally with Tilemaps for large levels.
  • Parameter-driven clip switching with transitions lives in State machines.
  • Footstep markers are the usual place to trigger sounds from Audio mixing.
  • Attack frames that should deal damage use timed collision windows instead of frame events alone.