Animation

Picks idle, walk and run animations from a character's speed, and plays one-off actions such as attacks.

Commands

Import blitz.animation As Anim gives your program these, with Anim. in front. Each name leads to its part of the reference.

Functions

Types

Enums

Guide

Animation looks after a character's clips the way a game needs them. You tell it how fast the character is moving, and it plays Idle, Walk or Run, blending smoothly from one to the next. When the character attacks, it plays the attack once, decides whether that may interrupt what's already playing, and then goes back to moving. Markers in a clip, such as the moment a punch lands, come back to you as events, so the damage happens when the fist arrives, not when the key is pressed.

A figure made of boxes throwing a punch on a round stage

Try it

Open boxer.bb and run it. Walk with the arrow keys, hold Shift to run, and press Space to punch. Watch the state change as you speed up and slow down, and the count go up at the very moment each punch lands.

; Left and Right walk, Shift runs, Space throws a punch and Escape quits.
Dialect "modern"
Import blitz.animation As Anim

; A limb hangs from a pivot, so that turning the pivot swings it.
Function Limb:Int(body:Int, x:Float, y:Float, length:Float)
    Local joint:Int = CreatePivot(body)
    PositionEntity joint, x, y, 0
    Local part:Int = CreateCube(joint)
    ScaleEntity part, 0.12, length / 2, 0.12
    PositionEntity part, 0, -length / 2, 0
    EntityColor part, 250, 200, 70
    Return joint
End Function

; Records a clip, one key a frame: the legs swing by stride and the arm by
; reach. AddAnimSeq turns the keys into a clip and returns its number.
Function MakeClip:Int(body:Int, leftLeg:Int, rightLeg:Int, arm:Int, frames:Int, stride:Float, reach:Float)
    Local frame:Int
    For frame = 0 To frames
        Local angle:Float = Float(frame) * 360 / Float(frames)
        RotateEntity leftLeg, stride * Sin(angle), 0, 0
        RotateEntity rightLeg, -stride * Sin(angle), 0, 0
        RotateEntity arm, stride * Sin(angle) - reach * Sin(angle / 2), 0, 0
        SetAnimKey leftLeg, frame
        SetAnimKey rightLeg, frame
        SetAnimKey arm, frame
    Next
    Return AddAnimSeq(body, frames)
End Function

Graphics3D 800, 480, 0, 2
SetBuffer BackBuffer()
Local camera:Int = CreateCamera()
CameraClsColor camera, 18, 52, 79
PositionEntity camera, 0, 1.5, -5
Local light:Int = CreateLight()
RotateEntity light, 30, 20, 0
Local stage:Int = CreateCylinder(32)
ScaleEntity stage, 4.2, 0.05, 1.2
EntityColor stage, 40, 90, 120

; The game moves the mover. The animation moves the body and limbs inside it.
Local mover:Int = CreatePivot()
Local body:Int = CreatePivot(mover)
Local chest:Int = CreateCube(body)
ScaleEntity chest, 0.3, 0.45, 0.2
PositionEntity chest, 0, 1.45, 0
EntityColor chest, 70, 190, 230
Local head:Int = CreateSphere(12, body)
ScaleEntity head, 0.22, 0.22, 0.22
PositionEntity head, 0, 2.15, 0
Local leftLeg:Int = Limb(body, -0.15, 1.0, 1.0)
Local rightLeg:Int = Limb(body, 0.15, 1.0, 1.0)
Local arm:Int = Limb(body, 0.42, 1.8, 0.8)

; Clips made in code have no names, so tell the animator their numbers.
Local options:Anim.AnimatorOptions = New Anim.AnimatorOptions
options.idleSequence = MakeClip(body, leftLeg, rightLeg, arm, 60, 0, 0)
options.walkSequence = MakeClip(body, leftLeg, rightLeg, arm, 40, 30, 0)
options.runSequence = MakeClip(body, leftLeg, rightLeg, arm, 24, 50, 0)
Local punch:Int = MakeClip(body, leftLeg, rightLeg, arm, 30, 0, 90)
AddAnimEvent body, punch, 15, "hit" ; A marker where the punch lands.
Local animator:Anim.Animator = Anim.CreateAnimator(body, options)
Delete options

Local hits:Int
Local previous:Int = MilliSecs()
While Not KeyDown(1)
    Local now:Int = MilliSecs()
    Local seconds:Double = Double(now - previous) / 1000D
    previous = now

    ; Walk at 2 units a second, or run at 4, and stay on the stage.
    Local direction:Int = KeyDown(205) - KeyDown(203)
    If direction <> 0 Then RotateEntity mover, 0, -90 * direction, 0
    Local speed:Float = Abs(direction) * 2
    If KeyDown(42) Or KeyDown(54) Then speed = speed * 2
    MoveEntity mover, 0, 0, speed * Float(seconds)
    If Abs(EntityX(mover)) > 3.6 Then PositionEntity mover, 3.6 * Sgn(EntityX(mover)), 0, 0

    ; Tell the animator the speed and any punch. It picks Idle, Walk or Run.
    Anim.AnimatorSpeed animator, speed
    If KeyHit(57) Then Anim.PlayAction animator, punch, Null
    Anim.UpdateAnimator animator, seconds
    UpdateWorld Float(seconds * 60D) ; The clips have 60 frames a second.
    Anim.CollectAnimatorEvents animator

    ; Read every event, then delete it.
    Local event:Anim.AnimationEvent = Anim.NextEvent(animator)
    While event <> Null
        If event.Name() = "hit" Then hits = hits + 1
        Delete event
        event = Anim.NextEvent(animator)
    Wend

    ; AnimatorState says what the animator has chosen to play.
    Local state:Str = "Idle"
    If Anim.AnimatorState(animator) = Anim.MotionState.Walk Then state = "Walk"
    If Anim.AnimatorState(animator) = Anim.MotionState.Run Then state = "Run"
    If Anim.AnimatorState(animator) = Anim.MotionState.Action Then state = "Punch"

    RenderWorld
    Text 12, 12, "Left/Right: walk   Shift: run   Space: punch"
    Text 12, 36, "State: " + state
    Text 12, 60, "Punches landed: " + hits
    Flip
Wend
Delete animator
End

The figure's clips are made in code, so they have no names, and the program gives the animator their numbers instead. Each frame, it tells the animator how fast the figure is moving and about any punch, lets UpdateWorld play the clips, and reads the events. The animator never moves the figure or reads the clock: the program moves the mover, and the clips move the limbs inside it. Press Space during a punch and nothing happens, because an action only interrupts another of lower priority.

How do I...

Use my own model?

Name its clips Idle, Walk and Run, and pass Null for the options: the animator finds the clips by name. Run is optional.

Local hero:Int = LoadAnimMesh("hero.glb", mover)
Local heroAnimator:Anim.Animator = Anim.CreateAnimator(hero, Null)
If heroAnimator = Null Then RuntimeError Anim.LastError() ; No Idle or Walk clip.

If the clips have other names, look them up and put their numbers in the options. The fox in the glTF sample stands about in a clip called Survey:

Local options:Anim.AnimatorOptions = New Anim.AnimatorOptions
options.idleSequence = FindAnimSeq(fox, "Survey") ; Walk and Run are found by name.
Local foxAnimator:Anim.Animator = Anim.CreateAnimator(fox, options)
Delete options

Change the speeds where it walks and runs?

Set walkSpeed and runSpeed in the options, in units a second. Unless you change them, they are 0.2 and 3.

Local speeds:Anim.AnimatorOptions = New Anim.AnimatorOptions
speeds.walkSpeed = 0.5 ; Walk at about half a unit a second...
speeds.runSpeed = 5    ; ...and run at about five.
Local guardAnimator:Anim.Animator = Anim.CreateAnimator(guard, speeds)
Delete speeds

Let a hit interrupt an attack?

Give the hit a higher priority. An action interrupts one with a lower priority, unless that one can't be interrupted.

Local hurt:Anim.ActionOptions = New Anim.ActionOptions
hurt.priority = 10         ; Higher than the usual 0, so it interrupts a punch.
hurt.interruptible = False ; And nothing interrupts it.
Anim.PlayAction animator, flinch, hurt
Delete hurt

PlayAction returns False when it can't play the action now. It never waits for its turn.

Find out when an action has finished?

Look at each event's Kind(). An action that plays to the end sends ActionFinished, one that another action takes over sends Interrupted, and one you cancel sends ActionCancelled.

If event.Kind() = Anim.EventKind.ActionFinished Then canPunch = True

Play a footstep at the right moment?

Add a marker to the walk clip before you make the animator. Each time the clip passes that frame, an event with the marker's name arrives.

AddAnimEvent hero, FindAnimSeq(hero, "Walk"), 12, "step" ; At frame 12.
; Then, among the events:
If event.Name() = "step" Then PlaySound stepSound

Stop an action part way through?

CancelAction goes straight back to Idle, Walk or Run, even from an action that can't be interrupted.

Anim.CancelAction animator

Blip's tip

Move a pivot that holds your character, not the animated entity itself. While one clip blends into another, the animated entity can be put back where it was. Load your model into a pivot, as in LoadAnimMesh("hero.glb", mover), and move the pivot, as boxer.bb does.

Good to know

More examples

Three more complete programs come with the module:

The reference below has every function, option and rule.

Reference

Needs
No other modules
Examples
arena.bb, quick_start.bb, root_motion.bb

Choose Idle/Walk/Run from movement speed and arbitrate one-shot actions while the engine handles skeleton sampling, crossfades and markers. No source dependencies, callbacks, files, global update loop or import-time allocations. Requires Extended with object-lifecycle-v1 (runtime 1114/module ABI 5 or newer compatible versions).

Blitz3D+ comes with this module, so any Extended program can Import blitz.animation. For a first run, open examples/quick_start.bb and run it; Up walks, Shift runs, Space attacks, C cancels, Escape exits. Change blend in DemoOptions to see a different transition.

Dialect "modern"
Import blitz.animation
; entity is an existing animated entity with Idle and Walk named clips.
Local animator:Animation.Animator = Animation.CreateAnimator(entity, Null)
If animator = Null Then RuntimeError Animation.LastError()
; Every frame, after input/game decisions and before rendering:
Animation.AnimatorSpeed animator, movementSpeed
Animation.UpdateAnimator animator, seconds
UpdateWorld Float(seconds * 60D)
Animation.CollectAnimatorEvents animator
Local event:Animation.AnimationEvent = Animation.NextEvent(animator)
While event <> Null
    If event.Kind() = Animation.EventKind.Marker Then Print event.Name()
    Delete event
    event = Animation.NextEvent(animator)
Wend
; At shutdown, before FreeEntity entity:
Delete animator

The full runnable examples generate articulated meshes and animation keys in source. They need no media downloads. Every example accepts --verify, uses a finite scripted interaction, asserts its result, and deletes its resources.

ExampleWorking interaction
examples/quick_start.bbSpeed selects a moving leg pose; an attack emits one damage marker and finishes.
examples/arena.bbW/Shift moves a courier; Space damages a target at the marker; H interrupts with a higher-priority hit; C cancels. Target height/color reflect health.
examples/root_motion.bbTwo independent characters; the game reads root motion once and clamps movement at a wall. P pauses, R resets, Up/Shift selects locomotion, Space/H selects actions.

API and ownership

The primary spelling is module-qualified functions. Only immutable transferred events have read methods; no extra wrapper/interface is needed around native playback or polling. Null options must be explicit: current Extended does not support optional object arguments with a Null default.

OperationContract and example
CreateAnimator(entity, options):AnimatorBorrow live local animated entity; copy options; caller owns result. CreateAnimator(entity, Null) resolves conventional names. No playback change. Missing clips return Null and LastError.
AnimatorSpeed(animator, speed)Latest finite non-negative absolute movement speed in game units/second: AnimatorSpeed(a, 2). No clock or movement changes.
PlayAction(animator, sequence, options):IntPlayAction(a, attackId, Null) accepts a one-shot now or returns False; no action queue. Copies options.
CancelAction(animator, blendSeconds=0.15D)Harvest pending markers, report cancellation, select the latest speed and restore locomotion immediately. Can cancel noninterruptible actions. Already finished playback reports Finished instead. No active action: no effect.
UpdateAnimator(animator, seconds)Pre-world command selection: UpdateAnimator(a, 1D/60D). Validates non-negative finite seconds; never advances the engine clock.
CollectAnimatorEvents(animator)Post-world marker/completion collection. Repeated calls drain once. Completion restores locomotion in the next command phase.
AnimatorState(animator):IntMotionState.Idle, Walk, Run or Action. Reflects last selection; set speed then Update to refresh it.
NextEvent(animator):AnimationEventTransfer oldest copied event, or Null. Caller deletes returned events.
LastError():StrMissing-clip diagnostic from last CreateAnimator/PlayAction; those operations clear it first. Copy immediately. Priority rejection leaves it empty.
Delete animatorDelete unclaimed queued events and queue. Leave entity, pose, native playback and pending native markers untouched. No implicit collection or terminal event.

AnimatorOptions is a public caller-owned value record. Delete it after creation. Its fields and defaults are:

FieldMeaning/default
idleSequence, walkSequence, runSequenceNative IDs; -1 looks up Idle/Walk/Run. Idle/Walk required. Missing named Run keeps Walk. Explicit out-of-range IDs fail creation. Different states may share an ID.
walkSpeed, runSpeedEntry centers, default 0.2 and 3 game units/second.
hysteresisHalf-width, default 0.1. Enter faster state at center+width, slower at center-width. Width must be non-negative, below walkSpeed, with disjoint bands. Zero width uses the faster state at equality.
blendSecondsLocomotion blend duration, default 0.15 seconds.
ticksPerSecondNative timebase, default 60. Positive finite value representable in Float. All controllers in the same world need the same timebase.
eventCapacityMaximum queued controller records, default 256, allowed 1..65536.
overflowEventOverflow.Strict (default): exceeding capacity is a programming error and no marker is ever lost. EventOverflow.DropOldest: the oldest queued events are discarded to make room and counted by DroppedEvents; use it when the game does not consume every event, for example locomotion-only characters with footstep markers.

ActionOptions has priority:Int=0, interruptible:Int=True and blendSeconds:Double=0.15D. It is copied on acceptance. A higher-priority request interrupts an interruptible current action. Lower/equal priority or a protected current action returns False without polling markers or changing playback. Once native playback has ended, a new action may be accepted at any priority. Accepted actions always restart even when reusing the same sequence. No hidden queue, automatic retry or delayed action callback exists.

Events expose Kind(), Sequence(), Generation() and Name(). Kinds are EventKind.Marker, ActionFinished, Interrupted, ActionCancelled. Name is the native marker text for Marker and empty otherwise. Each accepted action gets a new positive Long generation; locomotion and inherited playback use zero. Sequence/generation refer to the playback that produced the event, including after interruption. All data is copied and survives deletion of the animator and the borrowed entity. If the entity already has pending native markers when control begins, the first collection/replacement harvests them with its initial sequence and generation zero; they predate this controller's action IDs.

Order, time and native limits

  1. Capture input once and make game decisions/movement.
  2. Set speed, PlayAction/CancelAction as needed, and UpdateAnimator for each entity.
  3. Call the game's UpdateWorld Float(seconds * ticksPerSecond) once.
  4. Read/apply game-owned root motion, then CollectAnimatorEvents for each entity.
  5. Drain NextEvent, apply gameplay consequences, update cameras, render and Flip.

Do not replace playback between UpdateWorld and reading its root-motion delta: the native replacement resets that delta. Event handlers are ordinary game code after collection. They can change the next frame's requests or delete a controller once their use of it ends; there are no callbacks inside mutation and no callback reentrancy/deferred-delete facility to exercise.

Move a placement parent outside the animated subtree for game-owned world movement. Native crossfade fallback snapshots include unkeyed channels too, so moving the animated root itself before UpdateWorld can be overwritten during the fade. The procedural rig separates placement, animated entity and its motionRoot; game movement and root-motion clamping operate on placement. Native root translation deltas are local to the animated entity. The advanced example uses an unrotated Z corridor; a rotated character's game/physics adapter must transform that delta into the coordinate space it moves in.

Blends convert seconds to ticks. Playback speed remains the authored rate of one animation tick per UpdateWorld tick. Selection thresholds do not retime a gait; the fixture's Walk travels 2 units/second and Run 4 through authored root motion. For in-place animation, match authored stride to game speed. Smooth speed-driven retiming, directional blend trees and foot locking are deferred.

Zero seconds still selects commands. Pause the world with zero elapsed ticks; the controller has no hidden clock. A zero-length action finishes at the next world update (including zero delta), following native behavior. Large variable steps are allowed within Float range; practical simulations should subdivide long frames for marker limits, animation precision and game collision handling.

Pending native markers are harvested before CrossFadeAnim clears that queue, under the old sequence/generation, then the terminal event, then future incoming markers. A naturally completed action emits Finished, not Interrupted. During a native crossfade only incoming markers are reported. Terminal state changes only once. Collect does not itself change playback, so post-world root motion survives.

The controller checks capacity for a whole incoming batch, including terminal records, before consuming anything or replacing playback. Insufficient capacity raises a programming error; it never drops/evicts an attack marker to continue. Drain events every frame and size capacity for the worst unconsumed batch. A game that does not read every event should choose EventOverflow.DropOldest, which keeps the newest eventCapacity events and reports the rest through DroppedEvents(animator):Long; otherwise undrained footstep markers end the program once the queue fills. The underlying runtime independently caps pending markers at 256 and an update at 4096 crossed clip boundaries, and can discard excess there. This source controller cannot recover those discarded records or query overflow. Author/substep so each native update plus uncollected backlog stays below those limits. Increasing eventCapacity cannot increase native capacity.

One controller and one native marker consumer per entity is a caller contract. Do not externally call Animate/PlayAnim/CrossFadeAnim/SetAnimTime/SetAnimSpeed, mutate clips/events, or let another collector poll the entity while controlled. Configure root motion before creation. Entity handles cannot be validated after FreeEntity; always delete controllers first. Null/deleted controllers, invalid numbers/options and queue overflow are programming errors (RuntimeError). No Stop/Save/Commit operation is applicable to this non-persistent controller; Cancel remains distinct from disposal. Native playback after disposal is owned entirely by the game.

Named glTF integration

Use your own licensed glTF with skeletal clips named Idle and Walk (Run optional):

Local entity:Int = LoadAnimMesh("my-character.gltf")
If entity = 0 Then RuntimeError "character could not be loaded"
Local animator:Animation.Animator = Animation.CreateAnimator(entity, Null)
If animator = Null Then RuntimeError Animation.LastError()
Local attack:Int = FindAnimSeq(entity, "Attack")
; At an attack decision, before UpdateWorld:
If Animation.PlayAction(animator, attack, Null) = False
    ; LastError is nonempty only for a missing clip, not priority rejection.
    If Animation.LastError() <> "" Then Print Animation.LastError()
EndIf

FindAnimSeq follows runtime exact/case-insensitive matching. For other names, resolve IDs yourself into AnimatorOptions. Add gameplay markers with AddAnimEvent before controller creation. Configure SetAnimRootMotion entity, "your-root", False, False, False for game-applied deltas, or enable runtime application and do not apply the delta again. Imported clip timing follows the runtime's conversion; inspect the imported animation in your toolchain. No binary glTF fixture or third-party model is distributed.

Limits

No skeleton sampler, pose mixer, attachments, physics adapter, root-motion ownership, state/input dependency or remote-entity control is added by this release.