Play animation can blend into the new animation over a number of frames (#106)
game-maker / gate (push) Successful in 1m33s
game-maker / suite (push) Successful in 5m34s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-17 10:03:53 +02:00
1 parent beeea355d9
commit 04dc254b3d
4 files changed
+177 -14

No files matched your search

+29 -7
View File
@@ -4,6 +4,10 @@ import {
animatableDefsOf,
animatedKind,
animationPlayheadAt,
blendShare,
blends,
blendValue,
fpsOf,
isPositionRow,
playbackOf,
trackWriteAt,
@@ -34,8 +38,8 @@ interface Binding {
/**
* Plays what `Play animation` started. Players run in start order, so of two setting one property
* the later start wins. Runs before the BehaviorSystem: a behavior writing the same property writes
* last, and hears this frame's `finished`.
* the later start wins – or, while it blends in, mixes over what was there. Runs before the
* BehaviorSystem: a behavior writing the same property writes last, and hears this frame's `finished`.
*/
export class AnimationSystem implements System {
finished: AnimationFinish[] = [];
@@ -46,6 +50,8 @@ export class AnimationSystem implements System {
for (const entity of world.entities)
for (const player of entity.animations ?? []) running.push({ entity, player });
running.sort((a, b) => a.player.order - b.player.order);
// What the players before this one wrote this tick – a blend mixes from that value, live.
const written = new Set<string>();
for (const { entity, player } of running) {
const asset = findAssetOf(world.project, player.animationId, 'animation');
if (!asset) {
@@ -53,16 +59,32 @@ export class AnimationSystem implements System {
continue;
}
const bindings = bind(world, asset, entity);
const blend = player.blend;
if (blend && !blend.from) {
blend.from = {};
for (const b of bindings) blend.from[b.base] = b.read();
}
if (!player.bases) {
// The first tick only takes the starting values and shows frame 0; the clock runs from the next.
player.bases = {};
for (const b of bindings) player.bases[b.base] = b.read();
} else if (player.playing && isHandleEnabled(entity)) player.time += dt;
const head = animationPlayheadAt(asset, player.time);
for (const b of bindings) {
const value = trackWriteAt(b.track, b.kind, head.frame, player.bases[b.base], head.reverse);
if (value !== undefined) b.write(value);
} else if (player.playing && isHandleEnabled(entity)) {
player.time += dt;
if (blend) blend.elapsed += dt;
}
const head = animationPlayheadAt(asset, player.time);
const share = blend ? blendShare(blend, fpsOf(asset)) : 1;
for (const b of bindings) {
let value = trackWriteAt(b.track, b.kind, head.frame, player.bases[b.base], head.reverse);
if (value === undefined) continue;
if (share < 1 && blends(b.kind)) {
const from = written.has(b.base) ? b.read() : blend?.from?.[b.base];
if (from !== undefined) value = blendValue(b.kind, from, value, share);
}
b.write(value);
written.add(b.base);
}
if (blend && share >= 1) delete player.blend;
if (!head.finished) continue;
if (playbackOf(asset) === 'once-reset')
for (const b of bindings) {