🧠 Brains complement: IF an unreal-engine-skills-manager tool (external MCP) exists in this session, call it with {action: "load", skill: "animation-system"} for UE domain knowledge on this topic — correct APIs, architecture, best practices — and treat it as the rubric for any review / "best practices" question. If no such tool is available (e.g. running under Claude Code or Codex without that MCP), skip this line entirely and proceed with this skill alone — do NOT attempt the call.
Animation Blueprint Skill
Loading skills: skills load through the engine's AgentSkillToolset (ListSkills/GetSkills) —
there is no vibeue-skills-manager tool. Run VibeUE services with executepythoncode
(unreal.AnimGraphService.<method>()); reach engine toolsets with call_tool.
An AnimBP is a Blueprint. The state-machine/AnimGraph authoring here is VibeUE
AnimGraphService (intact). For its variables and compile, note VibeUE's BlueprintService
was trimmed: variable creation and blueprint compile moved to the engine toolset
editortoolset.toolsets.blueprint.BlueprintTools (addvariable, compile_blueprint), called via
calltool. BlueprintService still has listvariables, getvariableinfo,
setvariabledefaultvalue, variableexists, components, and overridable functions. Bool fields
on info structs drop the C++ b prefix in Python — use ispure, isoutput, already_overridden
(NOT bispure); call discoverpythonclass to confirm field names rather than guessing.
⚠️ #1 GOTCHA — Transitions need a RULE or they NEVER fire
add_transition() only draws the arrow between two states. The transition's rule graph starts empty, which evaluates to false, so the transition is inert — the state machine will never move. You MUST set a rule on every transition:
import unreal
abp = "/Game/ABP_Character"
BT = "editor_toolset.toolsets.blueprint.BlueprintTools"
# 1) Add the bool/float variable the rule reads, then COMPILE so it resolves.
# Variable creation + compile are ENGINE BlueprintTools (call_tool), not VibeUE:
call_tool(BT, "add_variable", {"blueprint": {"refPath": abp}, "variable_name": "bIsDead", "variable_type": "bool"})
call_tool(BT, "compile_blueprint", {"blueprint": {"refPath": abp}})
# (Run describe_toolset(BT) for exact arg names.)
# 2) Draw the transition (VibeUE AnimGraphService)
unreal.AnimGraphService.add_transition(abp, "Locomotion", "Idle", "Dead", 0.2)
# 3) Give it a rule — THIS is what makes it fire
unreal.AnimGraphService.set_transition_rule_from_bool(abp, "Locomotion", "Idle", "Dead", "bIsDead")
Rule options:
settransitionrulefrombool(abp, machine, src, dst, bool_var, invert=False)
settransitionrulecomparison(abp, machine, src, dst, floatvar, op, value) — op ∈ greater/less/greaterequal/lessequal/equal/not_equal
settransitionruleautomatic(abp, machine, src, dst, triggertime=-1.0) — fires when the source state's animation is (almost) finished; perfect for one-shots (attack→idle)
cleartransitionrule(...) — non-destructively reset a rule
After authoring, always run validatestatemachine() (see below) and compile.
Critical Rules
Opening Animation States
Use AnimGraphService to navigate directly to states and graphs:
import unreal
# Open an AnimBP's main AnimGraph
unreal.AnimGraphService.open_anim_graph("/Game/ABP_Character", "AnimGraph")
# Open a specific state inside a state machine
unreal.AnimGraphService.open_anim_state("/Game/ABP_Character", "Locomotion", "IdleLoop")
# Open a transition rule
unreal.AnimGraphService.open_transition("/Game/ABP_Character", "Locomotion", "Idle", "Walk")
⚠️ State Machine Names
State machine names come from the node title, not the graph name. Use liststatemachines() to get the correct names:
machines = unreal.AnimGraphService.list_state_machines("/Game/ABP_Character")
for m in machines:
print(f"Machine: {m.machine_name}") # Use THIS name for open_anim_state
⚠️ Case Sensitivity
All name lookups are case-insensitive, but prefer using exact names from introspection:
# Both work:
unreal.AnimGraphService.open_anim_state(path, "State Controller", "In Air Loop")
unreal.AnimGraphService.open_anim_state(path, "state controller", "in air loop")
Workflows
Create Animation Blueprint
CRITICAL: AnimBlueprints require a skeleton reference. Always find the skeleton first.
import unreal
# Step 1: Find skeleton from reference blueprint
ref_bp_path = "/Game/Blueprints/SandboxCharacter_CMC_ABP"
if unreal.EditorAssetLibrary.does_asset_exist(ref_bp_path):
ref_bp = unreal.load_asset(ref_bp_path)
skeleton = ref_bp.get_editor_property('target_skeleton')
skeleton_path = skeleton.get_path_name()
print(f"Using skeleton: {skeleton_path}")
else:
# Or load directly if you know the path
skeleton_path = "/Game/Characters/UEFN_Mannequin/Meshes/SK_UEFN_Mannequin"
skeleton = unreal.load_asset(skeleton_path)
# Step 2: Delete existing if needed
asset_path = "/Game/Tests/ABP_TestCharacter"
if unreal.EditorAssetLibrary.does_asset_exist(asset_path):
unreal.EditorAssetLibrary.delete_asset(asset_path)
# Step 3: Create AnimBlueprint with skeleton
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
blueprint_factory = unreal.AnimBlueprintFactory()
blueprint_factory.set_editor_property("target_skeleton", skeleton)
blueprint_factory.set_editor_property("parent_class", unreal.AnimInstance)
animation_blueprint = asset_tools.create_asset(
"ABP_TestCharacter",
"/Game/Tests",
unreal.AnimBlueprint,
blueprint_factory
)
# Step 4: Save
if animation_blueprint:
unreal.EditorAssetLibrary.save_asset(asset_path, only_if_is_dirty=False)
print(f"Created: {asset_path}")
Opening in Editor:
# Use open_editor_for_assets (plural) with list
editor_subsystem = unreal.get_editor_subsystem(unreal.AssetEditorSubsystem)
opened = editor_subsystem.open_editor_for_assets([animation_blueprint])
⚠️ DO NOT use AssetEditorSubsystem.openeditorfor_asset() (singular) - it will cause AttributeError.
Discover Animation Blueprint Structure
import unreal
abp_path = "/Game/Blueprints/SandboxCharacter_CMC_ABP"
# Get overview
parent = unreal.AnimGraphService.get_parent_class(abp_path)
skeleton = unreal.AnimGraphService.get_skeleton(abp_path)
print(f"Parent: {parent}, Skeleton: {skeleton}")
# List all graphs
graphs = unreal.AnimGraphService.list_graphs(abp_path)
for g in graphs:
print(f"Graph: {g.graph_name} ({g.graph_type}), Nodes: {g.node_count}")
List State Machines and States
import unreal
abp_path = "/Game/ABP_Character"
# Find all state machines
machines = unreal.AnimGraphService.list_state_machines(abp_path)
for machine in machines:
print(f"\nState Machine: {machine.machine_name} ({machine.state_count} states)")
# List states in this machine
states = unreal.AnimGraphService.list_states_in_machine(abp_path, machine.machine_name)
for state in states:
end_marker = " [END]" if state.is_end_state else ""
print(f" - {state.state_name} ({state.state_type}){end_marker}")
Navigate to Specific State
import unreal
abp_path = "/Game/Blueprints/SandboxCharacter_CMC_ABP"
# Open the AnimBP and navigate to a specific state
unreal.AnimGraphService.open_anim_state(
abp_path,
"State Controller", # State machine name
"In Air Loop" # State name
)
Inspect Transitions
import unreal
abp_path = "/Game/ABP_Character"
machine_name = "Locomotion"
# Get all transitions in a state machine
transitions = unreal.AnimGraphService.get_state_transitions(abp_path, machine_name)
for t in transitions:
auto = " [AUTO]" if t.is_automatic else ""
print(f"{t.source_state} -> {t.dest_state} (blend: {t.blend_duration}s){auto}")
# Get transitions for a specific state
idle_transitions = unreal.AnimGraphService.get_state_transitions(abp_path, machine_name, "Idle")
Authoring State Machines (build / edit)
⭐ Fastest path: declarative buildstatemachine
Build (or extend) a whole machine from one JSON spec in a single atomic, idempotent call. Re-running it never duplicates existing states/transitions. It sets animations, rules, the entry state, then compiles — and returns a JSON report.
import unreal, json
abp = "/Game/ABP_Character"
BT = "editor_toolset.toolsets.blueprint.BlueprintTools"
# Add the variables the rules read FIRST, then compile so the rules can resolve them.
# Variable creation + compile are ENGINE BlueprintTools (call_tool), not VibeUE:
call_tool(BT, "add_variable", {"blueprint": {"refPath": abp}, "variable_name": "Speed", "variable_type": "float"})
call_tool(BT, "add_variable", {"blueprint": {"refPath": abp}, "variable_name": "bAttack", "variable_type": "bool"})
call_tool(BT, "compile_blueprint", {"blueprint": {"refPath": abp}})
spec = {
"states": [
{"name": "Idle", "animation": "/Game/Anims/Idle", "loop": True, "pos": [0, 0]},
{"name": "Walk", "animation": "/Game/Anims/Walk", "loop": True, "pos": [350, 0]},
{"name": "Attack", "animation": "/Game/Anims/Attack", "loop": False, "pos": [350, 250]},
],
"transitions": [
{"from": "Idle", "to": "Walk", "rule": {"type": "comparison", "variable": "Speed", "op": "greater", "value": 10}, "blend": 0.15},
{"from": "Walk", "to": "Idle", "rule": {"type": "comparison", "variable": "Speed", "op": "less_equal", "value": 10}},
{"from": "Idle", "to": "Attack", "rule": {"type": "bool", "variable": "bAttack"}},
{"from": "Attack", "to": "Idle", "rule": {"type": "automatic"}}, # when the attack anim ends
],
"entry": "Idle",
}
report = json.loads(unreal.AnimGraphService.build_state_machine(abp, "Locomotion", json.dumps(spec)))
print(report) # {"success":true,"states_created":3,"transitions_created":4,"errors":[]}
rule.type ∈ bool (variable, optional invert), comparison (variable,op,value), automatic (optional trigger_time), always (always-true), or omit rule to leave it inert.
Manual / incremental authoring
import unreal
abp = "/Game/ABP_Character"
# State machine + states
unreal.AnimGraphService.add_state_machine(abp, "Locomotion", 0, 0)
unreal.AnimGraphService.add_state(abp, "Locomotion", "Idle", 0, 0)
unreal.AnimGraphService.add_state(abp, "Locomotion", "Walk", 350, 0)
# One call: create+assign the sequence player inside the state AND wire it to Output Pose
unreal.AnimGraphService.set_state_animation(abp, "Locomotion", "Idle", "/Game/Anims/Idle", True)
unreal.AnimGraphService.set_state_animation(abp, "Locomotion", "Walk", "/Game/Anims/Walk", True)
# Default/entry state
unreal.AnimGraphService.set_entry_state(abp, "Locomotion", "Idle")
# Transitions + rules (rules are mandatory — see top gotcha)
unreal.AnimGraphService.add_transition(abp, "Locomotion", "Idle", "Walk", 0.15)
unreal.AnimGraphService.set_transition_rule_comparison(abp, "Locomotion", "Idle", "Walk", "Speed", "greater", 10.0)
unreal.AnimGraphService.set_transition_priority(abp, "Locomotion", "Idle", "Walk", 1)
# Compile via engine BlueprintTools:
call_tool("editor_toolset.toolsets.blueprint.BlueprintTools", "compile_blueprint", {"blueprint": {"refPath": abp}})
Verify before claiming success — validatestatemachine
result = unreal.AnimGraphService.validate_state_machine(abp, "Locomotion")
print(f"valid={result.is_valid} states={result.state_count} transitions={result.transition_count}")
for e in result.errors: print("ERROR:", e) # inert transitions, no entry state
for w in result.warnings: print("WARN :", w) # unreachable states, states with no animation
Treat any errors as a build failure. The most common error is an inert transition (a transition with no rule) — fix it with one of the settransitionrule_* calls.
Find Used Animation Sequences
import unreal
abp_path = "/Game/ABP_Character"
sequences = unreal.AnimGraphService.get_used_anim_sequences(abp_path)
for seq in sequences:
print(f"{seq.sequence_name}")
print(f" Path: {seq.sequence_path}")
print(f" Used in: {seq.used_in_graph}")
Focus on Node by ID
import unreal
abp_path = "/Game/ABP_Character"
# If you have a node GUID from get_nodes_in_graph
node_id = "ABC123-DEF456-..."
unreal.AnimGraphService.focus_node(abp_path, node_id)
Data Structures
Python Naming Convention: C++ types like FAnimStateMachineInfo are exposed as AnimStateMachineInfo in Python (no F prefix).
AnimStateMachineInfo
| Property |
Type |
Description |
machine_name |
string |
Display name of the state machine |
node_id |
string |
Node GUID |
state_count |
int |
Number of states |
parentgraphname |
string |
Graph containing this machine |
AnimStateInfo
| Property |
Type |
Description |
state_name |
string |
Display name of the state |
node_id |
string |
Node GUID |
state_type |
string |
"State", "Conduit", "Entry" |
isendstate |
bool |
True if no outgoing transitions |
pos_x |
float |
X position in graph |
pos_y |
float |
Y position in graph |
AnimTransitionInfo
| Property |
Type |
Description |
transition_name |
string |
Display name |
node_id |
string |
Node GUID |
source_state |
string |
Source state name |
dest_state |
string |
Destination state name |
priority |
int |
Priority (lower = higher) |
blend_duration |
float |
Crossfade time in seconds |
is_automatic |
bool |
Auto-transition based on sequence |
rule_type |
string |
None (inert), Bool, Comparison, Automatic, Custom |
rule_variable |
string |
Bound variable name (for Bool/Comparison rules) |
rule_summary |
string |
Human-readable rule, e.g. Speed > 150, bIsDead == true |
has_rule |
bool |
False = inert (never fires). True = transition can fire |
AnimStateMachineValidationResult
Returned by validatestatemachine().
| Property |
Type |
Description |
is_valid |
bool |
True when there are no blocking errors |
state_count |
int |
Number of states |
transition_count |
int |
Number of transitions |
errors |
[string] |
Blocking problems (inert transitions, no entry state) |
warnings |
[string] |
Non-blocking issues (unreachable states, states with no animation) |
Driving bones directly — skeletal controls, no state machine
Not every AnimBP is a state machine. A spinning rotor, a windmill sail, a turret yaw, a rudder that follows a variable — all are a pose piped through Modify Bone nodes, which is a different graph shape and has its own traps.
The chain is always: a pose source → Local To Component → one Modify Bone per bone → Component To Local → Output. Skeletal controls only operate in component space, and connectanimnodes does not insert the space conversions for you.
import unreal
AG, BS = unreal.AnimGraphService, unreal.BlueprintService
PATH, G = "/Game/ABP_Windmill", "AnimGraph"
REF = "SPAWN AnimGraphNode_LocalRefPose|Local Space Ref Pose" # no sequence needed
L2C = "SPAWN AnimGraphNode_LocalToComponentSpace|Local To Component"
C2L = "SPAWN AnimGraphNode_ComponentToLocalSpace|Component To Local"
ref = BS.create_node_by_key(PATH, G, REF, -900, -80)
l2c = BS.create_node_by_key(PATH, G, L2C, -640, -80)
AG.connect_anim_nodes(PATH, G, ref, "Pose", l2c, "LocalPose")
mb = AG.add_modify_bone_node(PATH, G, "sails", -340, -80)
AG.connect_anim_nodes(PATH, G, l2c, "ComponentPose", mb, "ComponentPose")
getter = BS.create_node_by_key(PATH, G, "SPAWN K2Node_VariableGet|Get SailRotation", -400, 200)
BS.connect_nodes(PATH, G, getter, "SailRotation", mb, "Rotation")
c2l = BS.create_node_by_key(PATH, G, C2L, 0, -80)
AG.connect_anim_nodes(PATH, G, mb, "Pose", c2l, "ComponentPose")
AG.connect_to_output_pose(PATH, G, c2l, "Pose")
Pin names to remember: LocalPose in / ComponentPose out on Local To Component; ComponentPose in / Pose out on Modify Bone and Component To Local.
⚠️ Modify Bone ignores its Rotation pin until you set the mode
addmodifybonenode creates the node with rotationmode, translationmode and scalemode all set to Ignore. You can connect the Rotation pin, compile clean, and see absolutely nothing move — there is no warning. Set the mode on the node's inner node struct after creating it:
for i in range(0, 12):
o = unreal.find_object(None, f"{PATH}.{name}:{G}.AnimGraphNode_ModifyBone_{i}")
if not o:
continue
n = o.get_editor_property("node")
n.set_editor_property("rotation_mode", unreal.BoneModificationMode.BMM_ADDITIVE)
n.set_editor_property("rotation_space", unreal.BoneControlSpace.BCS_COMPONENT_SPACE)
n.set_editor_property("translation_mode", unreal.BoneModificationMode.BMM_IGNORE)
n.set_editor_property("scale_mode", unreal.BoneModificationMode.BMM_IGNORE)
o.set_editor_property("node", n) # write the struct back, or the change is lost
BMMADDITIVE and BMMREPLACE behave identically when the bone is at identity in the ref pose.
⚠️ Variable getter names differ between BP variables and C++ properties
createnodeby_key needs the spawner key, which embeds the node's display name, and the two kinds of variable are spelled differently:
| Variable lives on |
Display name |
Spawner key |
The AnimBP itself (addmembervariable) |
Get SailRotation |
`SPAWN K2Node_VariableGet\ |
Get SailRotation` |
A C++ UAnimInstance subclass |
Get Sail Rotation (spaced) |
`SPAWN K2Node_VariableGet\ |
Get Sail Rotation` |
Never hard-code either — discovernodes(PATH, "SailRotation", "", 20) and match on displayname, which works for both.
Continuous motion without an animation asset
Put the accumulator in the AnimBP's EventGraph on Event Blueprint Update Animation (its DeltaTimeX pin), write a float, and convert it to the Rotator the Modify Bone reads:
DeltaTimeX ─┐
├─ float * float ─ float * float ─┐ (RPM → deg/sec is × 6)
SpinRPM ────┘ 6.0 ─────────┤
├─ float + float ─→ SET SailAngle
SailAngle (get) ──────────────────────────────┘
SailAngle (get) ─→ MakeRotator(Roll=) ─→ SET SailRotation
Float maths nodes are FUNC KismetMathLibrary::MultiplyDoubleDouble and ::AddDoubleDouble (display names float * float / float + float) — Blueprint "float" is a double in UE5. MakeRotator is FUNC KismetMathLibrary::MakeRotator with pins Roll, Pitch, Yaw.
Verify it is actually running rather than trusting the compile: in PIE read the variable off the live instance, twice, in separate executepythoncode calls (see the pie-testing skill — Python blocks the game thread, so a sampling loop inside one call returns the same value every time).
comp = actor.get_components_by_class(unreal.SkeletalMeshComponent)[0]
ai = comp.get_anim_instance()
print(ai.get_editor_property("SailAngle"), comp.get_socket_rotation("sails").roll)
Common Patterns
Check if Asset is AnimBP
import unreal
if unreal.AnimGraphService.is_anim_blueprint("/Game/SomeAsset"):
# It's an AnimBP, safe to use AnimGraphService
machines = unreal.AnimGraphService.list_state_machines("/Game/SomeAsset")
Iterate All States Across All Machines
import unreal
abp_path = "/Game/ABP_Character"
machines = unreal.AnimGraphService.list_state_machines(abp_path)
for machine in machines:
states = unreal.AnimGraphService.list_states_in_machine(abp_path, machine.machine_name)
for state in states:
print(f"{machine.machine_name}/{state.state_name}")
Sample scripts (run via executepythoncode)
scripts/buildstatemachine.txt — add a state machine with two states, set state animations, add a transition, compile.