1
Animation Description Language
minjaesong edited this page 2025-11-24 21:24:45 +09:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Animation Description Language (ADL)

Audience: Engine maintainers and module developers creating animated humanoid characters.

The Animation Description Language (ADL) is a declarative system for defining skeletal animations for humanoid actors. It uses .properties files to define body parts, skeletons, and frame-by-frame transformations, which are then assembled into animated spritesheets at runtime.

Overview

ADL provides:

  • Skeletal animation — Define joints and transform them per-frame
  • Modular body parts — Separate images for head, torso, limbs, etc.
  • Animation sequences — Run cycles, idle animations, etc.
  • Equipment layering — Headgear, held items, armour overlays
  • Runtime assembly — Build final spritesheet from components

ADL File Format

ADL uses Java .properties format with special conventions.

File Structure

SPRITESHEET=mods/basegame/sprites/fofu/fofu_
EXTENSION=.tga
CONFIG=SIZE 48,56;ORIGINX 29
BODYPARTS=HEADGEAR 11,11;\
          HEAD 11,11;\
          ARM_REST_RIGHT 4,2;\
          TORSO 10,4
SKELETON_STAND=HEADGEAR 0,32;HEAD 0,32;\
               ARM_REST_RIGHT -7,23;\
               TORSO 0,22
ANIM_RUN=DELAY 0.15;ROW 2;SKELETON SKELETON_STAND
ANIM_RUN_1=LEG_REST_RIGHT 1,1;LEG_REST_LEFT -1,0
ANIM_RUN_2=ALL 0,1;LEG_REST_RIGHT 0,-1;LEG_REST_LEFT 0,1

Reserved Keywords

Global Properties:

  • SPRITESHEET — Base path for body part images (required)
  • EXTENSION — File extension for body part images (required)
  • CONFIG — Frame size and origin point (required)
  • BODYPARTS — List of body parts with joint positions (required)

Skeleton Definitions:

  • SKELETON_* — Defines a skeleton (e.g., SKELETON_STAND, SKELETON_CROUCH)

Animation Definitions:

  • ANIM_* — Defines an animation (e.g., ANIM_RUN, ANIM_IDLE)
  • ANIM_*_N — Defines frame N of an animation (e.g., ANIM_RUN_1, ANIM_RUN_2)

Special Body Parts:

  • HEADGEAR — Equipped helmet/hat
  • HELD_ITEM — Item in hand
  • BOOT_L, BOOT_R — Left/right boots
  • GAUNTLET_L, GAUNTLET_R — Left/right gauntlets
  • ARMOUR_* — Armour layers (e.g., ARMOUR_0, ARMOUR_1)

Property Types

SPRITESHEET and EXTENSION

SPRITESHEET=mods/basegame/sprites/fofu/fofu_
EXTENSION=.tga

Body part files are constructed as: SPRITESHEET + bodypart_name + EXTENSION

Example:

  • SPRITESHEET=mods/basegame/sprites/fofu/fofu_
  • Body part: HEAD
  • Result: mods/basegame/sprites/fofu/fofu_head.tga

CONFIG

CONFIG=SIZE 48,56;ORIGINX 29

Parameters:

  • SIZE w,h — Frame dimensions (width, height) in pixels (required)
  • ORIGINX x — X-coordinate of origin point (required)

Origin is always (originX, 0) — the top-centre anchor point for the character.

Frame dimensions include extra headroom:

frameWidth = configWidth + 32   // EXTRA_HEADROOM_X = 32
frameHeight = configHeight + 16  // EXTRA_HEADROOM_Y = 16

BODYPARTS

BODYPARTS=HEAD 11,11;\
          ARM_REST_RIGHT 4,2;\
          LEG_REST_LEFT 4,7;\
          TORSO 10,4;\
          HELD_ITEM 0,0

Defines the list of body parts and their joint positions (anchor points within each sprite).

Format: BODYPART_NAME jointX,jointY

Joint Position:

  • Coordinates are relative to the body part sprite's top-left corner
  • Joint is where this body part connects to the skeleton
  • Example: HEAD 11,11 — Head sprite's joint is 11 pixels right, 11 pixels down from top-left

Paint Order: Body parts are painted in reverse order — last in the list paints first (background), first in the list paints last (foreground).

# Paint order: TORSO (back) → LEG → ARM → HEAD (front)
BODYPARTS=HEAD 8,7;\
          ARM_REST_RIGHT 3,8;\
          LEG_REST_RIGHT 3,7;\
          TORSO 9,4

SKELETON Definitions

SKELETON_STAND=HEADGEAR 0,32;\
               HEAD 0,32;\
               ARM_REST_RIGHT -7,23;\
               TORSO 0,22;\
               LEG_REST_RIGHT -2,7

Defines joint positions for a skeleton pose.

Format: SKELETON_NAME=JOINT_NAME offsetX,offsetY;...

Joint Offsets:

  • Coordinates are relative to the character's origin (originX, 0)
  • Positive X → right, Positive Y → down
  • Example: HEAD 0,32 — Head joint is 0 pixels right, 32 pixels down from origin
  • Example: ARM_REST_RIGHT -7,23 — Right arm joint is 7 pixels left, 23 pixels down

Multiple Skeletons:

SKELETON_STAND=HEAD 0,32;TORSO 0,22;LEG_LEFT 2,7
SKELETON_CROUCH=HEAD 0,28;TORSO 0,20;LEG_LEFT 3,5
SKELETON_JUMP=HEAD 0,30;TORSO 0,21;LEG_LEFT 1,8

ANIM Definitions

ANIM_RUN=DELAY 0.15;ROW 2;SKELETON SKELETON_STAND

Defines an animation sequence.

Parameters:

  • DELAY seconds — Time between frames (float; actors may override delays)
  • ROW row_number — Row in the output spritesheet (starts at 1)
  • SKELETON skeleton_name — Which skeleton this animation uses

Frame Count: Frame count is determined by the highest ANIM_*_N suffix:

ANIM_RUN=DELAY 0.15;ROW 2;SKELETON SKELETON_STAND
ANIM_RUN_1=...
ANIM_RUN_2=...
ANIM_RUN_3=...
ANIM_RUN_4=...
# This animation has 4 frames

ANIM Frame Definitions

ANIM_RUN_1=LEG_REST_RIGHT 1,1;LEG_REST_LEFT -1,0
ANIM_RUN_2=ALL 0,1;LEG_REST_RIGHT 0,-1;LEG_REST_LEFT 0,1

Defines per-joint transformations for a specific frame.

Format: ANIM_NAME_N=JOINT translateX,translateY;...

Transformations:

  • JOINT offsetX,offsetY — Translate joint by (offsetX, offsetY) pixels
  • ALL offsetX,offsetY — Translate ALL joints by (offsetX, offsetY) pixels

Empty Frame:

ANIM_IDLE_1=
# Frame 1: No transformations (use skeleton as-is)

Overlapping Transforms:

ANIM_RUN_2=ALL 0,1;LEG_REST_RIGHT 0,-1
# 1. Move everything down 1 pixel
# 2. Then move right leg up 1 pixel (net: 0 offset for right leg)

Complete Example

# File paths
SPRITESHEET=mods/basegame/sprites/fofu/fofu_
EXTENSION=.tga

# Frame configuration
CONFIG=SIZE 48,56;ORIGINX 29

# Body parts with joint positions
BODYPARTS=HEADGEAR 11,11;\
          HEAD 11,11;\
          ARM_REST_RIGHT 4,2;\
          ARM_REST_LEFT 4,2;\
          LEG_REST_RIGHT 4,7;\
          LEG_REST_LEFT 4,7;\
          TORSO 10,4;\
          TAIL_0 20,1;\
          HELD_ITEM 0,0

# Skeleton: standing pose (paint order: top to bottom)
SKELETON_STAND=HEADGEAR 0,32;\
               HEAD 0,32;\
               ARM_REST_RIGHT -7,23;\
               ARM_REST_LEFT 5,24;\
               TORSO 0,22;\
               LEG_REST_RIGHT -2,7;\
               LEG_REST_LEFT 2,7;\
               TAIL_0 0,13;\
               HELD_ITEM -6,11

# Animation: running (4 frames, 0.15s per frame)
ANIM_RUN=DELAY 0.15;ROW 2;SKELETON SKELETON_STAND
ANIM_RUN_1=LEG_REST_RIGHT 1,1;LEG_REST_LEFT -1,0
ANIM_RUN_2=ALL 0,1;LEG_REST_RIGHT 0,-1;LEG_REST_LEFT 0,1
ANIM_RUN_3=LEG_REST_RIGHT -1,0;LEG_REST_LEFT 1,1
ANIM_RUN_4=ALL 0,1;LEG_REST_RIGHT 0,1;LEG_REST_LEFT 0,-1

# Animation: idle (2 frames, 2s per frame)
ANIM_IDLE=DELAY 2;ROW 1;SKELETON SKELETON_STAND
ANIM_IDLE_1=
ANIM_IDLE_2=TORSO 0,-1;HEAD 0,-1;HELD_ITEM 0,-1;\
            ARM_REST_LEFT 0,-1;ARM_REST_RIGHT 0,-1;\
            HEADGEAR 0,-1

ADProperties Class

The Kotlin class that parses ADL files.

Loading ADL

val adl = ADProperties(gdxFile)

Constructors:

constructor(gdxFile: FileHandle)
constructor(reader: Reader)
constructor(inputStream: InputStream)

Properties

class ADProperties {
    // File information
    lateinit var baseFilename: String       // From SPRITESHEET
    lateinit var extension: String          // From EXTENSION

    // Frame configuration
    var frameWidth: Int                     // From CONFIG SIZE + headroom
    var frameHeight: Int                    // From CONFIG SIZE + headroom
    var originX: Int                        // From CONFIG ORIGINX

    // Spritesheet dimensions
    var rows: Int                           // Max animation row
    var cols: Int                           // Max frame count

    // Body parts
    lateinit var bodyparts: List<String>
    lateinit var bodypartFiles: List<String>
    val bodypartJoints: HashMap<String, ADPropertyObject.Vector2i>

    // Animation data
    internal lateinit var skeletons: HashMap<String, Skeleton>
    internal lateinit var animations: HashMap<String, Animation>
    internal lateinit var transforms: HashMap<String, List<Transform>>
}

Data Classes

// Joint in a skeleton
internal data class Joint(
    val name: String,
    val position: ADPropertyObject.Vector2i
)

// Skeleton pose
internal data class Skeleton(
    val name: String,
    val joints: List<Joint>
)

// Animation sequence
internal data class Animation(
    val name: String,
    val delay: Float,       // Seconds per frame
    val row: Int,           // Row in spritesheet
    val frames: Int,        // Frame count
    val skeleton: Skeleton
)

// Per-frame joint transformation
internal data class Transform(
    val joint: Joint,
    val translate: ADPropertyObject.Vector2i
)

Vector2i

data class Vector2i(var x: Int, var y: Int) {
    operator fun plus(other: Vector2i) = Vector2i(x + other.x, y + other.y)
    operator fun minus(other: Vector2i) = Vector2i(x - other.x, y - other.y)

    fun invertY() = Vector2i(x, -y)
    fun invertX() = Vector2i(-x, y)
    fun invertXY() = Vector2i(-x, -y)
}

Rendering Process

ADL animations are rendered on-the-fly each frame by AssembledSpriteAnimation.renderThisAnimation().

On-the-Fly Rendering

class AssembledSpriteAnimation(
    val adp: ADProperties,
    parentActor: ActorWithBody,
    val disk: SimpleFileSystem?,
    val isGlow: Boolean,
    val isEmissive: Boolean
) : SpriteAnimation(parentActor) {

    // Body part textures cached in memory
    @Transient private val res = HashMap<String, TextureRegion?>()

    var currentAnimation = ""       // e.g., "ANIM_IDLE", "ANIM_RUN"
    var currentFrame = 0            // Current frame index (zero-based)

    fun renderThisAnimation(
        batch: SpriteBatch,
        posX: Float,
        posY: Float,
        scale: Float,
        animName: String,           // e.g., "ANIM_RUN_2"
        mode: Int = 0
    )
}

Rendering Steps (per frame)

  1. Load body part textures — Cache TextureRegion for each body part
  2. Get animation data — Retrieve skeleton and transforms for current frame
  3. Calculate positions — For each body part:
    val skeleton = animation.skeleton.joints.reversed()
    val transforms = adp.getTransform("ANIM_RUN_2")
    val bodypartOrigins = adp.bodypartJoints
    
    AssembleFrameBase.makeTransformList(skeleton, transforms).forEach { (name, bodypartPos) ->
        // Calculate final position
        val drawPos = adp.origin + bodypartPos - bodypartOrigins[name]
    
        // Draw body part texture at calculated position
        batch.draw(texture, drawPos.x * scale, drawPos.y * scale)
    }
    
  4. Draw equipment — Render held items and armour at joint positions

No pre-assembly required — Body parts are positioned and drawn directly each frame.

Example Rendering: ANIM_RUN_2

ADL Definition:

SKELETON_STAND=HEAD 0,32;TORSO 0,22;LEG_RIGHT -2,7
ANIM_RUN_2=ALL 0,1;LEG_RIGHT 0,-1
BODYPARTS=LEG_REST_RIGHT 4,7
CONFIG=SIZE 48,56;ORIGINX 29

Rendering LEG_RIGHT (per frame):

  1. Origin: (29, 0) (from ORIGINX 29)
  2. Skeleton pose: LEG_RIGHT -2,7 → joint offset from origin = (-2, 7)
  3. ALL transform: 0,1 → shift all joints by (0, 1)
  4. LEG_RIGHT transform: 0,-1 → shift this joint by (0, -1)
  5. Final joint offset: (-2, 7) + (0, 1) + (0, -1) = (-2, 7)
  6. Joint in world: (29, 0) + (-2, 7) = (27, 7)
  7. Body part anchor: LEG_REST_RIGHT 4,7 (from BODYPARTS)
  8. Draw position: (27, 7) - (4, 7) = (23, 0)

Body part sprite is drawn at (23, 0) relative to character origin, with its anchor point at the skeleton joint (27, 7).

Practical Usage

Creating a New Character

  1. Draw body parts as separate images:

    • character_head.tga
    • character_torso.tga
    • character_arm_rest_left.tga
    • character_arm_rest_right.tga
    • character_leg_rest_left.tga
    • character_leg_rest_right.tga
  2. Mark joint positions on each sprite (where it connects)

  3. Write ADL file:

SPRITESHEET=mods/mymod/sprites/character/character_
EXTENSION=.tga
CONFIG=SIZE 48,56;ORIGINX 24

BODYPARTS=HEAD 10,8;\
          ARM_REST_RIGHT 5,3;\
          ARM_REST_LEFT 5,3;\
          TORSO 12,6;\
          LEG_REST_RIGHT 4,8;\
          LEG_REST_LEFT 4,8

SKELETON_STAND=HEAD 0,30;\
               ARM_REST_RIGHT -8,24;\
               ARM_REST_LEFT 8,24;\
               TORSO 0,22;\
               LEG_REST_RIGHT -3,8;\
               LEG_REST_LEFT 3,8

ANIM_IDLE=DELAY 1;ROW 1;SKELETON SKELETON_STAND
ANIM_IDLE_1=
  1. Load in code:
val adl = ADProperties(ModMgr.getGdxFile("mymod", "sprites/character.properties"))
actor.sprite = AssembledSpriteAnimation(adl, actor, isGlow = false, isEmissive = false)
actor.sprite.currentAnimation = "ANIM_IDLE"

Adding New Animations

# Walk cycle (8 frames)
ANIM_WALK=DELAY 0.1;ROW 3;SKELETON SKELETON_STAND
ANIM_WALK_1=LEG_REST_RIGHT 0,1;ARM_REST_LEFT 0,1
ANIM_WALK_2=LEG_REST_RIGHT 1,0;ARM_REST_LEFT 1,-1
ANIM_WALK_3=LEG_REST_RIGHT 1,-1;ARM_REST_LEFT 2,-2
ANIM_WALK_4=ALL 0,1;LEG_REST_RIGHT 0,-1;ARM_REST_LEFT 0,1
ANIM_WALK_5=LEG_REST_LEFT 0,1;ARM_REST_RIGHT 0,1
ANIM_WALK_6=LEG_REST_LEFT 1,0;ARM_REST_RIGHT 1,-1
ANIM_WALK_7=LEG_REST_LEFT 1,-1;ARM_REST_RIGHT 2,-2
ANIM_WALK_8=ALL 0,1;LEG_REST_LEFT 0,-1;ARM_REST_RIGHT 0,1

Equipment Layering

Use special body part names for equipment:

BODYPARTS=HEADGEAR 11,11;\     # Helmet/hat slot
          HELD_ITEM 0,0;\      # Item in hand
          GAUNTLET_L 3,3;\     # Left glove
          GAUNTLET_R 3,3;\     # Right glove
          BOOT_L 4,2;\         # Left boot
          BOOT_R 4,2;\         # Right boot
          ARMOUR_0 10,4;\      # Armour layer 0
          ARMOUR_1 10,4        # Armour layer 1

These slots are rendered dynamically from actor inventory:

// In AssembledSpriteAnimation.renderThisAnimation()
if (name in jointNameToEquipPos) {
    val item = (parentActor as? Pocketed)?.inventory?.itemEquipped?.get(jointNameToEquipPos[name])
    fetchItemImage(mode, item)?.let { image ->
        // Draw equipped item at joint position
        batch.draw(image, drawPos.x, drawPos.y)
    }
}

Equipment is rendered automatically when actor has items equipped.

Best Practises

  1. Use consistent joint positions — Same body part across animations should have same joint
  2. Paint order matters — List body parts background-to-foreground
  3. Use ALL for body movement — Move entire character up/down with ALL
  4. Keep frame delays consistent — Use same delay for similar animations (e.g., all walks 0.1s)
  5. Test with origin marker — Verify origin is at character's centre-top
  6. Use skeletons for poses — Define SKELETON_CROUCH, SKELETON_JUMP for clarity
  7. Name body parts clearly — Use _LEFT/_RIGHT, _REST/_ACTIVE conventions
  8. Add headroom — ADProperties adds 32×16 pixels automatically; don't pre-add

Common Pitfalls

  • Wrong paint order — Arm painting behind torso instead of in front
  • Inconsistent joint positions — Head joint moves between body parts
  • Forgetting frame numbersANIM_RUN_1, ANIM_RUN_2, not ANIM_RUN_0
  • Missing ALL transform — Forgot to move entire body up/down
  • Wrong coordinate space — Mixing up joint offsets vs. world positions
  • Overlapping transformsALL and individual transforms don't add correctly
  • Origin misalignment — Character appears to "slide" when moving
  • Negative delays — Invalid delay value

Advanced Techniques

Layered Animations

# Base layer (body)
SKELETON_BASE=TORSO 0,22;LEG_LEFT 2,7;LEG_RIGHT -2,7

# Upper body layer (can animate separately)
SKELETON_UPPER=HEAD 0,32;ARM_LEFT 6,24;ARM_RIGHT -6,24

# Combine in animations
ANIM_WALK=DELAY 0.1;ROW 2;SKELETON SKELETON_BASE
ANIM_ATTACK=DELAY 0.05;ROW 3;SKELETON SKELETON_UPPER

Dynamic Body Part Swapping

Body parts are loaded on construction and cached:

class AssembledSpriteAnimation {
    @Transient private val res = HashMap<String, TextureRegion?>()

    init {
        // Load all body parts from ADL
        adp.bodyparts.forEach {
            res[it] = getPartTexture(fileGetter, it)
        }
    }
}

To swap body parts dynamically:

// Load base character
val sprite = AssembledSpriteAnimation(adl, actor, false, false)

// Replace body part texture (requires modifying internal res map)
val helmetTexture = loadHelmet("iron_helmet.tga")
// Note: res is private; body part swapping typically uses equipment slots instead

// For equipment, use HELD_ITEM/HEADGEAR slots which render from inventory
actor.inventory.itemEquipped[GameItem.EquipPosition.HEAD] = ItemCodex["item:helmet_iron"]
// Equipment renders automatically in renderThisAnimation()

Mirror Animations

AssembledSpriteAnimation supports horizontal and vertical flipping:

class AssembledSpriteAnimation {
    var flipHorizontal = false
    var flipVertical = false
}

// Mirror character for right-facing
sprite.flipHorizontal = true

// In renderThisAnimation(), flipping is applied:
fun renderThisAnimation(...) {
    if (flipHorizontal) bodypartPos = bodypartPos.invertX()
    if (flipVertical) bodypartPos = bodypartPos.invertY()

    // Draw with negative width/height for flipping
    if (flipHorizontal && flipVertical)
        batch.draw(image, x, y, -w, -h)
    else if (flipHorizontal)
        batch.draw(image, x, y, -w, h)
    // ...
}

See Also

  • Actors — Actor system using ADL animations
  • Fixtures — Fixture actors (non-animated)
  • Glossary — Animation terminology