Let a behavior run while the editor is open
A behavior object carries a "Run in editor" switch next to Enabled. When it is on, the behavior ticks outside of Play and works on the saved model directly - Unity semantics: what it moves is a real change, autosaved and not undoable. Input stays with the editor, so On key never fires while typing. The editor run never bakes placed containers into the model, since that would silently drop their instanceEdits. syncToModel reports whether it wrote anything, and the dirty flag is only raised on change - otherwise a behavior moving something every frame would trigger ~60 saves a second.
This commit is contained in:
1 parent
c74037b6e1
commit
8f31afac37
21 files changed
+718
-35
No files matched your search
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Keeps the behaviors switched to "run in editor" running while the project is *not* playing.
|
||||
*
|
||||
* The one thing that makes this different from Play: the world is built from the saved model, not
|
||||
* from a copy. What such a behavior moves or writes is a real edit – it is saved and it is not on
|
||||
* the undo stack. The keyboard and the mouse stay with the editor, so input nodes never fire here.
|
||||
*/
|
||||
import { hasEditorRun, runsInEditor } from '../core/properties/editorRun';
|
||||
import { BehaviorSystem } from '../runtime/systems/BehaviorSystem';
|
||||
import { CollisionSystem } from '../runtime/systems/CollisionSystem';
|
||||
import { Input } from '../runtime/systems/Input';
|
||||
import { type ModelWrite, World } from '../runtime/World';
|
||||
import type { EditorState } from './state';
|
||||
|
||||
export class EditorRun {
|
||||
private world: World | null = null;
|
||||
private behaviors: BehaviorSystem | null = null;
|
||||
/** Never attached: the editor owns the keyboard, so `Is key down` is always false in here. */
|
||||
private readonly input = new Input();
|
||||
/** The model changed, so *whether* anything runs is open again, not just the world's contents. */
|
||||
private stale = true;
|
||||
/** Set while our own write travels through the listeners, so it does not count as a change. */
|
||||
private writing = false;
|
||||
|
||||
constructor(private readonly state: EditorState) {}
|
||||
|
||||
/** The model changed: a command, a load, or leaving play mode. */
|
||||
invalidate(): void {
|
||||
if (!this.writing) this.stale = true;
|
||||
}
|
||||
|
||||
tick(dt: number): void {
|
||||
if (this.state.mode !== 'edit') {
|
||||
// Playing runs every behavior anyway – and on the runtime copy, which this world is not on.
|
||||
this.stop();
|
||||
this.stale = true;
|
||||
return;
|
||||
}
|
||||
if (this.stale) {
|
||||
this.stale = false;
|
||||
this.refresh();
|
||||
}
|
||||
const world = this.world;
|
||||
if (!world) return;
|
||||
world.tick(Math.min(dt, 0.1));
|
||||
if (this.control(world)) return;
|
||||
this.commit(world.syncToModel());
|
||||
}
|
||||
|
||||
/** Facts for the check scripts: object ids of the behaviors currently running without Play. */
|
||||
debugInfo(): { running: boolean; time: number; behaviors: string[] } {
|
||||
return {
|
||||
running: this.world !== null,
|
||||
time: this.world?.time ?? 0,
|
||||
behaviors: this.behaviors?.runningIds() ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
/** Builds, keeps or drops the world, depending on what the scene now asks for. */
|
||||
private refresh(): void {
|
||||
const scene = this.state.activeScene;
|
||||
if (!scene || !hasEditorRun(this.state.project, scene)) {
|
||||
this.stop();
|
||||
return;
|
||||
}
|
||||
const world = this.world;
|
||||
if (world && world.project === this.state.project && world.sceneId === scene.id) {
|
||||
world.syncFromModel();
|
||||
return;
|
||||
}
|
||||
this.stop();
|
||||
const project = this.state.project;
|
||||
// Baking a container's content into the scene would be an edit nobody asked for, so it is off
|
||||
// here – which is why a behavior inside a placed container does not run outside play mode.
|
||||
const fresh = World.instantiate(project, scene.id, { bakeContainers: false });
|
||||
const collisions = new CollisionSystem();
|
||||
this.behaviors = new BehaviorSystem(project, this.input, collisions, (e) => runsInEditor(e.props));
|
||||
fresh.addSystem(collisions);
|
||||
fresh.addSystem(this.behaviors);
|
||||
this.world = fresh;
|
||||
// `On start` already ran while the system started, and it may have failed.
|
||||
this.control(fresh);
|
||||
}
|
||||
|
||||
/** True when the run ended: a behavior failed, stopped the game, or asked to start over. */
|
||||
private control(world: World): boolean {
|
||||
if (world.restartRequested) {
|
||||
this.stop();
|
||||
this.stale = true;
|
||||
return true;
|
||||
}
|
||||
if (!world.stopRequested) return false;
|
||||
const error = world.log.find((line) => line.startsWith('Error in behavior'));
|
||||
this.stop();
|
||||
this.state.setNotice(error ?? 'A behavior running in the editor stopped it.');
|
||||
return true;
|
||||
}
|
||||
|
||||
private commit(write: ModelWrite): void {
|
||||
if (!write.values && !write.structure) return;
|
||||
this.writing = true;
|
||||
this.state.runtimeEdit();
|
||||
// Objects appeared or vanished: the panels have to rebuild, not just re-read their values.
|
||||
if (write.structure) this.state.preview();
|
||||
this.writing = false;
|
||||
}
|
||||
|
||||
private stop(): void {
|
||||
this.world?.destroy();
|
||||
this.world = null;
|
||||
this.behaviors = null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user