From 48d120550528aab43f345e3e71191ee67a37af51 Mon Sep 17 00:00:00 2001 From: nvms Date: Thu, 27 Mar 2025 19:13:05 -0400 Subject: [PATCH] feat: "scene" (i.e. world) management - Add tests to ensure predictable start/stop world behavior. - Update README to include example usages. --- packages/ngn/README.md | 32 +++++ packages/ngn/src/tests/index.ts | 1 + packages/ngn/src/tests/ngn/scenes.test.ts | 137 ++++++++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 packages/ngn/src/tests/ngn/scenes.test.ts diff --git a/packages/ngn/README.md b/packages/ngn/README.md index f658b12..a533235 100644 --- a/packages/ngn/README.md +++ b/packages/ngn/README.md @@ -11,6 +11,7 @@ An ECS framework (and robust input system) for the web. * [Entities](#entities) * [Components](#components) * [Extending components](#extending-components) + * [Scene Management](#scene-management) * [Extras](#extras) * [Keyboard, mouse and gamepad input](#keyboard-mouse-and-gamepad-input) * [Input system](#input-system) @@ -485,6 +486,37 @@ mortals((results) => { stop(); ``` +### Scene Management + +NGN doesn't enforce any specific scene management pattern, giving you the freedom to implement what works best for your game. The simplest approach is to use separate worlds as scenes: + +```typescript +// Create different worlds for different scenes +const menuScene = createWorld(); +const gameScene = createWorld(); +const pauseScene = createWorld(); + +// Configure each scene +menuScene.defineMain(() => { /* menu logic */ }); +gameScene.defineMain(() => { /* game logic */ }); + +// Track current scene +let currentScene = menuScene; + +// Switch scenes +function switchToScene(newScene) { + currentScene.stop(); + currentScene = newScene; + currentScene.start(); +} + +// Start with menu +menuScene.start(); + +// Later, switch to game +switchToScene(gameScene); +``` + - **`World > step`** Calls all systems once. Passes the `WorldState` to each system. You should do this in your main program loop, e.g.: diff --git a/packages/ngn/src/tests/index.ts b/packages/ngn/src/tests/index.ts index 447977f..11d766d 100644 --- a/packages/ngn/src/tests/index.ts +++ b/packages/ngn/src/tests/index.ts @@ -2,4 +2,5 @@ import { describe } from "manten"; await describe("ngn", async ({ runTestSuite }) => { runTestSuite(import("./ngn")); + runTestSuite(import("./ngn/scenes.test")); }); diff --git a/packages/ngn/src/tests/ngn/scenes.test.ts b/packages/ngn/src/tests/ngn/scenes.test.ts new file mode 100644 index 0000000..3543ef1 --- /dev/null +++ b/packages/ngn/src/tests/ngn/scenes.test.ts @@ -0,0 +1,137 @@ +import { expect, test, testSuite } from "manten"; +import { createWorld, WorldState } from "../../ngn"; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export default testSuite(async () => { + test("can switch between worlds (scenes)", async () => { + // Create two separate worlds (scenes) + const sceneA = createWorld(); + const sceneB = createWorld(); + + // Track execution counts for each scene + let sceneAExecutions = 0; + let sceneBExecutions = 0; + + // Set up main loops for each scene + sceneA.defineMain((state: WorldState) => { + sceneAExecutions++; + if (sceneAExecutions >= 3) sceneA.stop(); + }); + + sceneB.defineMain((state: WorldState) => { + sceneBExecutions++; + if (sceneBExecutions >= 3) sceneB.stop(); + }); + + // Start sceneA and let it run for a bit + sceneA.start(); + await sleep(500); + + // Verify sceneA ran and sceneB didn't + expect(sceneAExecutions).toBe(3); + expect(sceneBExecutions).toBe(0); + + // Start sceneB and let it run + sceneB.start(); + await sleep(500); + + // Verify sceneB ran + expect(sceneBExecutions).toBe(3); + }); + + test("worlds maintain separate entity collections", () => { + const sceneA = createWorld(); + const sceneB = createWorld(); + + // Create entities in each scene + const entityA = sceneA.createEntity({ name: "EntityA" }); + const entityB = sceneB.createEntity({ name: "EntityB" }); + + // Verify entities exist in their respective scenes + expect(sceneA.getEntity(entityA.id)).toBeDefined(); + expect(sceneA.getEntity(entityB.id)).toBeUndefined(); + + expect(sceneB.getEntity(entityB.id)).toBeDefined(); + expect(sceneB.getEntity(entityA.id)).toBeUndefined(); + }); + + test("worlds maintain separate system collections", () => { + const sceneA = createWorld(); + const sceneB = createWorld(); + + // Track system executions + let systemAExecutions = 0; + let systemBExecutions = 0; + + // Create systems for each scene + const systemA = () => { + systemAExecutions++; + }; + const systemB = () => { + systemBExecutions++; + }; + + // Add systems to their respective scenes + sceneA.addSystem(systemA); + sceneB.addSystem(systemB); + + // Step each scene + sceneA.step(); + sceneB.step(); + + // Verify systems ran in their respective scenes + expect(systemAExecutions).toBe(1); + expect(systemBExecutions).toBe(1); + + // Remove system from sceneA + sceneA.removeSystem(systemA); + + // Step each scene again + sceneA.step(); + sceneB.step(); + + // Verify systemA didn't run but systemB did + expect(systemAExecutions).toBe(1); + expect(systemBExecutions).toBe(2); + }); + + test("worlds maintain separate time tracking", async () => { + const sceneA = createWorld(); + const sceneB = createWorld(); + + // Set different time scales + sceneA.state.time.scale = 0.5; + sceneB.state.time.scale = 2.0; + + let sceneATime = 0; + let sceneBTime = 0; + + // Set up main loops to capture time values + sceneA.defineMain((state: WorldState) => { + sceneATime = state.time.delta; + sceneA.stop(); + }); + + sceneB.defineMain((state: WorldState) => { + sceneBTime = state.time.delta; + sceneB.stop(); + }); + + // Run both scenes + sceneA.start(); + await sleep(200); + + sceneB.start(); + await sleep(200); + + // Verify time scales were applied correctly + expect(sceneATime).toBeGreaterThan(0); + expect(sceneBTime).toBeGreaterThan(0); + expect(sceneBTime).toBeGreaterThan(sceneATime); + + // Verify the ratio is approximately 4:1 (2.0 vs 0.5) + const ratio = sceneBTime / sceneATime; + expect(ratio).toBeGreaterThan(3); // Allow some flexibility in timing + }); +});