Behavior lifecycle: On enable, On disable and On destroy events (#42)
game-maker / suite (push) Successful in 7m29s
game-maker / suite (push) Successful in 7m29s
Order per instance: On start -> On enable -> On update ... -> On disable -> On destroy. On enable fires right after On start and whenever the behavior comes back on (itself and its parents); On disable in the frame it goes off, the instance stays frozen. On destroy fires when the entity leaves the world (Destroy node, child of a destroyed object) and when the world is torn down on stop/restart, preceded by On disable if still on. A new On start node added while playing still does not fire for a running instance (kept as is, documented in the wiki). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
f0673d1957
commit
6141d07d4f
8 files changed
+203
-8
No files matched your search
@@ -323,3 +323,105 @@ describe('a switched-off behavior receives no events', () => {
|
||||
expect(world.log).toEqual(['down', 'tick', 'tick']);
|
||||
});
|
||||
});
|
||||
|
||||
/** Every lifecycle event logs its own name, so `world.log` reads as the order they fired in. */
|
||||
function lifecycle(): ReturnType<typeof scene> {
|
||||
const f = scene();
|
||||
for (const event of ['OnStart', 'OnEnable', 'OnUpdate', 'OnDisable', 'OnDestroy'])
|
||||
logOn(f, event, {}, event.slice(2).toLowerCase());
|
||||
return f;
|
||||
}
|
||||
|
||||
describe('lifecycle events', () => {
|
||||
it('fires On start, then On enable, then the first On update', () => {
|
||||
const { world, frame } = startWorld(lifecycle(), { behaviors: true });
|
||||
frame();
|
||||
expect(world.log).toEqual(['start', 'enable', 'update']);
|
||||
});
|
||||
|
||||
it('fires On disable when switched off and On enable – not On start – when switched back on', () => {
|
||||
const f = lifecycle();
|
||||
const { world, frame } = startWorld(f, { behaviors: true });
|
||||
const logic = world.entities.find((e) => e.name === 'Logic')!;
|
||||
frame();
|
||||
|
||||
logic.props.enabled = false;
|
||||
frame();
|
||||
frame();
|
||||
expect(world.log).toEqual(['start', 'enable', 'update', 'disable']);
|
||||
|
||||
logic.props.enabled = true;
|
||||
frame();
|
||||
expect(world.log).toEqual(['start', 'enable', 'update', 'disable', 'enable', 'update']);
|
||||
});
|
||||
|
||||
it('treats a parent being switched off the same way', () => {
|
||||
const f = lifecycle();
|
||||
const { world, frame } = startWorld(f, { behaviors: true });
|
||||
const box = world.findByObjectId(f.box)!;
|
||||
frame();
|
||||
|
||||
box.props.enabled = false;
|
||||
frame();
|
||||
expect(world.log).toEqual(['start', 'enable', 'update', 'disable']);
|
||||
box.props.enabled = true;
|
||||
frame();
|
||||
expect(world.log.slice(4)).toEqual(['enable', 'update']);
|
||||
});
|
||||
|
||||
it('fires On disable then On destroy when the owner is destroyed, in the same frame', () => {
|
||||
const f = lifecycle();
|
||||
// `On key down → Destroy(Owner)`: the behavior destroys the box it hangs below, itself included.
|
||||
const edit = graphEditor(f, f.behavior);
|
||||
const key = edit.addNode('OnKeyDown', { key: 'x' });
|
||||
const owner = edit.addNode('Owner');
|
||||
const destroy = edit.addNode('Destroy');
|
||||
edit.wire([
|
||||
[key, 'exec', destroy, 'exec'],
|
||||
[owner, 'object', destroy, 'object'],
|
||||
]);
|
||||
const { world, input, frame, behaviors } = startWorld(f, { behaviors: true });
|
||||
frame();
|
||||
expect(behaviors!.runningIds()).toHaveLength(1);
|
||||
|
||||
input.press('x');
|
||||
frame();
|
||||
// The frame that destroys it still runs to its end – the farewell comes after this frame's update.
|
||||
expect(world.log).toEqual(['start', 'enable', 'update', 'update', 'disable', 'destroy']);
|
||||
expect(behaviors!.runningIds()).toEqual([]);
|
||||
expect(world.entities.some((e) => e.name === 'Logic')).toBe(false);
|
||||
frame();
|
||||
expect(world.log).toHaveLength(6);
|
||||
});
|
||||
|
||||
it('skips On disable for a behavior destroyed while it was off', () => {
|
||||
const f = lifecycle();
|
||||
const { world, frame } = startWorld(f, { behaviors: true });
|
||||
const logic = world.entities.find((e) => e.name === 'Logic')!;
|
||||
logic.props.enabled = false;
|
||||
frame();
|
||||
world.remove(logic);
|
||||
frame();
|
||||
expect(world.log).toEqual(['start', 'enable', 'disable', 'destroy']);
|
||||
});
|
||||
|
||||
it('runs On disable and On destroy for every instance when the world is torn down', () => {
|
||||
const f = lifecycle();
|
||||
place(f, f.behavior.id, { x: 0, y: 0 });
|
||||
const { world, frame } = startWorld(f, { behaviors: true });
|
||||
frame();
|
||||
world.destroy();
|
||||
expect(world.log).toEqual([
|
||||
'start',
|
||||
'enable',
|
||||
'start',
|
||||
'enable',
|
||||
'update',
|
||||
'update',
|
||||
'disable',
|
||||
'destroy',
|
||||
'disable',
|
||||
'destroy',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -677,7 +677,10 @@ function eqNamesakes(): Record<string, Value> {
|
||||
const EVENT_NODES: Record<string, string> = {
|
||||
BehaviorStart: 'nested-behavior.test.ts',
|
||||
OnStart: 'graph.test.ts',
|
||||
OnEnable: 'behavior-events.test.ts',
|
||||
OnUpdate: 'graph.test.ts',
|
||||
OnDisable: 'behavior-events.test.ts',
|
||||
OnDestroy: 'behavior-events.test.ts',
|
||||
OnKeyDown: 'behavior-events.test.ts',
|
||||
OnKeyUp: 'behavior-events.test.ts',
|
||||
OnKeyHeld: 'behavior-events.test.ts',
|
||||
|
||||
+6
-2
@@ -239,6 +239,7 @@ export interface WorldOptions {
|
||||
export interface StartedWorld {
|
||||
world: World;
|
||||
collisions: CollisionSystem | null;
|
||||
behaviors: BehaviorSystem | null;
|
||||
/** Feed keys with `input.press('a')`; `frame()` clears the edges the way the editor does. */
|
||||
input: Input;
|
||||
/** One tick plus the end-of-frame bookkeeping the editor's play loop performs. */
|
||||
@@ -258,11 +259,14 @@ export function startWorld(fixture: SceneFixture, options: WorldOptions = {}): S
|
||||
const animations = options.animations ? new AnimationSystem() : null;
|
||||
if (collisions) world.addSystem(collisions);
|
||||
if (animations) world.addSystem(animations);
|
||||
if (options.behaviors)
|
||||
world.addSystem(new BehaviorSystem(fixture.project, input, { collisions, animations }));
|
||||
const behaviors = options.behaviors
|
||||
? new BehaviorSystem(fixture.project, input, { collisions, animations })
|
||||
: null;
|
||||
if (behaviors) world.addSystem(behaviors);
|
||||
return {
|
||||
world,
|
||||
collisions,
|
||||
behaviors,
|
||||
input,
|
||||
frame(dt = 1 / 60) {
|
||||
world.tick(dt);
|
||||
|
||||
Reference in new issue
Block a user