Edit the graph during play and watch values and flow run through it
The graph was only locked by five edit-mode guards in GraphCanvas. The running game reads the graph by reference each frame, so a new node, edge, changed value, disconnect or undo takes effect from the next frame. Stop discards these edits like everything else changed in play. A new On start node does not fire for a behavior that has already started. During play every data output shows the value it last produced, in the highlight colour and refreshed ten times a second. Values are kept per graph, because node ids are only unique within one; with several instances the most recent value wins. The Variables node reads the live variable bag, so only its own outputs are kept, as a snapshot. Each time an exec edge fires, a bead runs along it in 0.3 s, at most one every 0.15 s per edge, following the same curve the edge is drawn with. Beads run on game time and freeze while paused; the game logic is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
495756b500
commit
656d61f187
17 files changed
+542
-37
No files matched your search
@@ -7,6 +7,7 @@ import {
|
||||
hasExecInput,
|
||||
inlineValue,
|
||||
isEventNode,
|
||||
type NodeDef,
|
||||
type PortDef,
|
||||
type RepeatResult,
|
||||
} from './NodeRegistry';
|
||||
@@ -25,6 +26,8 @@ const MAX_DEPTH = 1000;
|
||||
export interface TraceListener {
|
||||
onEdge(edgeId: string): void;
|
||||
onNode(nodeId: string): void;
|
||||
/** What a node's outputs held when it last produced them, in the graph it belongs to. */
|
||||
onValues?(graph: Graph, nodeId: string, values: Record<string, Value>): void;
|
||||
}
|
||||
|
||||
/** One graph's pulse state. A nested behavior runs in its own – node ids are unique per graph only. */
|
||||
@@ -78,6 +81,7 @@ export class GraphInterpreter {
|
||||
this.steps = 0;
|
||||
this.frame.outputCache.set(node.id, { ...outputs });
|
||||
this.trace?.onNode(node.id);
|
||||
this.record(node.id, outputs);
|
||||
this.follow(node, 'exec', ctx, 0);
|
||||
fired++;
|
||||
}
|
||||
@@ -103,8 +107,10 @@ export class GraphInterpreter {
|
||||
const inputs = this.collectInputs(node, ctx);
|
||||
const result = def.run(ctx, inputs, node.params, def);
|
||||
const returned = result.call ? this.call(result.call, ctx, depth) : {};
|
||||
if (!def.read) this.frame.outputCache.set(node.id, { ...result.outputs, ...returned });
|
||||
const produced = { ...result.outputs, ...returned };
|
||||
if (!def.read) this.frame.outputCache.set(node.id, produced);
|
||||
this.trace?.onNode(node.id);
|
||||
if (this.trace?.onValues) this.record(node.id, def.read ? this.readAll(node, def, ctx) : produced);
|
||||
if (result.repeat) this.loop(node, result.repeat, ctx, depth);
|
||||
const next = result.next === undefined ? [] : Array.isArray(result.next) ? result.next : [result.next];
|
||||
for (const port of next) this.follow(node, port, ctx, depth);
|
||||
@@ -124,6 +130,7 @@ export class GraphInterpreter {
|
||||
const outer = this.frame;
|
||||
this.frame = emptyFrame(program.graph);
|
||||
this.frame.outputCache.set(start.id, { ...call.inputs });
|
||||
this.record(start.id, call.inputs);
|
||||
this.callStack.push(call.behaviorId);
|
||||
this.calledBehaviors.add(call.behaviorId);
|
||||
try {
|
||||
@@ -148,7 +155,9 @@ export class GraphInterpreter {
|
||||
if (++this.steps > MAX_STEPS_PER_FIRE)
|
||||
throw new Error('Behavior graph exceeded the step limit (endless loop?)');
|
||||
this.frame.dataCache.clear();
|
||||
this.frame.outputCache.set(node.id, { ...outputs, [repeat.index]: round });
|
||||
const roundOutputs = { ...outputs, [repeat.index]: round };
|
||||
this.frame.outputCache.set(node.id, roundOutputs);
|
||||
this.record(node.id, roundOutputs);
|
||||
this.follow(node, repeat.port, ctx, depth);
|
||||
}
|
||||
this.frame.dataCache.clear();
|
||||
@@ -177,11 +186,9 @@ export class GraphInterpreter {
|
||||
if (!def) return null;
|
||||
if (def.read) {
|
||||
this.trace?.onNode(nodeId);
|
||||
const input = (key: string): Value => {
|
||||
const found = def.inputs.find((p) => p.key === key);
|
||||
return found ? this.inputValue(source, found, ctx) : null;
|
||||
};
|
||||
return def.read(ctx, source.params, input)[port] ?? null;
|
||||
const values = this.readAll(source, def, ctx);
|
||||
this.record(nodeId, values);
|
||||
return values[port] ?? null;
|
||||
}
|
||||
// Exec nodes that have not run in this pulse have no value yet.
|
||||
if (isEventNode(def) || hasExecInput(def) || !def.run) return null;
|
||||
@@ -192,9 +199,26 @@ export class GraphInterpreter {
|
||||
const outputs = def.run(ctx, this.collectInputs(source, ctx), source.params, def).outputs ?? {};
|
||||
this.frame.dataCache.set(nodeId, outputs);
|
||||
this.trace?.onNode(nodeId);
|
||||
this.record(nodeId, outputs);
|
||||
return outputs[port] ?? null;
|
||||
}
|
||||
|
||||
private readAll(node: GraphNode, def: NodeDef, ctx: NodeContext): Record<string, Value> {
|
||||
const input = (key: string): Value => {
|
||||
const found = def.inputs.find((p) => p.key === key);
|
||||
return found ? this.inputValue(node, found, ctx) : null;
|
||||
};
|
||||
// `read` may hand back a whole bag (the Variables node returns the object's variables, live).
|
||||
const all = def.read?.(ctx, node.params, input) ?? {};
|
||||
const own: Record<string, Value> = {};
|
||||
for (const port of def.outputs) if (port.key in all) own[port.key] = all[port.key] ?? null;
|
||||
return own;
|
||||
}
|
||||
|
||||
private record(nodeId: string, values: Record<string, Value>): void {
|
||||
this.trace?.onValues?.(this.frame.graph, nodeId, values);
|
||||
}
|
||||
|
||||
private def(node: GraphNode) {
|
||||
return resolveDef(node, this.frame.graph, this.behaviors);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user