Skip to content

Scene-driven movement animations#723

Merged
robtfm merged 10 commits into
mainfrom
feat/scene-driven-movement-animations
Apr 22, 2026
Merged

Scene-driven movement animations#723
robtfm merged 10 commits into
mainfrom
feat/scene-driven-movement-animations

Conversation

@robtfm

@robtfm robtfm commented Apr 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Routes avatar movement animation selection through the movement scene via a nested MovementAnimation block on PbAvatarMovement, resolved through the existing emote loader pipeline. The engine still picks from velocity heuristics when the scene hasn't supplied one.

Protocol dependency: this PR depends on decentraland/protocol#392. Once that lands and the protocol-squad artifact is cut, we'll bump dcl-protocol here.

Pipeline

  • New MovementAnimation nested on PbAvatarMovement (scene → engine): src, loop, speed, idle, optional transition_seconds, optional playback_time seek.
  • New AvatarAnimationState on PbAvatarMovementInfo (engine → scenes): mirrors the currently-playing scene-driven clip plus playback_time, duration, loop_count. Any scene can read it.
  • A single SceneDrivenAnim component covers both primary and foreign players.
  • Scene-emote URNs resolve relative to the scene's baseUrl via a new IpfsType::SceneContent, fixing asset 404s for portables whose content lives outside the realm.

Interaction with triggered emotes

  • idle: true lets triggerSceneEmote take over; idle: false suppresses emotes while the movement animation plays.
  • A non-idle scene-driven request clears any matching triggered emote so the movement scene can seize control mid-play.
  • AvatarAnimationState.playback_time freezes while a triggered emote is overriding the clip.

Remote sync

  • Latch single-frame playback_time seeks in broadcast_position so they aren't dropped between 10Hz/1Hz send gates.
  • Bypass the STATIC_FREQ skip when the active URN changes so jump→idle transitions reach remotes promptly when the player comes to rest.
  • Defer SceneDrivenAnim insertion on foreign avatars until the interpolated position catches up to the target, so the clip change visibly lines up with the motion rather than with the packet arrival.
  • Flush any unapplied scene_anim before overwriting the target position so bursts of events don't silently drop one-shot seeks or intermediate transitions.

Resilience

  • Precompute the velocity-based ActiveEmote and stash it as a fallback; on permanent URN resolution failure (parse/missing clip/Failed/Missing/NoRepresentation/no _Avatar) the engine swaps to the fallback and retries.

Unrelated (bundled)

  • Emote prop scene: spawn an invisible wrapper entity between the avatar and the prop scene so instantiated scene entities inherit Hidden the moment they appear. Avoids a one-frame flash of _reference / _basemesh / m_mask_* children.

🤖 Generated with Claude Code

robtfm and others added 7 commits April 20, 2026 11:02
Route avatar movement animations through the movement scene via a
nested MovementAnimation block on PbAvatarMovement, resolved through
the existing emote loader pipeline.

Adds an IpfsType::SceneContent variant so scene-emote URNs resolve
relative to the scene's baseUrl rather than the realm content URL,
fixing asset 404s for portables where scene content lives outside
the realm.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Spawn an invisible wrapper entity between the avatar and the prop
scene, so instantiated scene entities inherit Hidden the moment they
appear. Flip the wrapper to Inherited on the same pass that marks
name-matched props Hidden explicitly, avoiding the one-frame flash of
_reference / _basemesh / m_mask_* children that should stay hidden.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When a triggered emote is the current active emote, a new non-idle
MovementAnimation would be ignored because branch 1 of the animation
select always wins while a matching EmoteCommand is still present. Add
a scene_cancels arm in the same-URN cancel block so a non-idle
scene-driven request clears requested_emote, allowing the movement
scene to seize control.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Collapse the primary-player resource and foreign-player component into a
single SceneDrivenAnim component used for both. Latch single-frame seeks
in broadcast_position so they aren't dropped between 10Hz/1Hz send gates.
Defer remote SceneDrivenAnim insertion until the interpolated position
catches up with the target, so animation transitions line up with the
visible motion rather than the packet arrival.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Precompute the velocity-based ActiveEmote in animate and stash it as
`fallback` on scene-driven selections. When play_current_emote hits a
permanent resolution failure (URN parse, missing gltf animation, Failed /
Missing / NoRepresentation, or no _Avatar clip), swap to the fallback and
retry once. Loading still skips the frame as before.

In update_foreign_user_target_position, flush any unapplied scene_anim
before overwriting PlayerTargetPosition so bursts of events (stalls or
multi-event frames) don't silently drop one-shot seeks or intermediate
transitions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When a foreign player's velocity becomes zero (e.g. landing from a jump)
the STATIC_FREQ skip held back the next broadcast for up to a second, so
remotes kept the outgoing jump clip visible until the keepalive fired.
Bypass the skip when the active URN changed so transitions reach remotes
immediately.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Avoids resending the ~50-char `urn:decentraland:off-chain:scene-emote:…-false`
preamble on every broadcast. Receivers reconstruct the URN from the hash pair.
robtfm and others added 3 commits April 21, 2026 11:16
Route scene-published content-hash sound triggers through the broadcast
into a per-frame avatar-bus play system. Adds a custom `.audio`
AssetLoader that wraps kira's symphonia-backed static sound loader so
audio content can be fetched by content hash without knowing the
container format up front. Includes a retry-filter fix for the wasm
audio path so AudioEmitters whose buffer isn't decoded on the spawn
frame get a chance to start once the buffer lands.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
bevy_animation::AnimationNodeActive::replay() resets `seek_time` to 0.0
(alongside clearing `completions` / `just_completed`), so calling it after
`seek_to` wiped the requested position. The visible symptom was that any
scene-driven MovementAnimation with a non-None `playbackTime` snapped the
clip back to frame 0 every tick. Swap the order so replay clears the
completion state first, then seek_to positions the cursor.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Upstream rfc4/comms.proto already assigns Movement tags 19–28 (head-sync,
glide, jump_count, point-at) and MovementCompressed tags 3–4 (head_sync_data,
point_at_data). Our local anim_* extensions were colliding with those, so a
Movement from an upstream-compliant sender would have its head_ik_yaw_enabled
bool decoded as our anim_scene_hash string (wire-type mismatch, garbage
value).

Renumber the local additions to Movement.29–35 and MovementCompressed.5–11,
both well beyond any current upstream usage, and note the reservation in
a comment so future additions skip the reserved range.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@robtfm robtfm merged commit f1ed332 into main Apr 22, 2026
9 checks passed
@charly-bg charly-bg deleted the feat/scene-driven-movement-animations branch June 4, 2026 18:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant