mirror of
https://github.com/nvms/prsm.git
synced 2025-12-16 08:00:53 +00:00
feat: "scene" (i.e. world) management
- Add tests to ensure predictable start/stop world behavior. - Update README to include example usages.
This commit is contained in:
parent
6e063101cc
commit
48d1205505
@ -11,6 +11,7 @@ An ECS framework (and robust input system) for the web.
|
|||||||
* [Entities](#entities)
|
* [Entities](#entities)
|
||||||
* [Components](#components)
|
* [Components](#components)
|
||||||
* [Extending components](#extending-components)
|
* [Extending components](#extending-components)
|
||||||
|
* [Scene Management](#scene-management)
|
||||||
* [Extras](#extras)
|
* [Extras](#extras)
|
||||||
* [Keyboard, mouse and gamepad input](#keyboard-mouse-and-gamepad-input)
|
* [Keyboard, mouse and gamepad input](#keyboard-mouse-and-gamepad-input)
|
||||||
* [Input system](#input-system)
|
* [Input system](#input-system)
|
||||||
@ -485,6 +486,37 @@ mortals((results) => {
|
|||||||
stop();
|
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`**
|
- **`World > step`**
|
||||||
|
|
||||||
Calls all systems once. Passes the `WorldState` to each system. You should do this in your main program loop, e.g.:
|
Calls all systems once. Passes the `WorldState` to each system. You should do this in your main program loop, e.g.:
|
||||||
|
|||||||
@ -2,4 +2,5 @@ import { describe } from "manten";
|
|||||||
|
|
||||||
await describe("ngn", async ({ runTestSuite }) => {
|
await describe("ngn", async ({ runTestSuite }) => {
|
||||||
runTestSuite(import("./ngn"));
|
runTestSuite(import("./ngn"));
|
||||||
|
runTestSuite(import("./ngn/scenes.test"));
|
||||||
});
|
});
|
||||||
|
|||||||
137
packages/ngn/src/tests/ngn/scenes.test.ts
Normal file
137
packages/ngn/src/tests/ngn/scenes.test.ts
Normal file
@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user