Skip to main content

Character animation

New here? Read the overview first

How DyingStar networking works explains the multiplayer basics in plain language, and Player network management covers how the player replicates. This page is about the animated body that sits on top of that.

Every player now has an animated body: you see your own arms and legs in first person, and other players walk, run, jump, crouch, sit, carry and emote in front of you. The character model is rigged on the Quaternius Universal Animation Library (UAL) skeleton, and plays a copy of that library converted for its own body (see Adding a character model).

Three players showing the range: one crouched and one prone — each with a mining tool holstered on the belt — and one standing in a fold-arms idle, next to a carriable box

The one idea to remember: animation is derived, not sent​

No animation data ever travels the network. We never send "play the walk clip" or a bone pose. Instead, every client re-derives the animation from state that is already replicated for other reasons:

  • walking / running → from the body's own position change (speed + direction),
  • jump, landing, emote, sit, interact → from the whitelisted action event field,
  • carrying → from the carrying property,
  • crouch / prone → from the stance property,
  • where the head looks → from head / head_yaw.

The same controller runs on your body and on every remote avatar, because both sides read the same already-present state. This keeps the wire cheap (only one small property was added for the whole feature — the seated look yaw) and it means a new animation usually costs zero networking.

Why this matters

If you add a gameplay state that should show as an animation, ask first: is this already replicated? If yes, the animation is free — just read it. Only add a new replicated property when the animation truly cannot be derived from existing state (see Player).

The pieces​

Script / resourceRole
scenes/_universe/characters/humanoids/human_puppet.tscnThe visible body: the character model, plus an AnimationPlayer holding the animation library converted for it. Child of the player body.
addons/dyingstar/build_retargeted_animations.gdEditor script that converts the animation library for a given character model. Run once per model (see below).
character_animator.gd (class_name CharacterAnimator)The controller. One node, same code for the owner and remotes (DRY). Picks and cross-fades the clip each frame from the derived state.
character_animation_set.gd (class_name CharacterAnimationSet)A Resource mapping each logical state (idle, walk, jog, crouch, jump, sit…) to a clip name. Data only — one set per model, so a different rig is retargeted by swapping the set, not the code.
scenes/props/emote_catalog.gd (EmoteCatalog)The emote-wheel layout (which clip each emote plays).

The animator reads a small locomotion sample (smoothed speed, direction, airborne, stance, carrying, yaw-rate…) that PlayerClient computes once per frame and caches on the body, so the footsteps and the animation share one computation.

Clip names lose their _Loop

Godot's glTF importer strips a trailing _Loop from clip names (and marks them looping). So the source clip Idle_Loop becomes Idle — the names in the CharacterAnimationSet are the source names without _Loop.

Where the clip names live (the @export fields)​

The clip vocabulary is the resource scenes/_universe/characters/humanoids/ual_animation_set.tres (a CharacterAnimationSet). Open it in the Godot Inspector and you get one @export field per state, grouped by family (Idle · Walk · Run · Sprint · EVA · Crouch · Prone · Turn · Jump · Climb · Carry · Sit · Emote). Each field holds the name of the clip to play for that state:

Family (group)Example fields → clip name
Walkwalk_fwd → "Walk", walk_bwd → "Walk_Bwd", walk_left/right, diagonals
Runrun_fwd → "Jog_Fwd", run_bwd, run_left/right, diagonals
Crouch / Pronecrouch_idle → "Crouch_Idle", crouch_fwd…, prone_idle → "Crawl_Idle"…
Jumpjump_start, jump_loop, jump_land
Sitsit_driving → "Driving", sit_passenger → "Sitting_Nodding"
Emoteemote_dance, emote_celebration, … (referenced by EmoteCatalog)

An empty field means "this model doesn't have that clip" → the animator falls back to a sensible one (nearest direction → forward → idle). The set is assigned to the CharacterAnimator node's Anim Set slot in human_puppet.tscn. So pointing a state at a different clip is just editing a text field in the Inspector — no code, and a different model gets its own set, same controller.

What drives what​

You seeDerived fromNotes
Idle / walk / jogBody position delta → speed + 8-way directionMouse-wheel walk speed (0.5–3) also warps the walk playback so the feet don't slide. Sprint (Shift) is a fast tier.
In-place turnBody yaw-rate while standing stillPlays Turn90_L/R.
Jump start / loop / landaction = "jump:<n>" and "land:<n>" eventsSee the event-field idiom.
Crouch / prone gaitstance (0/1/2)Directional crouch / crawl clips, with enter/exit transitions.
Carryingcarrying (bool)You can only carry standing (see Player).
Sit / drive / passengerseat:/unseat: events, and the seat state for a player who arrives laterSee Vehicles.
Emoteaction = "emote:<key>:<n>"Chosen from the emote wheel (T).
Head aimhead (pitch) + head_yaw (seated)See below.

First person: your own body, without the seasickness​

For the local player the puppet is the visible body (arms, legs, tool). Four things keep it comfortable, and each of them exists because its absence was visible:

Your own head and neck are hidden​

Both bones are scaled to almost nothing for the owner. Remote players keep theirs, of course.

Hiding the head alone is not enough

The neck is a separate bone. With only the head collapsed, looking straight ahead was fine and looking up, left or right swept the view through your own throat. No camera height fixes that — the geometry is around the eye, not below it.

Collapsing the neck drags the head bone down with it, and that is harmless here: the camera is anchored to CameraPivot in the scene and takes only a delta from the head bone, measured against a rest captured with the same collapse applied, so a constant shift cancels on both sides. The arms hang off the spine, not the neck.

The hidden head took its shadow with it: your own shadow had no head. A stand-in casts it (CharacterAnimator.add_head_shadow): a capsule the size of a head, drawn in shadows only (SHADOW_CASTING_SETTING_SHADOWS_ONLY), carried by the bone above the neck, on the own-body render layer so your own torch ignores it.

Seated or floating, the camera stands at its rest plus the seated eye height. That rest is taken once, from where the scene puts the pivot (HeadCam.capture_base): taken again every frame, as it was until the player had stood once, it climbed the eye height each frame, and a player who reconnected in EVA watched their body from kilometres up.

The eye is the centre of rotation​

Camera3D sits at CameraPivot, not offset from it, and the eye is nudged a little forward of the head bone (head_cam_forward) where a real one is. The camera used to hang 6.3 cm above and 8.5 cm behind the pivot, so looking around swung it on a 10 cm arm and the viewpoint translated — plainly visible at the wheel, where the dashboard gives you a fixed reference to see it against.

No head bob: the camera holds the posture's eye​

There is no head bob in the game, and no setting to bring it back. The first-person camera still takes its position from the head bone (HeadCam, scenes/_universe/characters/humanoids/head_cam.gd), because that one movement carries what the view must keep — but it is split by what it is:

What it isHow it is handled
Bounce & swayup/down and side to side, a few times a secondNever followed
Posturestanding, crouched, proneThe camera holds that stance's eye point, taken once from the head the first time the stance is held still
One-shot movesa stance transition, a vaultFollowed in full: the view goes down as you crouch, up over the ledge
Leanforward, as the torso pitches over into a jog or a sprintFollowed in full while moving — or you look at your own neck

Standing still, turning on the spot, playing an emote or reading a 3D screen, nothing moves the view: the eye point is fixed, so idle breathing and the turn clip move the body under a still camera. There is nothing to tune per clip either — a new animation set brings its own eye points. The two settings left under Head camera (first person) are head_cam_smooth (how fast the camera reaches its target, which is also how fast the view drops when you crouch) and head_cam_forward (above).

Why not a low-pass filter on the head

A filter was tried. Slow enough to ignore the stride, it is also slow enough to lag a whole gait change, and the neck showed for the half second it took to catch up. What tells the bounce from the posture is not its speed, it is what the character is doing: walking, still, or in a one-shot move.

Three heights, three settings​

They used to be one number, so tuning any of them broke the others:

SettingWhat it moves
Standing eyeCameraPivot.position.y (in player.tscn)the camera, on foot
Seated bodyseat_body_drop (Player)the model on its seat
Seated eyeseat_eye_height (Player)the camera, at the wheel

_ride_seat used to derive the seated body position from the standing eye height, so lowering the pivot 3.7 cm to fix the on-foot view raised the seated body by exactly that and the driver floated. And seat_eye_height was applied to the body origin too — camera and body moved rigidly together, so no value ever changed what you saw.

seat_body_drop is a full offset, not a height

All three components matter. Its -0.17 on Z is the pivot's forward set-back inside the skull; reducing it to a height alone slid every seated body 17 cm.

The seat ride owns the camera while you are seated.

The torch on your head, and your own shoulders​

The torch is put on the head bone at start-up, on every avatar, so that its beam follows the animated body. Its place is torch_head_offset on the Player node (metres, in body axes, forward is -Z). The Torch node's own transform in player.tscn does nothing: the mount overwrites it.

Sitting there, the torch is behind your shoulders, and they would cut the bottom of its beam. Your own body is therefore drawn on a render layer of its own (Globals.RENDER_MASK_OWN_BODY, layer 19), and your torch leaves that layer out of its shadows (shadow_caster_mask). The body is on that layer alone: a mesh casts for a light as soon as one of its layers is in the light's shadow mask. The sun and the moons, which only light layer 1, light layer 19 too; every other light, another player's torch included, still casts your shadow.

Head-look​

The head bone tilts to follow where the player aims, so others can read your gaze:

  • Standing — only the pitch (up/down) is needed on the head, because the body already turns with the yaw. Pitch is the existing head property.
  • Seated — the body is locked in the seat, so the head follows both yaw and pitch. The seated yaw is the one small property added for this whole feature: head_yaw (0 while standing).

Both are clamped so the neck never snaps to an impossible angle.

Emote wheel​

Hold T to open a radial menu of emotes (dance, celebration, sit on the ground, rock-paper-scissors…). Picking one sends the emote to the server, which validates it and replicates it as an action = "emote:<key>:<n>" event, so every player sees it play.

The emote wheel open (hold T) — Dance, Celebration, Crying, Drink, Sit, Game, Yes/No, Rage, Lay down, Consume, Surprise, Zombie, T-Pose — with a remote player mid-emote in the background The emote clips live in the CharacterAnimationSet; EmoteCatalog only says which set field each wheel entry uses.

Adding a character model​

The controller works on clip names, not on a specific model, so a new character is mostly art. But the shared animation library has to be converted for that body first, and that step is not optional.

Why a conversion is needed​

Two rigs can carry the exact same bone names and still not play the same animation. An animation stores each bone's rotation as an absolute value, not as an offset from the rest pose — so a clip puts the character's bones wherever the library's bones were, not where the equivalent movement would put them.

That is fine as long as both rigs rest the same way. They usually do not: the library rests in a T-pose (arms straight out), while a character delivered by an artist normally rests in an A-pose (arms down at about 45 degrees). Play one on the other and every arm is off by that gap: elbows bend the wrong way, the head rolls instead of turning.

Do not fix this in the Import dock

Godot's import-time rest fixer can force the two rest poses to match, and it is tempting because it is one checkbox. It does it by moving the character's bones — but the mesh is skinned to its own rest, so every bone it straightens deforms the body. Expect a belly pushed forward, toes lifting off the ground, and a lump at the shoulder. This was tried and measured; the settings only move the deformation from one bone to another.

The steps​

  1. Rig the model on the UAL skeleton — Unreal-style bone names (pelvis, spine_01, upperarm_l, head…). You do not have to match the library's rest pose or its proportions; only the bone names and the hierarchy have to line up.
  2. Import both glTF with retargeting on. Select the .glb in the FileSystem, Import dock, Advanced…, pick the Skeleton3D node, and under Retarget assign a Bone Map using the SkeletonProfileHumanoid profile. Do this on the character and on UALDyingStar.glb. Leave every Rest Fixer option off (see the warning above) — the bone map is only there to give both rigs the same bone names.
  3. Convert the library. Open addons/dyingstar/build_retargeted_animations.gd, point its TARGET_SCENE at your model, and run it (File ▸ Run, or Ctrl+Shift+X). It writes a <model>_animations.res next to the model. Re-run it whenever either glTF changes — the file is generated output, never edited by hand.
  4. Wire the puppet. In human_puppet.tscn, instance the model, add an AnimationPlayer whose libraries holds that .res and whose root_node points at the model, and assign a CharacterAnimationSet on the CharacterAnimator.

Because the whole system reads replicated state, the new model animates the same way in first person and for remote avatars, with no code and no extra networking.

What the conversion does and does not carry​

Carried overLeft to the character
Every bone rotation, as the turn away from the rest pose measured in world space — the one quantity that means the same thing on two bodies of different shapeBone lengths: a shorter forearm stays shorter. Only the hip height is scaled, so a taller character does not sink into the floor
The collar bones, neck and head keep the character's own rest direction — those rest the same way in both rigs and differ only in buildFingers are not animated at all (see below)
Fingers stay at rest

Finger motion does not survive a change of hand: replayed on another rig it closes past the anatomical stop until the fingertips sink into the palm. The converter therefore leaves fingers at the character's own rest, which reads as a relaxed hand. There is no finger-level gameplay today. When a grip pose is needed, author it on the character rather than importing one — drop FROZEN_GROUPS in the script and the finger tracks come back.

The armature node name is handled for you

An animation track finds its bone by node path, so the model's armature node has to carry the name the library addresses. Blender names it after whatever the artist called the object (test1, character_v3…). A post-import script (addons/dyingstar/post_import_armature_alias.gd) renames it, so you do not have to think about it — just leave that script in the model's Import Script field.

When you export the model (or new animations) from Blender to glTF, match the Quaternius kit's export settings — most importantly Animation mode = Actions, +Y Up, Use Rest Position Armature, and Force keeping channels for bones (so every bone track survives):

Blender glTF (.glb) export settings used for the Quaternius kit: Animation mode &quot;Actions&quot;, +Y Up, Use Rest Position Armature, Force keeping channels for bones

One rig, many bodies

Creators only rig a model on the shared skeleton; the code brings the animation library to it. Keep every humanoid on the same UAL bone names, run the converter once, and everything downstream — locomotion, stances, head-look, the belt holster, the head-mounted torch — keeps working for free.