Let a called behavior share the caller's local state machines
A local state machine started over in every Run behavior call, so a state set in a sub-behavior was gone by the next call. A character controller split into sub-behaviors could therefore never remember that it was attacking. Scene or global scope would keep the state, but then every character shares it. - A call now runs on the states of the instance that runs it, however deep. A state one call sets is still set in the next one, and the call begins in the start state like the instance does. - A move made inside a call is reported like one the caller made. The caller's On state entered and On state exited hear it, the event nodes of the called graph stay silent as before. - Two objects running the same behavior still hold a state each. - This reverses the rule from #229 that local machines of a called behavior are not kept. Keep stays a switch for variables only. Tests: - Two unit tests that pinned the old rule now expect the shared state. - New world test: a sub-behavior closes a door per object, the caller's events hear it, and the next call finds it closed. - Counter-check: with the old call context the 3 tests fail. - Gate on melektaus-atx: 239 files, 3600 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d80d5cce26
commit
ed7872c893
3 files changed
+53
-19
No files matched your search
@@ -240,17 +240,10 @@ export class GraphInterpreter {
|
||||
this.callStack.push(call.behaviorId);
|
||||
this.callSites.push(site);
|
||||
this.calledBehaviors.add(call.behaviorId);
|
||||
// Fresh states every call, and nothing of the caller's variables or states. What it does to a
|
||||
// local machine therefore dies with the call and is not reported; a `global` machine is the
|
||||
// run's, so moving that one is a move like any other.
|
||||
const inner: NodeContext = {
|
||||
...ctx,
|
||||
variables,
|
||||
states: {},
|
||||
stateChanged: (machineId, from, to) => {
|
||||
if (machineId in ctx.shared.states) ctx.stateChanged(machineId, from, to);
|
||||
},
|
||||
};
|
||||
// Nothing of the caller's variables, but its local machines: a local machine belongs to the
|
||||
// running instance however deep the call, so what a call sets is still set on the next one,
|
||||
// and the move is heard like one the caller made itself.
|
||||
const inner: NodeContext = { ...ctx, variables };
|
||||
try {
|
||||
for (const entry of entries) {
|
||||
this.trace?.onNode(entry.id);
|
||||
|
||||
@@ -203,16 +203,17 @@ describe('Keep on a behavior variable (#229)', () => {
|
||||
expect([ctx.variables.r0, ctx.variables.r1]).toEqual([2, 2]);
|
||||
});
|
||||
|
||||
it('starts a local state machine of the callee over every call, kept variables or not', () => {
|
||||
it('leaves a local state machine with the caller, so a call finds what the last one set', () => {
|
||||
const fixture = makeProject();
|
||||
const create = new CreateAssetCommand('statemachine', 'Doors');
|
||||
fixture.stack.execute(create);
|
||||
const open = new AddNodeCommand(create.asset.id, STATE, 0, 0, { [STATE_NAME_PARAM]: 'Open' });
|
||||
fixture.stack.execute(open);
|
||||
const machine = create.asset.id;
|
||||
// No state yet → "first" and put it in Open; in Open → "open". The kept variable is only
|
||||
// there to show that keeping one does not keep the machine along with it.
|
||||
// No state yet → "first" and put it in Open; in Open → "open". The variable is not kept, which
|
||||
// shows that the machine does not hang on the Keep switch.
|
||||
const callee = makeBehavior(fixture, 'Door', ['count']);
|
||||
fixture.stack.execute(new SetVariableKeptCommand(callee.id, 'count', false));
|
||||
const edit = graphEditor(fixture, callee);
|
||||
const start = edit.addNode(BEHAVIOR_START);
|
||||
const branch = edit.addNode('StateBranch', { [MACHINE_PORT]: machine });
|
||||
@@ -237,9 +238,9 @@ describe('Keep on a behavior variable (#229)', () => {
|
||||
const ctx = fakeContext();
|
||||
fire(interpreter, 'OnStart', ctx);
|
||||
fire(interpreter, 'OnStart', ctx);
|
||||
expect(ctx.logs).toEqual(['first', 'first']);
|
||||
// A local machine of the callee is none of the caller's business either.
|
||||
expect(ctx.states).toEqual({});
|
||||
expect(ctx.logs).toEqual(['first', 'open']);
|
||||
// The state sits in the caller's bag, where the running instance keeps its local machines.
|
||||
expect(ctx.states).toEqual({ [machine]: open.node.id });
|
||||
});
|
||||
|
||||
it('survives saving and loading, and a name that is not declared is dropped', () => {
|
||||
|
||||
@@ -275,7 +275,7 @@ describe('a running state machine', () => {
|
||||
expect(started.world.log).toEqual(['open', 'open', 'closed', 'closed']);
|
||||
});
|
||||
|
||||
it('starts a called behavior in no state, whatever the caller set', () => {
|
||||
it('lets a called behavior see the state its caller set', () => {
|
||||
const f = makeProject();
|
||||
const machine = makeMachine(f);
|
||||
const inner = makeBehavior(f, 'Inner');
|
||||
@@ -299,7 +299,8 @@ describe('a running state machine', () => {
|
||||
const ctx = fakeContext();
|
||||
fire(new GraphInterpreter(outer.graph, graphLookup(f.project)), 'OnStart', ctx);
|
||||
expect(ctx.states).toEqual({ [machine.id]: machine.ids[0] });
|
||||
expect(ctx.logs).toEqual([]);
|
||||
// A local machine belongs to the instance, not to the graph that happens to run.
|
||||
expect(ctx.logs).toEqual(['inner saw open']);
|
||||
});
|
||||
|
||||
it('is driven by name where the machine arrives as a value, and branches through else', () => {
|
||||
@@ -501,6 +502,45 @@ describe('On state entered and On state exited', () => {
|
||||
expect(open).not.toBe('');
|
||||
});
|
||||
|
||||
it('hears a move made inside a called behavior, each object in its own state', () => {
|
||||
const f = makeSpriteScene();
|
||||
const machine = makeMachine(f);
|
||||
const [open, closed] = machine.ids as [string, string];
|
||||
f.stack.execute(new SetMachineStartCommand(machine.id, open));
|
||||
// Called every frame: closes the door it finds open, says so once it finds it closed.
|
||||
const step = makeBehavior(f, 'Step');
|
||||
const stepEdit = graphEditor(f, step);
|
||||
const branch = stepEdit.addNode('StateBranch', { [MACHINE_PORT]: machine.id });
|
||||
const set = stepEdit.addNode('SetState', { [MACHINE_PORT]: machine.id });
|
||||
const said = stepEdit.addNode('Log', { message: 'Step: found closed' });
|
||||
stepEdit.wire([
|
||||
[stepEdit.addNode(BEHAVIOR_START), 'exec', branch, 'exec'],
|
||||
[branch, open, set, closed],
|
||||
[branch, closed, said, 'exec'],
|
||||
]);
|
||||
const { behavior, edit } = watcher(f, machine.id, machine.ids);
|
||||
const call = edit.addNode('RunBehavior', { [BEHAVIOR_PARAM]: step.id });
|
||||
edit.connect(edit.addNode('OnUpdate'), 'exec', call, 'exec');
|
||||
place(f, behavior.id, { x: 0, y: 0 });
|
||||
place(f, behavior.id, { x: 10, y: 0 });
|
||||
const started = startWorld(f, { behaviors: true });
|
||||
started.frame();
|
||||
// The call starts in the instance's start state and its move reaches that instance's events.
|
||||
expect([...started.world.log].sort()).toEqual([
|
||||
'Watcher: entered closed',
|
||||
'Watcher: entered closed',
|
||||
'Watcher: entered open',
|
||||
'Watcher: entered open',
|
||||
'Watcher: exited open',
|
||||
'Watcher: exited open',
|
||||
]);
|
||||
expect(started.behaviors?.machineStates(machine.id).map((s) => s.state)).toEqual([closed, closed]);
|
||||
started.world.log.length = 0;
|
||||
started.frame();
|
||||
// The next call finds what the last one set, once per object.
|
||||
expect(started.world.log).toEqual(['Step: found closed', 'Step: found closed']);
|
||||
});
|
||||
|
||||
it('reaches every instance when the machine is global, and enters its start once', () => {
|
||||
const f = makeSpriteScene();
|
||||
const machine = makeMachine(f);
|
||||
|
||||
Reference in new issue
Block a user