Skip to content

Animation controllers ​

assets/<namespace>/ancientcreature/animation_controllers/<name>.controller.json

A state machine deciding which animations play, in the standard Bedrock format Blockbench exports. A client entity runs it by listing it in scripts.animate. Without any, a creature renders in its rest pose.

json
{
  "format_version": "1.10.0",
  "animation_controllers": {
    "controller.animation.triceratops.main": {
      "initial_state": "idle",
      "states": {
        "idle": {
          "animations": ["idle"],
          "transitions": [
            { "death": "query.is_dead" },
            { "graze": "query.action == 'graze'" },
            { "walk": "query.is_moving" }
          ],
          "blend_transition": 0.2
        },
        "walk": {
          "animations": ["idle", { "walk": "math.clamp(query.modified_move_speed * 4, 0, 1)" }],
          "transitions": [{ "idle": "!query.is_moving" }],
          "blend_transition": 0.15
        },
        "graze": {
          "animations": ["graze"],
          "transitions": [{ "idle": "query.action != 'graze'" }],
          "blend_transition": 0.3
        },
        "death": { "animations": ["idle"] }
      }
    }
  }
}

One file can hold several controllers. Each is referenced from the client entity's animations map by its full controller.animation.* name.

States ​

FieldTypeDefaultNotes
animationsarray[]Short names from the client entity, or { "name": "blend weight" } with a Molang weight
transitionsarray[]Checked in order; the first true one wins
blend_transitionfloat or curve0Seconds to cross-fade into the next state, or a { "time": weight } curve
blend_via_shortest_pathboolfalseBlend rotations the short way round
on_entry, on_exitMolang[][]Run when the state is entered or left, typically to set variable.*
sound_effectsarray[]{ "effect": "name" }, played on entry. Names map through the client entity's sound_effects
particle_effectsarray[]{ "effect": "name", "locator": "bone_locator", "pre_effect_script": "..." }, spawned on entry

initial_state defaults to "default".

Each transition is a single-entry object mapping the target state to its condition:

json
{ "walk": "query.is_moving" }

A chain of immediately-true transitions settles in one frame, and cannot loop forever. An initial_state or transition target that isn't a declared state is reported when the pack loads.

Molang ​

Every condition, blend weight, keyframe and script is Molang, Bedrock's expression language, with the whole language available:

  • numbers, 'strings', true/false
  • query./q., variable./v. (per creature, kept between frames), temp./t. (cleared every evaluation) and context./c.
  • arithmetic, comparisons, &&, ||, !, a ? b : c, a ? b, ??
  • assignment, ;-separated statements, return, {} blocks, loop, for_each, break, continue
  • Array., Geometry., Texture. and Material. references in render controllers
  • the math.* library, in degrees: abs, sin, cos, asin, acos, atan, atan2, sqrt, pow, exp, ln, floor, ceil, round, trunc, mod, min, max, clamp, sign, copy_sign, lerp, lerprotate, inverse_lerp, hermite_blend, min_angle, random, random_integer, die_roll, die_roll_integer, pi, and Blockbench's easing family (math.ease_in_quad, math.ease_out_bounce, math.ease_in_out_elastic, ...)

Everything is case-insensitive. The -> entity-reference operator parses but always gives 0, since there are no entity references outside Bedrock.

Expressions are compiled once when the resource pack loads; constant ones (most keyframes) are folded to a number. Anything that fails to compile is a load-time error naming the file and the offending text. An unknown query evaluates to 0 and is logged once, so a typo is visible rather than silently dead.

Queries ​

Bedrock's own queries mean what they mean in Bedrock, so animations written for Bedrock entities work unchanged.

QueryValue
anim_timeSeconds the current animation or controller state has been running
life_timeSeconds since the creature spawned
delta_time, frame_alphaSeconds since the last frame; partial tick
time_of_day, moon_phase, time_stampWorld time
modified_distance_moved, modified_move_speedWalk cycle position and speed, for walk animations. limb_swing and limb_swing_amount are aliases
ground_speed, vertical_speedBlocks per second
walk_distance, yaw_speed
body_y_rotationDegrees
head_y_rotation, head_x_rotationHead yaw and pitch in degrees. Also target_y_rotation/target_x_rotation and head_yaw/head_pitch
position(axis), position_delta(axis)0 = x, 1 = y, 2 = z
distance_from_camera, scale
health, max_health, hurt_time
is_moving, is_on_ground, is_in_water, is_in_water_or_rain, is_in_lava, is_swimming, is_jumping0 or 1
is_sprintingSet while charging. Also is_running
is_glidingA flyer in the air. Also is_flying
is_alive, is_dead, is_hurt, is_on_fire, is_baby, is_invisible, is_leashed0 or 1
has_targetHas an attack target. Also is_attacking, is_angry
is_eatingGrazing or eating. Also is_grazing
is_roaring
is_riding, has_rider
is_tamedHas an owner
is_saddled, is_sittingSaddled; told to stay with the Horn Whistle
armor_tier0 none, 1 iron, 2 golden, 3 diamond, 4 netherite
hunger, is_hungryHow fed the creature is (high is full); whether it has fallen to the species' hunt_threshold
comfort, is_distressedComfort 0–100; whether it is below 30
variantPosition of the creature's variant in the datapack variants list. Also mark_variant, skin_id
variant_nameThe variant id, as a string
speciesThe species id, as a string
action'none', 'attack', 'roar', 'graze' or 'eat', see below
all_animations_finished, any_animation_finishedFor the animations of the current controller state
is_first_personAlways 0

q. is accepted as a synonym for query.. Other mods can add queries — see the API.

A flyer uses is_on_ground to choose between perched and airborne clips, and is_hungry is useful for a leaner idle or a feeding pose:

json
{ "idle": "query.is_on_ground && !query.is_moving" }
{ "fly": "!query.is_on_ground" }
{ "starving_idle": "query.is_hungry" }
{ "eat": "query.action == 'eat'" }

query.action ​

Continuous state (moving, sprinting, in water, baby, health) is derived client-side. action covers the discrete things the client cannot infer: the server sets it when a bite lands, a roar starts, or a graze begins, and clears it afterwards.

ValueSet by
attackLanding a melee hit (cleared after 8 ticks), and most mount abilities
roarThe roar_attack component, for its roar_duration, and the roar mount ability
grazeThe graze component, for its duration
eatReserved
noneNothing in progress

The server stays authoritative ​

Controllers are purely presentational. They read synchronised state and choose clips; they never apply damage, never change entity state and never send anything to the server. variable.* lives only on the client that is drawing the creature. The server decides when an attack lands — the controller only draws the matching animation.

Format version 1 ​

Controller files written for earlier releases ("format_version": 1, with initial_state and states at the top level) still load as a single controller. They keep their old behaviour of blending for 0.2 seconds when a state sets no blend_transition, and of starting in idle.

Adding your own queries ​

java
// during client initialization
AncientCreatureClientApi.registerNumberQuery("wing_beat",
    (state, secondsInState) -> Math.sin(state.ageInTicks * 0.3) * 0.5 + 0.5);

AncientCreatureClientApi.registerStringQuery("mood",
    (state, secondsInState) -> state.creatureHealth < 5 ? "afraid" : "calm");
json
{ "glide": "query.wing_beat > 0.8" }
{ "cower": "query.mood == 'afraid'" }

Names that clash with a built-in query are rejected. Register before any resource pack loads.

Ancient Creature is in beta; species formats may change between releases.