Hold a running game, and let the editor run reach into containers
Pause is a sub-state of play, not a third mode: the toolbar button and Ctrl+Enter stop ticking the world, the clock included. A held frame still drops the input edges, so resuming does not fire a pause worth of keys in one go. The editor run missed the case it is most wanted for. hasEditorRun only looked at the stored scene objects, so a behavior inside a placed container never started one; the world now builds from resolveObjects and writes back through objectRef, keeping the move as an instance edit instead of baking the instance out. Keys and clicks go to the run while the Game tab is on screen, where no key means anything to the editor, and are handed back on leaving it - held keys released, so nothing keeps walking unpressed.
This commit is contained in:
1 parent
8f31afac37
commit
9c4148dc1b
19 files changed
+838
-78
No files matched your search
+35
-28
@@ -2,8 +2,8 @@ import { isHandleEnabled, type PointerState, worldPositionOf } from '../core/gra
|
||||
import { newId } from '../core/ids';
|
||||
import type { Asset } from '../core/model/Asset';
|
||||
import { isPlaceableType } from '../core/model/AssetTypes';
|
||||
import { bakeInstances, materializeInstance } from '../core/model/instance';
|
||||
import { findObject, treeOrder } from '../core/model/hierarchy';
|
||||
import { bakeInstances, materializeInstance, objectRef, resolveObjects } from '../core/model/instance';
|
||||
import { treeOrder } from '../core/model/hierarchy';
|
||||
import { descendantsBelow } from '../core/model/walk';
|
||||
import { findAsset, findScene, type Project } from '../core/model/Project';
|
||||
import { createSceneObject, type SceneObject } from '../core/model/SceneObject';
|
||||
@@ -16,8 +16,8 @@ import type { System } from './systems/System';
|
||||
export interface WorldOptions {
|
||||
/**
|
||||
* Whether a placed container's content may be written into the scene (see `syncFromModel`).
|
||||
* Baking is a real, irreversible edit, so the editor run switches it off – at the price of
|
||||
* behaviors inside a placed container not running there.
|
||||
* Baking is a real, irreversible edit, so the editor run switches it off; its content still runs,
|
||||
* but what it writes goes into the instance edits and it cannot destroy itself for good.
|
||||
*/
|
||||
bakeContainers?: boolean;
|
||||
}
|
||||
@@ -31,11 +31,11 @@ export interface ModelWrite {
|
||||
/**
|
||||
* The running game: entities instantiated from one scene plus the systems ticking them.
|
||||
*
|
||||
* 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.
|
||||
* The world mirrors the scene of the project it was built from: while playing that is a copy of
|
||||
* the model, thrown away on stop; the editor run builds on the saved one, where the writes stay.
|
||||
* `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, so the panels show live values.
|
||||
*/
|
||||
export class World {
|
||||
readonly entities: Entity[] = [];
|
||||
@@ -93,14 +93,15 @@ export class World {
|
||||
syncFromModel(): void {
|
||||
const scene = findScene(this.project, this.sceneId);
|
||||
if (!scene) return;
|
||||
// Entities are built from stored objects, and a behavior may move or destroy one of them –
|
||||
// which content resolved from a container template could not remember. So it is written out
|
||||
// once, into the play-mode copy of the model that is thrown away afterwards.
|
||||
// Baking spells a placed container's content out as stored objects, which is what lets a
|
||||
// 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;
|
||||
const objects = new Map(resolveObjects(this.project, scene).map((o) => [o.id, o]));
|
||||
const keep = new Set<Entity>();
|
||||
for (const object of scene.objects) {
|
||||
for (const object of objects.values()) {
|
||||
const asset = findAsset(this.project, object.assetId);
|
||||
if (!asset || !isPlaceableType(asset.type)) continue;
|
||||
keep.add(this.materialize(object, asset));
|
||||
@@ -109,7 +110,7 @@ export class World {
|
||||
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 = findObject(scene, entity.objectId);
|
||||
const object = objects.get(entity.objectId);
|
||||
entity.parent = object?.parentId ? (this.findByObjectId(object.parentId) ?? null) : null;
|
||||
}
|
||||
}
|
||||
@@ -179,7 +180,7 @@ export class World {
|
||||
/**
|
||||
* 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).
|
||||
* entities (their objects are dropped – only stored ones, see `WorldOptions.bakeContainers`).
|
||||
*/
|
||||
syncToModel(): ModelWrite {
|
||||
const scene = findScene(this.project, this.sceneId);
|
||||
@@ -194,21 +195,22 @@ export class World {
|
||||
const asset = findAsset(this.project, object.assetId);
|
||||
if (asset && isPlaceableType(asset.type) && !alive.has(object.id)) scene.objects.splice(i, 1);
|
||||
}
|
||||
for (const entity of this.entities) {
|
||||
const object = findObject(scene, entity.objectId);
|
||||
const asset = object && findAsset(this.project, entity.assetId);
|
||||
if (!object || !asset) continue;
|
||||
for (const object of resolveObjects(this.project, scene)) {
|
||||
const entity = this.findByObjectId(object.id);
|
||||
const asset = entity && findAsset(this.project, entity.assetId);
|
||||
if (!entity || !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 };
|
||||
values = true;
|
||||
}
|
||||
const moved = p.x !== entity.x || p.y !== entity.y || p.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;
|
||||
values = true;
|
||||
}
|
||||
const drifted = Object.keys(entity.props).filter((k) => k in base && base[k] !== entity.props[k]);
|
||||
if (!moved && drifted.length === 0) continue;
|
||||
// Only now, because a ref for resolved content costs a resolve of its own – and because a
|
||||
// direct write to such an object would land in the freshly built copy and be lost.
|
||||
const ref = objectRef(this.project, scene, object.id);
|
||||
if (!ref) continue;
|
||||
if (moved) ref.setPosition({ x: entity.x, y: entity.y, z: entity.z });
|
||||
for (const key of drifted) ref.setOverride(key, entity.props[key]);
|
||||
values = true;
|
||||
}
|
||||
return { structure: spawned || scene.objects.length !== before, values };
|
||||
}
|
||||
@@ -227,6 +229,11 @@ export class World {
|
||||
this.elapsed += dt;
|
||||
for (const system of this.systems) system.update(this, dt);
|
||||
// The pointer edges belong to exactly one tick, whatever the systems did with them.
|
||||
this.dropPointerEdges();
|
||||
}
|
||||
|
||||
/** Forgets a pending click edge without running a tick (used while paused). */
|
||||
dropPointerEdges(): void {
|
||||
this.pointer.pressed = false;
|
||||
this.pointer.released = false;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user