the symptom, in your words

"Attachments and bones read wrong on the spawn frame"

✓ verified on 26.07.22
lane Rigging & animationposted

▸ SYMPTOM

You attach something to a character in OnStart — a held item, a camera, a nameplate — and it lands nowhere useful, with no error in the console:

  • GetAttachmentObject(name) returns null for every name, even attachments that genuinely exist on the model.
  • TryGetBoneTransform succeeds but hands back a rest / T-pose transform (arms-out geometry), not wherever the current animation actually holds the limb.

Both failures are silent — no exception is thrown either way — so the mount just ends up at the origin or buried in the mesh.

▸ CAUSE

A SkinnedModelRenderer created this frame has not been processed yet:

  • Nothing has parented its attachment objects, so GetAttachmentObject finds none on the spawn frame regardless of whether the attachment exists on the model.
  • Nothing has driven the skeleton yet, so a bone read returns the bind pose (the rig's rest pose baked into the model), not the animated pose.

OnStart runs before the first pose evaluation, so any resolution done there reads pre-animation state.

▸ FIX

Defer resolution and retry across frames instead of resolving once in OnStart:

  • Try attachments first — the engine parents and drives them for free once it has processed the renderer.
  • Fall back to bones if no attachment matches.
  • If nothing resolves inside a timeout, settle on the model root and log which names were tried, so a renamed or missing attachment degrades to a visible, diagnosable fallback instead of a silent null.

Once a mount lands on a bone (not an attachment), re-pin its world transform in OnPreRender every frame:

snippet
protected override void OnPreRender()
{
    if ( renderer.TryGetBoneTransform( boneName, out var tx ) )
        mount.WorldTransform = tx;
}

OnPreRender runs after the pose evaluates, so the mount tracks the pose that will actually be drawn — not the bind pose or a stale frame.

Seeding an offset relative to a rig you haven't inspected

Never hard-code a bone-local offset. Bone-local axes belong to the model and differ per bone and per rig, so the same constant buries the object in one rig and flings it away in the next. Seed in a frame you control instead:

snippet
go.WorldPosition = mount.WorldPosition + characterRotation * seedOffset;

Using the character's frame (forward / left / up) lands the object somewhere visible regardless of the bone's local axes. Then read the resulting local transform back (go.LocalPosition / LocalRotation / LocalScale) and use that as your authored baseline for tuning and reset — don't try to author the local offset by hand. This only works from a frame the caller owns; seeding before the mount resolves reads a stale or root-fallback position and bakes the wrong baseline.

▸ WHY IT WORKS

Attachment parenting and skeleton animation both happen after component start, so anything read in OnStart predates them. Retrying with a timeout waits for the frame where the data becomes real, and re-pinning bone-driven mounts in OnPreRender reads the pose at the one point in the frame where it is final. Seeding through the character's own frame sidesteps the per-rig unknowns of bone-local space entirely, then captures the correct local baseline from a transform the engine computed for you.

Verified on engine 26.07.22: seen in a real project.
s&box moves fast; an undated fix is a liability. Spot a stale detail?
changelog
  • Published

Want to know when new guides or fixes drop? Join the community to help build this out. Report gotchas, flag outdated fixes, or just lurk.

Join the Discord