Add animation store for pre-made clips

aac.AnimationStore(folder) seeds a clip from an existing .anim asset and
returns a normal ClipBuilder, so pre-made clips work anywhere generated
clips do and can still be edited (looping, keyframes, toggles, blend
shapes). The Unity side clones the asset into the container and applies
the script's changes to the clone, leaving the original untouched.

- ClipData gains optional source; looping becomes Option<bool> so an
  untouched store clip keeps the asset's own looping setting.
- Add rust/examples/animation_store.rhai, a worked store example.
This commit is contained in:
2026-09-18 14:52:02 +01:00
parent 42a1234e7f
commit 2c3b670ab9
10 changed files with 377 additions and 17 deletions
+115
View File
@@ -0,0 +1,115 @@
// Pre-made animation clips: a store seeds a clip from an `.anim` asset in the project.
//
// cargo run --bin aac-dump -- rust/examples/animation_store.rhai
//
// A store clip is a starting point. Requesting `Assets/.../<name>.anim` produces a clip that is
// usable anywhere a generated clip is; anything written in the script (looping, keyframes, toggles,
// blend shapes) is applied to a clone, so the pre-made asset itself is never modified. A clip the
// script does not touch keeps the asset's own settings.
//
// Combine conditions with `when_all([...])`: Rhai's `&&` cannot be overloaded.
let aac = AnimatorAsCode();
aac.system_name("AnimationStore");
aac.asset_key("AAC_STORE_");
let speed = aac.float_param("Speed", 0.0);
let vertical = aac.float_param("Vertical", 0.0);
let grip = aac.float_param("Grip", 0.0);
let crouching = aac.bool_param("Crouching", false);
let gesture = aac.int_param("Gesture", 0);
let ctrl = aac.new_controller();
let locomotion_layer = ctrl.layer("Locomotion");
let props_layer = ctrl.layer("Props");
// A store per folder. The name is joined as written, so it may contain subfolders, and the `.anim`
// extension is optional.
let locomotion = aac.AnimationStore("Assets/Doloro/Clips/Locomotion");
let props = aac.AnimationStore("Assets/Doloro/Clips/Props/");
let directions = aac.AnimationStore("Assets/Doloro/Clips/Directions");
let idle_clip = locomotion.clip("Idle");
let walk_clip = locomotion.clip("Walk");
let run_clip = locomotion.clip("Run");
// Requesting the same asset again -- with the extension this time -- returns the same clip, so it is
// only declared once and only one clone is generated.
let walk_again = locomotion.clip("Walk.anim");
// Untouched: `Idle` is used as authored, with no looping override and no curves added here.
let idle = locomotion_layer.state("Idle", 0, 0);
idle.set_clip(idle_clip);
// Edited: `Walk` loops and toggles a prop on top of whatever the asset already animates.
walk_clip.looping(true);
walk_clip.toggle("Body/Props/Umbrella", true);
let walk = locomotion_layer.state("Walk", 1, 0);
walk.set_clip(walk_clip);
// Edited: `Run` forces non-looping and blends a smile in over one second.
run_clip.looping(false);
run_clip.blend_shape("Body", "Smile", 0.0, 0.0);
run_clip.blend_shape("Body", "Smile", 1.0, 1.0);
let run = locomotion_layer.state("Run", 2, 0);
run.set_clip(run_clip);
// --- Blend trees over store clips
// A 1D tree over three store clips. `idle_clip` is shared with the `Idle` state above.
let movement = aac.blend_tree("movement");
movement.simple_1d(speed);
movement.add_motion(idle_clip, 0.0);
movement.add_motion(walk_clip, 2.0);
movement.add_motion(run_clip, 5.0);
// A 2D tree over four directional assets.
let forward = directions.clip("Forward");
let back = directions.clip("Back");
let left = directions.clip("Left");
let right = directions.clip("Right");
let strafe = aac.blend_tree("strafe");
strafe.freeform_directional_2d(speed, vertical);
strafe.add_motion(forward, 0.0, 1.0);
strafe.add_motion(back, 0.0, -1.0);
strafe.add_motion(left, -1.0, 0.0);
strafe.add_motion(right, 1.0, 0.0);
// A tree may nest trees declared before it, so the two above blend into one full-body motion.
let full_body = aac.blend_tree("full_body");
full_body.simple_1d(speed);
full_body.add_motion(movement, 0.0);
full_body.add_motion(strafe, 5.0);
let move = locomotion_layer.state("Move", 3, 0);
move.set_motion(full_body);
// A direct tree addresses a store clip by parameter, here a clip nested in a store subfolder.
let umbrella = props.clip("Weapons/Umbrella");
umbrella.looping(true);
let hold = aac.blend_tree("hold");
hold.direct();
hold.add_motion_direct(umbrella, grip);
let holding = props_layer.state("Hold", 0, 0);
holding.set_motion(hold);
// --- Transitions, any-state, and a sub-state machine
idle.transition_to(walk).when(speed > 0.1).no_exit_time().duration(0.2);
walk.transition_to(run).when(speed > 4.0).no_exit_time().duration(0.2);
run.transition_to(walk).when(speed < 4.0).no_exit_time().duration(0.2);
walk.transition_to(idle).when_all([speed < 0.05, crouching == false]).duration(0.2);
let sit_clip = locomotion.clip("Sit");
let sit = locomotion_layer.state("Sit", 4, 0);
sit.set_clip(sit_clip);
locomotion_layer.any_state().transition_to(sit).when(crouching == true).no_exit_time().duration(0.15);
let gestures = locomotion_layer.sub_machine("Gestures", 5, 0);
let wave = gestures.state("Wave", 0, 0);
let point = gestures.state("Point", 1, 0);
wave.set_clip(props.clip("Gestures/Wave"));
point.set_clip(props.clip("Gestures/Point"));
wave.transition_to(point).when(gesture == 1).duration(0.1);
point.transition_to(wave).when(gesture == 0).duration(0.1);