Picks idle, walk and run animations from a character's speed, and plays one-off actions such as attacks.
Import blitz.animation As Anim gives your program these, with Anim. in front. Each name leads to its part of the reference.
LastError:Str()CreateAnimator:Animator(entity:Int, options:AnimatorOptions)AnimatorSpeed(animator:Animator, speed:Float)PlayAction:Int(animator:Animator, sequence:Int, options:ActionOptions)CancelAction(animator:Animator, blendSeconds:Double = 0.15D)UpdateAnimator(animator:Animator, seconds:Double)CollectAnimatorEvents(animator:Animator)AnimatorState:Int(animator:Animator)DroppedEvents:Long(animator:Animator)NextEvent:AnimationEvent(animator:Animator)AnimatorOptionsidleSequence:Int = -1, walkSequence:Int = -1, runSequence:Int = -1, walkSpeed:Float = 0.2, runSpeed:Float = 3.0, hysteresis:Float = 0.1, blendSeconds:Double = 0.15D, ticksPerSecond:Double = 60D, eventCapacity:Int = 256, overflow:Int = EventOverflow.StrictActionOptionspriority:Int = 0, interruptible:Int = True, blendSeconds:Double = 0.15DAnimationEventKind:Int(), Sequence:Int(), Generation:Long(), Name:Str()AnimatorMotionStateIdle, Walk, Run, ActionEventKindMarker, ActionFinished, Interrupted, ActionCancelledEventOverflowStrict, DropOldestAnimation 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.

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.
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
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
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.
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
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
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, asboxer.bbdoes.
options.overflow = Anim.EventOverflow.DropOldest, which throws the
oldest away instead.UpdateWorld plays the clips. Call it after UpdateAnimator, with the
seconds times 60: UpdateWorld Float(seconds * 60D). To pause, give them
both 0 seconds.Three more complete programs come with the module:
The reference below has every function, option and rule.
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.
| Example | Working interaction |
|---|---|
examples/quick_start.bb | Speed selects a moving leg pose; an attack emits one damage marker and finishes. |
examples/arena.bb | W/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.bb | Two 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. |
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.
| Operation | Contract and example |
|---|---|
CreateAnimator(entity, options):Animator | Borrow 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):Int | PlayAction(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):Int | MotionState.Idle, Walk, Run or Action. Reflects last selection; set speed then Update to refresh it. |
NextEvent(animator):AnimationEvent | Transfer oldest copied event, or Null. Caller deletes returned events. |
LastError():Str | Missing-clip diagnostic from last CreateAnimator/PlayAction; those operations clear it first. Copy immediately. Priority rejection leaves it empty. |
Delete animator | Delete 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:
| Field | Meaning/default |
|---|---|
idleSequence, walkSequence, runSequence | Native 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, runSpeed | Entry centers, default 0.2 and 3 game units/second. |
hysteresis | Half-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. |
blendSeconds | Locomotion blend duration, default 0.15 seconds. |
ticksPerSecond | Native timebase, default 60. Positive finite value representable in Float. All controllers in the same world need the same timebase. |
eventCapacity | Maximum queued controller records, default 256, allowed 1..65536. |
overflow | EventOverflow.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.
UpdateWorld Float(seconds * ticksPerSecond) once.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.
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.
No skeleton sampler, pose mixer, attachments, physics adapter, root-motion ownership, state/input dependency or remote-entity control is added by this release.