animate method
Advances every animation. Once a frame, with the frame's own delta.
From published state when there is some (published): whether an actor lives — which decides a corpse, a clip that holds its last pose and whether a pose is kept for a ragdoll — and which clips it plays (ActorAppearance.clipsFrom) are read from its published row, not from the actor, so this keeps animating when the simulation runs in another isolate. An actor the step left no row for keeps what was last read.
Implementation
void animate(double dt) {
_markers.clear();
final state = published?.call();
for (final entry in _players.entries) {
final actor = entry.key;
final row = state == null
? null
: PublishedActor.read(state, actor.entity);
final alive = _isAlive(actor, row, state);
if (_taken.contains(actor)) {
// A corpse stays the corpses' until its actor stands again — a
// rewind to before the death, a rollback — and then it is let go,
// so the actor is animated again and a second death is handed over
// again. `end` for a body the corpses already let go (the ragdolls'
// own snapshot part) does nothing.
if (!alive) continue;
_taken.remove(actor);
corpses?.end(actor);
}
if (!alive && _takeOver(actor)) continue;
if (simulated?.call(actor) case final stepped?) {
_drawnPose(actor, stepped).writeTo(entry.value.targets);
if (corpses != null && alive) _keepPose(actor);
continue;
}
final graph = _graphs[actor];
if (graph != null) {
final model = _instances[actor]!;
graphs!.drive(switch (_driveContext) {
null => _driveContext = ActorDriveContext(
actor: actor,
graph: graph,
model: model,
),
final context =>
context
..actor = actor
..graph = graph
..model = model,
});
graph.evaluate(dt).writeTo(entry.value.targets);
for (final passed in graph.passed) {
_markers.add(
ActorMarkerPassed(
actor: actor,
state: passed.state,
name: passed.name,
),
);
}
if (corpses != null && alive) _keepPose(actor);
continue;
}
final wanted = row == null
? appearance.clipsFor(actor)
: appearance.clipsFrom(switch (_clipsContext) {
null => _clipsContext = ActorClipsContext(
actor: actor,
published: row,
),
final context =>
context
..actor = actor
..published = row,
});
// The first the model actually has. `crossFadeToNamed` reports whether
// the name was there, so a clip this export does not carry is a miss
// rather than a crash — and the next candidate gets a turn.
for (final name in wanted) {
if (_playing[entry.key] == name) break;
// Only on a change: `crossFadeToNamed` restarts the fade every time it
// is called, so calling it per frame is an animation that never gets
// anywhere — always a tenth of a second into the same blend.
if (entry.value.crossFadeToNamed(name)) {
_playing[entry.key] = name;
break;
}
}
entry.value
..wrap = wrapWhen(isAlive: alive)
..advance(dt)
..apply();
if (corpses != null && alive) _keepPose(actor);
}
_wasAlive.removeWhere((Actor actor, _) => !_players.containsKey(actor));
_lastDt = dt > 0.0 ? dt : _lastDt;
corpses?.step(dt);
}