An animation reaches the scene's own background and objects inside placed containers (#109)
game-maker / gate (push) Successful in 1m33s
game-maker / suite (push) Successful in 5m37s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-17 06:48:21 +02:00
1 parent 353b348c3c
commit f68941fa3a
7 files changed
+334 -128

No files matched your search

+9 -3
View File
@@ -1,6 +1,7 @@
import { ENTITY_HANDLE, isHandleEnabled, type PointerState, worldMatrixOf } from '../core/graph';
import { newId } from '../core/ids';
import type { Asset } from '../core/model/Asset';
import type { PropertyBag } from '../core/model/Properties';
import { isPlaceableType } from '../core/model/AssetTypes';
import { bakeInstances, materializeInstance, objectRef, resolveObjects } from '../core/model/instance';
import { treeOrder } from '../core/model/hierarchy';
@@ -34,7 +35,8 @@ export interface ModelWrite {
*/
export class World {
readonly entities: Entity[] = [];
backgroundColor = 0x000000;
/** The running scene's own properties – read from the model, and written by an animation over the scene. */
readonly sceneProps: PropertyBag = {};
/** Set by a behavior (Stop game node); the editor leaves play mode on the next tick. */
stopRequested = false;
/** Set by a behavior (Restart game node); the editor restarts the scene on the next tick. */
@@ -83,6 +85,11 @@ export class World {
return this.elapsed;
}
get backgroundColor(): number {
const background = this.sceneProps.backgroundColor;
return typeof background === 'number' ? background : 0x000000;
}
/** Makes the entities match the scene; props are updated in place so systems holding a bag stay current. */
syncFromModel(): void {
const scene = findScene(this.project, this.sceneId);
@@ -91,8 +98,7 @@ export class World {
// behavior destroy one of them for good. Play does it (on its throwaway copy); the editor run
// must not, so there the content stays resolved and `objectRef` writes into the instance edits.
if (this.bakeContainers && bakeInstances(this.project, scene)) this.structureChanged = true;
const background = scene.props.backgroundColor;
this.backgroundColor = typeof background === 'number' ? background : 0x000000;
Object.assign(this.sceneProps, scene.props);
const objects = new Map(resolveObjects(this.project, scene).map((o) => [o.id, o]));
const keep = new Set<Entity>();
for (const object of objects.values()) {
+44 -16
View File
@@ -21,12 +21,14 @@ export interface AnimationFinish {
animationId: string;
}
/** A row that reaches something this tick: the entity it writes and the kind it blends as. */
/** A row that reaches something this tick: how to read and write it, and the kind it blends as. */
interface Binding {
track: AnimationTrack;
entity: Entity;
kind: string;
/** Where the player keeps the starting value: `<entity id> <property>`, or `scene <property>`. */
base: string;
read(): PropertyValue;
write(value: PropertyValue): void;
}
/**
@@ -53,18 +55,18 @@ export class AnimationSystem implements System {
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] = read(b.entity, b.track.property);
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]);
if (value !== undefined) write(b.entity, b.track.property, value);
if (value !== undefined) b.write(value);
}
if (!head.finished) continue;
if (playbackOf(asset) === 'once-reset')
for (const b of bindings) {
const base = player.bases[b.base];
if (base !== undefined) write(b.entity, b.track.property, base);
if (base !== undefined) b.write(base);
}
drop(entity, player);
this.finished.push({ entity, animationId: asset.id });
@@ -81,32 +83,58 @@ function drop(entity: Entity, player: AnimationPlayer): void {
}
/**
* The rows that reach something on this object. A scene's rows aim at its objects by id, and only
* while that scene runs; everything else aims at the object itself or at what its template placed
* inside it. A row whose target is gone, or lacks the property, is skipped.
* The rows that reach something on this object. A scene's rows aim at the scene itself or at its
* objects by id, and only while that scene runs; everything else aims at the object itself or at
* what its template placed inside it, however deep. A row whose target is gone, or lacks the
* property, is skipped.
*/
function bind(world: World, asset: AnimationAsset, owner: Entity): Binding[] {
const root = world.project.assets.find((a) => a.id === asset.root);
if (!root) return [];
const bindings: Binding[] = [];
for (const track of asset.tracks) {
const key = track.property;
if (root.type === 'scene' && track.target === '') {
const kind = world.sceneId === root.id ? animatedKind('scene', key) : undefined;
if (kind)
bindings.push({
track,
kind,
base: `scene ${key}`,
read: () => world.sceneProps[key] ?? 0,
write: (value) => {
world.sceneProps[key] = value;
},
});
continue;
}
let entity: Entity | undefined;
if (root.type === 'scene')
entity = world.sceneId === root.id && track.target ? world.findByObjectId(track.target) : undefined;
entity = world.sceneId === root.id ? world.findByObjectId(track.target) : undefined;
else if (track.target === '') entity = owner;
else entity = world.findByObjectId(childId(owner.objectId, track.target));
const kind = entity && animatedKind(entity.type, track.property);
if (entity && kind) bindings.push({ track, entity, kind, base: `${entity.id} ${track.property}` });
const kind = entity && animatedKind(entity.type, key);
if (entity && kind) {
const aimed = entity;
bindings.push({
track,
kind,
base: `${aimed.id} ${key}`,
read: () => read(aimed, key),
write: (value) => write(aimed, key, value),
});
}
}
return bindings;
}
/** The id a template object gets below a placed container – nested one deeper when the container is content itself. */
function childId(ownerId: string, templateId: string): string {
/**
* The id a template object gets below a placed container – nested one deeper when the container is
* content itself. `path` may run through containers placed inside the template (`a/b`).
*/
function childId(ownerId: string, path: string): string {
const split = splitInstanceId(ownerId);
return split
? instanceChildId(split.instanceId, `${split.path}/${templateId}`)
: instanceChildId(ownerId, templateId);
return split ? instanceChildId(split.instanceId, `${split.path}/${path}`) : instanceChildId(ownerId, path);
}
function read(entity: Entity, key: string): PropertyValue {