Editing while playing: play runs on a runtime copy, new Game tab (Scene | Game | Graph)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
cb8885359f
commit
c3a533c4e5
14 files changed
+408
-118
No files matched your search
+77
-30
@@ -6,11 +6,16 @@ import type { System } from './systems/System';
|
||||
|
||||
/**
|
||||
* The running game: entities instantiated from one scene plus the systems ticking them.
|
||||
* Created on Play, thrown away on Stop – the project model is never touched.
|
||||
*
|
||||
* The world mirrors the scene of the project it was built from – in the editor that is the
|
||||
* play-mode *copy* of the model, never the saved one. `syncFromModel()` pulls editor changes
|
||||
* (moved, placed, removed or re-parented objects, edited properties) into the entities;
|
||||
* `syncToModel()` pushes what the systems changed (positions, variables, destroyed entities)
|
||||
* back into that copy, so the panels show live values. Stop throws both away.
|
||||
*/
|
||||
export class World {
|
||||
readonly entities: Entity[] = [];
|
||||
readonly backgroundColor: number;
|
||||
backgroundColor = 0x000000;
|
||||
/** Set by a behavior (Stop game node); the editor leaves play mode on the next tick. */
|
||||
stopRequested = false;
|
||||
/** Messages logged by behaviors (Log node), newest last. */
|
||||
@@ -18,40 +23,82 @@ export class World {
|
||||
private readonly systems: System[] = [];
|
||||
private elapsed = 0;
|
||||
|
||||
private constructor(readonly sceneId: string, backgroundColor: number) {
|
||||
this.backgroundColor = backgroundColor;
|
||||
}
|
||||
private constructor(readonly project: Project, readonly sceneId: string) {}
|
||||
|
||||
/** Builds a world from the given scene. Throws when the scene does not exist. */
|
||||
static instantiate(project: Project, sceneId: string): World {
|
||||
const scene = findScene(project, sceneId);
|
||||
if (!scene) throw new Error(`Scene ${sceneId} not found`);
|
||||
const background = scene.props.backgroundColor;
|
||||
const world = new World(sceneId, typeof background === 'number' ? background : 0x000000);
|
||||
const parentIds = new Map<Entity, string>();
|
||||
for (const object of scene.objects) {
|
||||
const asset = findAsset(project, object.assetId);
|
||||
if (!asset || asset.type === 'scene') continue;
|
||||
const entity: Entity = {
|
||||
id: `e-${object.id}`,
|
||||
objectId: object.id,
|
||||
assetId: asset.id,
|
||||
type: asset.type,
|
||||
name: object.name || asset.name,
|
||||
x: object.position.x,
|
||||
y: object.position.y,
|
||||
z: object.position.z,
|
||||
props: resolveSceneObjectProps(asset, object),
|
||||
parent: null,
|
||||
};
|
||||
world.entities.push(entity);
|
||||
if (object.parentId) parentIds.set(entity, object.parentId);
|
||||
}
|
||||
// Link parents once every entity exists (storage order is not guaranteed to be parent-first).
|
||||
for (const [entity, parentId] of parentIds) entity.parent = world.findByObjectId(parentId) ?? null;
|
||||
if (!findScene(project, sceneId)) throw new Error(`Scene ${sceneId} not found`);
|
||||
const world = new World(project, sceneId);
|
||||
world.syncFromModel();
|
||||
return world;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the entities match the scene: one entity per placed object (created on demand,
|
||||
* dropped when the object is gone), positions, names and resolved properties taken from
|
||||
* the model, parent links re-established. Property bags are updated in place so systems
|
||||
* holding a reference keep seeing the current values.
|
||||
*/
|
||||
syncFromModel(): void {
|
||||
const scene = findScene(this.project, this.sceneId);
|
||||
if (!scene) return;
|
||||
const background = scene.props.backgroundColor;
|
||||
this.backgroundColor = typeof background === 'number' ? background : 0x000000;
|
||||
const keep = new Set<Entity>();
|
||||
for (const object of scene.objects) {
|
||||
const asset = findAsset(this.project, object.assetId);
|
||||
if (!asset || asset.type === 'scene') continue;
|
||||
let entity = this.findByObjectId(object.id);
|
||||
if (!entity) {
|
||||
entity = { id: `e-${object.id}`, objectId: object.id, assetId: asset.id, type: asset.type, name: '', x: 0, y: 0, z: 0, props: {}, parent: null };
|
||||
this.entities.push(entity);
|
||||
}
|
||||
entity.assetId = asset.id;
|
||||
entity.type = asset.type;
|
||||
entity.name = object.name || asset.name;
|
||||
entity.x = object.position.x;
|
||||
entity.y = object.position.y;
|
||||
entity.z = object.position.z;
|
||||
const resolved = resolveSceneObjectProps(asset, object);
|
||||
for (const key of Object.keys(entity.props)) if (!(key in resolved)) delete entity.props[key];
|
||||
Object.assign(entity.props, resolved);
|
||||
keep.add(entity);
|
||||
}
|
||||
for (let i = this.entities.length - 1; i >= 0; i--) if (!keep.has(this.entities[i]!)) this.entities.splice(i, 1);
|
||||
// Link parents once every entity exists (storage order is not guaranteed to be parent-first).
|
||||
for (const entity of this.entities) {
|
||||
const object = scene.objects.find((o) => o.id === entity.objectId);
|
||||
entity.parent = object?.parentId ? this.findByObjectId(object.parentId) ?? null : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the entities' state into the scene: positions, property values that differ from
|
||||
* what the model resolves (stored as overrides, so the inspector marks them) and removed
|
||||
* entities (their objects are dropped). Returns true when the object list changed.
|
||||
*/
|
||||
syncToModel(): boolean {
|
||||
const scene = findScene(this.project, this.sceneId);
|
||||
if (!scene) return false;
|
||||
const alive = new Set(this.entities.map((e) => e.objectId));
|
||||
const before = scene.objects.length;
|
||||
for (let i = scene.objects.length - 1; i >= 0; i--) {
|
||||
const object = scene.objects[i]!;
|
||||
const asset = findAsset(this.project, object.assetId);
|
||||
if (asset && asset.type !== 'scene' && !alive.has(object.id)) scene.objects.splice(i, 1);
|
||||
}
|
||||
for (const entity of this.entities) {
|
||||
const object = scene.objects.find((o) => o.id === entity.objectId);
|
||||
const asset = object && findAsset(this.project, entity.assetId);
|
||||
if (!object || !asset) continue;
|
||||
const p = object.position;
|
||||
if (p.x !== entity.x || p.y !== entity.y || p.z !== entity.z) object.position = { x: entity.x, y: entity.y, z: entity.z };
|
||||
const base = resolveSceneObjectProps(asset, object);
|
||||
for (const [key, value] of Object.entries(entity.props)) if (key in base && base[key] !== value) object.overrides[key] = value;
|
||||
}
|
||||
return scene.objects.length !== before;
|
||||
}
|
||||
|
||||
/** World position of an entity: local position plus those of all ancestors. */
|
||||
worldPosition(entity: Entity): { x: number; y: number; z: number } {
|
||||
return worldPositionOf(entity);
|
||||
|
||||
Reference in new issue
Block a user