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:
root committed 2026-09-09 17:13:07 +02:00
1 parent c74037b6e1
commit 8f31afac37
21 files changed
+718 -35

No files matched your search

+6 -4
View File
@@ -84,8 +84,8 @@ const ASSET_SCHEMAS: Record<AssetType, PropertySchema> = {
],
},
],
// Behaviors declare their own properties (variables) per asset – see `schemaForAsset`.
behavior: [],
// Behaviors declare their own properties (variables) per asset too – see `schemaForAsset`.
behavior: [{ key: 'runInEditor', label: 'Run in editor', kind: 'boolean', default: false }],
collider: [
{
key: 'shape',
@@ -133,9 +133,11 @@ export function schemaFor(type: AssetType): PropertySchema {
return FULL_SCHEMAS[type];
}
/** The type schema, or for behaviors their declared variables. */
/** The type schema, for behaviors followed by their declared variables. */
export function schemaForAsset(asset: Asset): PropertySchema {
return asset.type === 'behavior' ? [...COMMON_PROPS, ...asset.variables] : FULL_SCHEMAS[asset.type];
return asset.type === 'behavior'
? [...FULL_SCHEMAS.behavior, ...asset.variables]
: FULL_SCHEMAS[asset.type];
}
export function propDef(asset: Asset, key: string): PropertyDef | undefined {
+2 -2
View File
@@ -3,7 +3,7 @@ import { ancestorIdsOf, type ObjectTree } from '../model/hierarchy';
import { instanceTargetOf, isEmptyEdit, resolveObjects } from '../model/instance';
import { findAsset, type Project } from '../model/Project';
import type { SceneObject } from '../model/SceneObject';
import { COMMON_PROPS, type PropertySchema, schemaForAsset } from './PropertySchema';
import { type PropertySchema, schemaFor, schemaForAsset } from './PropertySchema';
import { deviatesFromAsset, isModified } from './resolve';
/**
@@ -11,7 +11,7 @@ import { deviatesFromAsset, isModified } from './resolve';
* default is declared on the variable itself, so the asset value cannot differ from it.
*/
export function markedAssetSchema(asset: Asset): PropertySchema {
return asset.type === 'behavior' ? COMMON_PROPS : schemaForAsset(asset);
return asset.type === 'behavior' ? schemaFor('behavior') : schemaForAsset(asset);
}
/** Whether any asset property the inspector would mark gold differs from its default. */
+22
View File
@@ -0,0 +1,22 @@
/**
* The `runInEditor` switch of a behavior (see `ASSET_SCHEMAS.behavior`). A behavior with it on
* keeps running while the project is *not* playing, and then it edits the saved model: what it
* moves or writes is a real change that is saved and is not on the undo stack. That is what makes
* the switch useful (a behavior can build or arrange content) and why it is off by default.
*/
import type { ObjectTree } from '../model/hierarchy';
import type { PropertyBag } from '../model/Properties';
import { findAsset, type Project } from '../model/Project';
import { resolveSceneObjectProps } from './resolve';
export function runsInEditor(props: PropertyBag): boolean {
return props.runInEditor === true;
}
/** Whether a holder contains any behavior asking to run outside play mode. */
export function hasEditorRun(project: Project, holder: ObjectTree): boolean {
return holder.objects.some((object) => {
const asset = findAsset(project, object.assetId);
return asset?.type === 'behavior' && runsInEditor(resolveSceneObjectProps(asset, object));
});
}