Files
open-wc/docs/modules/m2-animation-playback-controller.md

11 KiB

M2 Animation Playback Controller

Metadata

Field Value
Status Implemented extraction
Target/work package M03 / M03-RND-M2-ANIMATION-PLAYBACK-001
Owners Per-instance AnimationPlayer/native animator playback mutation
Last verified Worktree work/sindo-main-codex-m03-integrator/m03-closeout, 2026-08-02
Profiles/capabilities Imported GLB and native experimental animated M2 instances

Purpose

Apply deterministic playback to one already duplicated M2 instance: copy native animator runtime fields, derive a stable phase, prepare/phase native animators, select an imported animation and configure linear looping/playback/seek.

Non-goals

  • Duplicate, name, transform or attach instances/batch roots.
  • Apply visibility, shadows or Editor ownership.
  • Load/finalize/cache animated scenes or decide eligibility.
  • Change animation priorities, phase formula or native animator implementation.

Context and boundaries

flowchart LR
    Prototype[Animated prototype] --> Materializer[M2AnimatedInstanceMaterializer]
    Materializer -->|source and duplicate| Playback[M2AnimationPlaybackController]
    Finalizer[M2AnimatedSceneFinalizer player inventory] --> Playback
    Playback --> Native[M2NativeAnimator mutation]
    Playback --> Imported[AnimationPlayer mutation]

Allowed dependencies are supplied Nodes, Script identity, AnimationPlayer and Animation resources. ResourceLoader, files, workers, caches, scheduler, MultiMesh, SceneTree attachment and application layers are forbidden.

Public API

Symbol Kind Purpose Thread/lifetime Errors
copy_native_animator_data(source_root, target_root, script) Command Copy five runtime fields in depth-first pair order Main thread; borrowed nodes Null/script mismatch yields no pairs
start_instance_playback(root, path, index, script, players, collect_diagnostics) Command/query Start native/imported playback and optionally return native state Main thread; one instance Null/empty inventories no-op
native_animators_in_subtree(root, script) Query Exact-script depth-first inventory Main thread; borrowed nodes Null root/script returns empty
phase_for_instance(path, index) Pure query Stable phase in [0, 1) Any thread; scalar Historical hash behavior retained
choose_default_animation(player, path) Pure engine query Apply exact ordinary/fish/bird/fallback priority Main thread; borrowed player Null/no animations returns empty

Inputs and outputs

Direction Contract/data Producer Consumer Ownership Thread/lifetime
Input Prototype and duplicate subtrees M2 animated instance materializer Native field copy Borrowed Nodes One duplicate
Input Exact native animator Script Loader composition Native inventory Borrowed Script Call-local
Input Relative path and instance index Build batch adapter Phase/selection Copied values One start
Input AnimationPlayer inventory Animated scene finalizer traversal Imported playback Borrowed players One start
Output Mutated native/imported playback Controller Rendered instance Nodes retain state/resources Instance lifetime
Output Detached native diagnostic records Controller Loader log adapter Caller-owned Dictionaries Debug call

Side effects are native field assignment, phased preparation calls, animation loop mutation, play and seek. The service retains no inputs.

Data flow

flowchart TD
    Identity[Path and index] --> Phase[Stable hash phase]
    NativeInventory[Exact-script native inventory] --> Prepare[prepare local runtime mesh]
    Phase --> Prepare
    Players[AnimationPlayers] --> Select[Choose path-specific default]
    Select --> Loop[Set every animation LOOP_LINEAR]
    Loop --> Play[Play selected name]
    Phase --> Seek[Seek positive-length selection]
    Prepare --> Diagnostics{Debug requested?}
    Diagnostics -->|yes| Snapshot[Detached runtime state]

Lifecycle/state

stateDiagram-v2
    [*] --> Unstarted
    Unstarted --> Prepared: copy native data and start
    Prepared --> Playing: native phase and/or AnimationPlayer play
    Playing --> Playing: repeated deterministic start
    Playing --> [*]: instance owner releases subtree

The controller itself is stateless; lifecycle labels describe borrowed instance state.

Main sequence

sequenceDiagram
    participant M as M2AnimatedInstanceMaterializer
    participant F as M2AnimatedSceneFinalizer
    participant P as M2AnimationPlaybackController
    participant N as M2NativeAnimator
    participant A as AnimationPlayer
    M->>P: copy_native_animator_data(prototype, duplicate, script)
    M->>F: animation_players_in_subtree(duplicate)
    F-->>M: ordered players
    M->>P: start_instance_playback(path, index, players, debug)
    P->>N: prepare_runtime_at_phase
    P->>A: choose, loop, play and seek
    P-->>M: optional detached native diagnostics
    M-->>M: tag states with instance index

Dependency diagram

flowchart TB
    Loader[StreamingWorldLoader] --> Materializer[M2AnimatedInstanceMaterializer]
    Materializer --> Playback[M2AnimationPlaybackController]
    Materializer --> Finalizer[M2AnimatedSceneFinalizer]
    Playback --> Engine[Node / Script / AnimationPlayer / Animation]
    Materializer --> Batch[SceneTree batch materialization]
    Loader --> NativeScript[M2NativeAnimator script]
    Playback -. no dependency .-> ResourceLoader
    Playback -. no dependency .-> Cache[M2PrototypeCacheState]
    Playback -. no dependency .-> Scheduler[RenderBudgetScheduler]

Ownership, threading and resources

  • Materializer owns prototype duplication, instance/batch roots and SceneTree attachment.
  • Materializer uses finalizer traversal for borrowed AnimationPlayer references.
  • Controller borrows Nodes/Script/players only for the synchronous call.
  • Native arrays are assigned by reference exactly as before extraction.
  • Diagnostic Dictionaries are deep-duplicated before return.
  • Main thread performs all engine-object mutation; pure phase math is thread-safe.
  • A duplicated native animator resolves and duplicates its local Mesh, applies phase and deforms once before attachment. Its later _ready() is idempotent.
  • Preparation allocates an empty instance-local ArrayMesh because deformation immediately rebuilds every surface from retained native arrays. Captured immutable Material resources remain shared and are reapplied.

Errors, cancellation and recovery

Failure Detection Behavior Diagnostic Recovery
Missing roots/script Inventory guard No native copy/start Contract fixture Correct composition
Fewer target animators Pair-count minimum Copy available pairs only Native copy fixture Rebuild import/prototype
No matching name Ordered fallback Substring, first name, then empty Selection fixtures Add compatible animation
Empty animation selection Empty result Skip loop/play/seek for that player Selection fixture Static/native path continues
Zero-length selected animation Length guard Play without seek Playback contract Imported timing remains engine-owned
Cancellation/shutdown Outside service No retained state N/A Loader owns subtree release

Configuration and capabilities

Setting/capability Default Profile Runtime mutable Effect
Phase buckets 1000 All No Stable per path/index desynchronization
Loop mode LOOP_LINEAR All No Every available imported animation loops
Native diagnostics debug_streaming Debug Yes Samples state only when requested

Persistence, cache and migration

No state or format is serialized. GLB/native cache formats, prototype lifetime and material versions are unchanged; no rebake is required.

Diagnostics and observability

  • Optional native states remain detached and contain existing prepared, processing, mesh, bone, surface and length fields.
  • Loader retains M2_NATIVE_ANIMATOR log formatting and path normalization.
  • Controller emits no logs or metrics.

Verification

  • verify_m2_animation_playback_controller.gd covers exact phase, ordinary/ fish/bird priorities, substring/first fallback, loop/play/seek, native exact- script order, five-field copy, single-rebuild phased preparation, idempotent ready, detached diagnostics and boundaries.
  • Finalizer/build/prototype/material/shutdown regressions protect adjacent behavior.
  • Fidelity evidence is exact policy/mutation extraction; no private asset or original-client animation comparison is claimed.
  • Performance budget: 20,000 phase-and-selection pairs under one second.

Extension points

Server-driven animation state may later replace default selection through a separate presentation contract; this fallback controller must remain available for world doodads and compatibility fixtures.

Capability status

Capability Status Evidence Gap/next step
Imported default selection/playback Implemented extraction Synthetic priority/play/seek verifier Asset-backed animation-name corpus pending
Native data/phase startup Implemented extraction Exact-reference/native state verifier Native visual fidelity remains experimental
Instance/batch materialization Implemented extraction Materializer contract verifier Asset-backed traversal pending

Known gaps and risks

  • Hash phase intentionally depends on existing Godot String hashing behavior.
  • Default-name heuristics are not a complete WoW animation-state mapping.
  • Native CPU deformation remains proportional to vertex count and is unsuitable for large numbers of independently animated instances without a future GPU path.

Source map

Path Responsibility
src/render/m2/m2_animation_playback_controller.gd Phase, selection, native copy/start and imported playback
src/render/m2/m2_animated_scene_finalizer.gd Accepted prototype/player inventory
src/scenes/streaming/m2_native_animator.gd Experimental native deformation runtime
src/render/m2/m2_animated_instance_materializer.gd Instance duplication/attachment and playback composition
src/scenes/streaming/streaming_world_loader.gd Build adapter, diagnostic formatting and Editor ownership
src/tools/verify_m2_animation_playback_controller.gd Policy/mutation/boundary/timing regression