Animation trees (.atr)¶
What a tree does¶
A tree gives scripts stable animation names. A script writes %run, and the
compiler resolves run in the tree selected by #using_animtree. The
reference becomes a packed animation value containing the tree registry index
and the node index.
A tree also groups animations:
a parent can group a body region, a weapon set, or a pose family;
a parent can declare that its children play in sync;
a parent can force its descendants to survive tree pruning.
The tree is not a state machine. It has no transitions, conditions, or timers. The player animation script and the AI scripts add those layers.
File location and selection¶
The loader forms the path animtrees/<name>.atr. The name comes from a
#using_animtree directive:
#using_animtree("generic_human");
The name must be a single identifier. The stock parser accepts letters, digits, and underscores, and rejects a slash, dot, or hyphen in this position. The loader canonicalizes the name like a script filename, so case is not significant.
Tree selection is compiler state. Each new source file starts without a
selected tree. A #using_animtree directive selects a tree for the
remainder of that file. A later directive in the same file can select another
tree, and references after it resolve against the new tree. A % literal or
an #animtree expression before any directive is a compile error.
The engine also loads trees directly by name. For example, the player
animation layer looks up the multiplayer tree, and a turret script can load
a narrow tree such as mg42 or 30cal.
Syntax¶
The tree file contains a sequence of nodes. It has no outer pair of braces.
tree = { node } ;
node = name
| name "{" { node } "}"
| name ":" properties "{" { node } "}" ;
properties = property { property } ;
property = "loopsync" | "nonloopsync" | "complete" ;
A leaf is a bare name:
walk
run
strafe_left
A parent is a name followed by a braced body:
main
{
torso
{
stand_shoot
stand_idle
}
legs
{
walk_loop
run_loop
}
}
A parent with properties uses a colon before the body:
aim : nonloopsync
{
aim_left
aim_center
aim_right
}
The colon must be a separate token, and the property list runs to the end of the line. The opening and closing braces must each be followed only by the end of the line. These rules make the format line-oriented even though it uses braces.
Names are identifiers. A duplicate name in one parent is a parse error:
duplicate animation
A name may appear under different parents because each parent has its own name
table. The parser accepts that, but the tree resolver binds a name once per
tree. If a script references a name that occurs more than once, the first node
binds the reference and a later node reports duplicate animation '%s' in
'animtrees/%s.atr' during tree build. Keep names unique across a tree that
scripts reference by name.
Node kinds¶
A leaf is an animation asset reference. The builder resolves it with
FS_GetDataForFile("xanim", name, ""). If the asset is missing, the load
fails with:
Cannot find 'xanim/%s'
A parent is a group. It may contain leaves, other parents, or both. The runtime keeps a child count and a first-child index for each parent. The root of the compact tree is a synthetic node at index 0; the file’s top-level nodes are its children.
A parent is not an animation asset. It has no leaf asset, and
XAnimGetLength is not valid for it. A sync parent still reports time
through XAnimHasTime. XAnimGetAnimName returns <non-leaf anim> for
a parent that is asked for an animation name.
The parser also accepts an alias form at the syntax level, but the stock implementation rejects it:
FIXME: aliases not yet implemented
Do not use aliases in stock content.
Node properties¶
Property |
Effect |
Applies to |
|---|---|---|
|
Marks the parent as a synchronous group whose leaves loop. The parent becomes a sync node and its children are played in phase. |
Parent with a body. |
|
Marks the parent as a synchronous group whose leaves do not loop. The parent becomes a sync node and its children are played in phase. |
Parent with a body. |
|
Keeps the parent’s whole subtree when the parent is referenced by a script. It has no effect on playback. |
Parent with a body. |
A property on a leaf is an error:
properties cannot be applied to primitive animations
Properties combine on one line:
proneMG42_fire : complete loopsync
{
proneMG42_fire_15down
{
// five body poses
}
proneMG42_fire_level
{
// ...
}
proneMG42_fire_15up
{
// ...
}
}
complete and the sync bits are separate flags. complete is bit 3,
loopsync is bit 0, and nonloopsync is bit 1 in the recovered property
encoding.
Do not put both sync properties on one parent. The parser records both bits, and the sync setup rejects the node as both looping and non-looping.
Sync groups¶
A sync group makes several animations share a playback phase. The classic use is a set of aim or fire poses that differ by direction but must switch without drifting out of step.
The sync rules are enforced when the compact tree is built:
every leaf under a
loopsyncparent must itself be a looping asset;every leaf under a
nonloopsyncparent must be a non-looping asset;a sync group may not contain another sync group;
a sync node may not be declared twice in the same subtree.
A mismatch is fatal. The stock diagnostic is:
animation cannot be sync looping and sync nonlooping
A nested sync group is also fatal, reported as a duplicate sync specification.
The practical rule is to keep looping and non-looping variants in separate
parents. generic_human.atr contains a comment that a set of animations was
moved into a separate group because a single sync group mixed the two.
Pruning and placeholders¶
The stock loader does not keep every node in the file. It keeps the nodes that scripts actually reference, plus the parts of the tree those references require.
The sequence is:
The script compiler records each
%namereference against the selected tree.When the tree is loaded, the parser marks each node whose name is not in that reference set as
missingFromTree.The builder removes marked nodes unless an ancestor forces them to stay.
completeon a referenced parent forces the whole subtree to stay.A referenced parent whose children were all removed receives a synthetic
voidchild. A referencedloopsyncparent receivesvoid_loop.An empty root tree also receives
void.
This is why a sync group often carries complete. A script may reference
only one aim direction, but the sync group needs its siblings to keep phase.
Without complete, the unreferenced siblings can be pruned and the group
loses the set it was built to synchronize.
The behavior is controlled by com_animCheck. The stock default is 0.
Setting it to a nonzero value keeps every node, including nodes no script
references. Use it to inspect a full tree or to expose a missing xanim
asset that pruning would otherwise hide. Do not ship content that depends on
the debugging value.
Runtime representation¶
The compact tree is an XAnim:
typedef struct {
const char *name;
uint32_t nodeCount;
XAnimEntry entries[];
} XAnim;
Each XAnimEntry is either a leaf or a parent:
a leaf has a resolved animation asset pointer;
a parent has a child count, a first-child index, and property flags;
every entry has a parent index.
The root is index 0. The first child of the root is index 1. A parent’s
children occupy a contiguous range starting at firstChildIndex.
The builder walks the parsed tree, stamps parent indexes, resolves leaves, and then runs the sync setup over the result. The runtime queries are small:
Query |
Result |
|---|---|
|
True for a leaf, false for a parent. |
|
True for a leaf or for a parent with a sync property. |
|
True for a looping leaf asset or a looping sync parent. |
|
The leaf asset name, or |
|
The leaf asset length in seconds. Valid only for a primitive node;
the function reads |
|
The child count for a parent. |
The tree itself stores no frame data. Every leaf holds a reference to the
loaded xanim asset.
Script integration¶
A script selects a tree with a directive before its first animation reference:
#using_animtree("generic_human");
It then writes animation literals with %:
self setAnimKnob(%stand_idle, 1, 0.2);
The compiler resolves the name in the tree selected at that point in the file
and emits a packed scr_anim_t value. The packing is:
(treeIndex << 16) | nodeIndex
The low 16 bits hold the node index. The high 16 bits hold the tree registry
index. The registry index is nonzero; zero is reserved. This makes a
reference to the root of the first registered tree pack as 0x00010000.
#animtree emits the current tree’s integer registry index. AI turret
scripts use it to switch an entity to a narrow tree:
self UseAnimTree(#animtree);
The runtime keeps a table of loaded trees. Scr_FindAnimTree returns the
loaded XAnim for a name, or NULL. Scr_FindAnim records a reference
to a name in a named tree. The runtime reports a missing name when it builds
the tree. The stock diagnostics are:
animation '%s' not defined in anim tree '%s'
duplicate animation '%s' in 'animtrees/%s.atr'
The first is a name that no node in the tree defines.
Scr_CheckAnimsDefined raises it after Scr_CreateAnimationTree finishes.
The second is a name that more than one node defines and a script references:
the first node binds the reference, and a later node finds it already bound.
Limits¶
Limit |
Value and consequence |
|---|---|
Tree name length |
49 bytes. The path buffer is |
Tree registry |
127 registered names per active registration slot. The runtime keeps
two slots. Overflow reports
|
Node pool |
|
Node name |
A node name must fit the shared identifier and path limits. A leaf
name is also an |
Property line |
The property list must fit on one line. The parser does not continue a property list across lines. |
A tree is loaded once and reused by its registry index. Changing the file while the game is running does not update an already loaded tree.
Diagnostics¶
Stage |
Message |
Meaning |
|---|---|---|
Parse |
|
The same name appears twice in one parent. |
Parse |
|
A leaf has a colon property list. |
Parse |
|
The file uses the unsupported alias form. |
Build |
|
A leaf names an asset that is not present. |
Build |
|
A leaf under a |
Build |
|
A leaf under a |
Build |
|
The parent declares both sync properties. |
Build |
|
A sync group is nested inside another sync group. The message names the tree, the depth of the inner node, and the leaf it reached. |
Build |
|
A |
Build |
|
A |
Script compile |
|
The |
Script compile |
|
A |
Script compile |
|
A |
Load |
|
The tree name does not fit the |
Load |
|
The tree registry is full. |
Examples¶
Minimal root leaves¶
stalingrad_runner.atr is a single root-level leaf:
stalingrad_flagrunner_idle
This tree has no parent groups. A script can reference the leaf by name.
Named groups¶
bomber_anim.atr uses root-level groups for crew roles:
pilot_anims
{
// ...
}
copilot_anims
{
// ...
}
wirelessop_anims
{
// ...
}
The group names are organizational. Scripts reference the leaves inside them, not the group path.
Sync group¶
trenches_truck_dummies.atr contains:
walk_run : loopsync
{
// looping walk and run leaves
}
generic_human.atr contains both loop modes for the same pose family:
stand_shoot : nonloopsync
{
// ...
}
stand_shoot_auto : loopsync
{
// ...
}
The two groups are separate because the leaves have different loop states.
Complete sync group¶
multiplayer.atr contains:
proneMG42_fire : complete loopsync
{
proneMG42_fire_15down
{
// five body poses
}
proneMG42_fire_level
{
// ...
}
proneMG42_fire_15up
{
// ...
}
}
complete keeps the three fire pose groups when the parent is referenced.
The sync property keeps their leaves in phase.
Empty group¶
generic_human.atr contains an empty group:
testmap_testing
{
}
An empty group on its own is valid syntax. If a script references it, the
loader inserts a void child so the parent is not empty. Empty groups that
no script references are pruned.
Implementation basis¶
Source |
Role |
|---|---|
|
Tree parser, property flags, registry, pruning, script reference collection. |
|
Animation tree and reference types. |
|
Compact tree builder, leaf resolution, parent indexes, sync setup. |
|
Primitive, time, loop, length, child, and name queries. |
|
|
|
|
|
|
|
|
|
Shared comment and token handling. |
|
Client tree load entry. |
|
Game-side tree lookup. |
|
Stock sync, |
|
Stock player tree with |
|
Stock named root groups. |
|
Stock |
|
Stock single-leaf tree. |
Comments¶
Both comment forms from the shared parser are accepted:
Stock files use both forms.
generic_human.atrcontains a block comment around a disabled sync group.