@vgai/engine 0.5.40 → 0.5.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/authoring.d.ts +6 -0
- package/dist/adapter/authoring.d.ts.map +1 -1
- package/dist/adapter/root-adapter.d.ts +16 -0
- package/dist/adapter/root-adapter.d.ts.map +1 -1
- package/dist/adapter/root-seam-contract.d.ts +5 -0
- package/dist/adapter/root-seam-contract.d.ts.map +1 -1
- package/dist/adapter/root-seam-contract.js +1 -0
- package/dist/ai/navigation.d.ts +30 -0
- package/dist/ai/navigation.d.ts.map +1 -1
- package/dist/ai/navigation.js +248 -15
- package/dist/ecs/user-data.d.ts +6 -0
- package/dist/ecs/user-data.d.ts.map +1 -1
- package/dist/ecs/user-data.js +2 -0
- package/dist/runtime/game.d.ts +13 -6
- package/dist/runtime/game.d.ts.map +1 -1
- package/dist/runtime/game.js +41 -6
- package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
- package/dist/world3d-react/r3f-root-factory.js +12 -5
- package/package.json +1 -1
- package/src/adapter/authoring.ts +6 -0
- package/src/adapter/root-adapter.ts +17 -0
- package/src/adapter/root-seam-contract.ts +1 -0
- package/src/ai/navigation.ts +298 -13
- package/src/ecs/user-data.ts +6 -0
- package/src/runtime/game.ts +55 -11
- package/src/world3d-react/r3f-root-factory.tsx +12 -5
package/src/runtime/game.ts
CHANGED
|
@@ -122,8 +122,9 @@ export type AdapterSurface = AdapterSurfaceLeaf;
|
|
|
122
122
|
* A world's per-phase frame hooks.
|
|
123
123
|
* Populated on a `RootInstance` only for first-party mounts — an opaque/
|
|
124
124
|
* foreign mount has no phase-partitioned entry point, so it stays
|
|
125
|
-
* `undefined` and the Game's frame executor
|
|
126
|
-
*
|
|
125
|
+
* `undefined` and the Game's frame executor falls back to its single
|
|
126
|
+
* `mounted.update(dt)`. The mount's `updateCadence` chooses fixed-substep
|
|
127
|
+
* (the default) or display-rate dispatch.
|
|
127
128
|
*/
|
|
128
129
|
export interface RootFrameHooks {
|
|
129
130
|
/** Run this world's engine systems + component ticks + world-bound game
|
|
@@ -639,8 +640,8 @@ export interface Game {
|
|
|
639
640
|
* sway, a cosmetic bob. It is NOT a gameplay hook: it can fire twice
|
|
640
641
|
* between two fixed substeps (120 Hz display, 60 Hz sim) and zero times
|
|
641
642
|
* across a `runTicks` fast-forward, so anything that must be deterministic
|
|
642
|
-
* belongs in a fixed phase (`ctx.systems.add
|
|
643
|
-
* `useFrame`
|
|
643
|
+
* belongs in a fixed engine phase (`ctx.systems.add`) instead. Ecosystem
|
|
644
|
+
* presentation hooks such as R3F's `useFrame` share this display clock.
|
|
644
645
|
*
|
|
645
646
|
* Reached from game code as `ctx.game?.onRenderStep(...)`. Returns an
|
|
646
647
|
* unsubscribe function; pass `opts.signal` to unsubscribe with the same
|
|
@@ -685,13 +686,17 @@ export interface GameInternal extends Game {
|
|
|
685
686
|
* for world in roots: // after ALL phases
|
|
686
687
|
* if world.mounted.drivesOwnLoop: continue
|
|
687
688
|
* if world.frame: world.frame.endFrame?.()
|
|
688
|
-
* else
|
|
689
|
+
* else if updateCadence != 'display' OR render phases are included:
|
|
690
|
+
* world.mounted.update?.(dt) // opaque world fallback
|
|
689
691
|
* ```
|
|
690
692
|
*
|
|
691
693
|
* A single first-party world and its direct `mounted.update(dt)` entry run
|
|
692
694
|
* the same `SystemRunner` and `postFrame` work. A `drivesOwnLoop` world is
|
|
693
695
|
* never ticked here. An opaque host-driven world (no `frame`) gets exactly
|
|
694
|
-
* one `update(dt)` call per substep
|
|
696
|
+
* one `update(dt)` call per substep after the phase loop by default. A
|
|
697
|
+
* display-cadence opaque world is withheld when `skipRenderPhases` is true;
|
|
698
|
+
* the matching host calls it once from {@link runRenderFrame}. A direct
|
|
699
|
+
* `runFrame` with render phases included still calls it once.
|
|
695
700
|
*
|
|
696
701
|
* D10/T7.6 play-state addendum: when `Game.play.paused` is true, every
|
|
697
702
|
* `pausable` (and non-`drivesOwnLoop`) world skips every phase EXCEPT
|
|
@@ -729,6 +734,8 @@ export interface GameInternal extends Game {
|
|
|
729
734
|
* for world in roots (declaration order):
|
|
730
735
|
* if world.mounted.drivesOwnLoop: continue
|
|
731
736
|
* world.frame?.runPhase(phase, frozen ? 0 : displayDt)
|
|
737
|
+
* for opaque world with updateCadence == 'display':
|
|
738
|
+
* world.mounted.update?.(frozen ? 0 : displayDt)
|
|
732
739
|
* ```
|
|
733
740
|
*
|
|
734
741
|
* Advances NOTHING: no `tick`, no `simT`, no sim-clock flush, no state-bridge
|
|
@@ -1081,7 +1088,12 @@ export function createGame(opts: {
|
|
|
1081
1088
|
);
|
|
1082
1089
|
}
|
|
1083
1090
|
}
|
|
1084
|
-
|
|
1091
|
+
// A native entry's static `systems` export is this root's primary
|
|
1092
|
+
// declaration surface. `computeSystemAdapters()` already gives it
|
|
1093
|
+
// precedence over a substrate-seeded mounted adapter; pause must read
|
|
1094
|
+
// the same source or a valid entry-declared audio graph is visible to
|
|
1095
|
+
// the editor yet paradoxically reported as ungateable here.
|
|
1096
|
+
const audio = declaredSystemAdapters.get(world.id)?.audio ?? world.mounted.systems?.audio;
|
|
1085
1097
|
if (audio) {
|
|
1086
1098
|
audio.setMuted(next);
|
|
1087
1099
|
} else if (
|
|
@@ -1288,10 +1300,21 @@ export function createGame(opts: {
|
|
|
1288
1300
|
// Checklist item 2: same isolation for the opaque-world fallback —
|
|
1289
1301
|
// one foreign mount's `update` throwing must not starve its
|
|
1290
1302
|
// siblings' `endFrame`/`update` calls this same loop.
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1303
|
+
// A display-cadence opaque update is the root's indivisible native
|
|
1304
|
+
// presentation (R3F `advance()` is callbacks + draw). The real
|
|
1305
|
+
// rAF host withholds render phases from EVERY fixed catch-up step;
|
|
1306
|
+
// withhold this update beside them, then run it once in
|
|
1307
|
+
// `runRenderFrameImpl`. A direct/offline `runFrame` includes render
|
|
1308
|
+
// phases and therefore still advances the root exactly once.
|
|
1309
|
+
if (!(skipRenderPhases && world.mounted.updateCadence === 'display')) {
|
|
1310
|
+
try {
|
|
1311
|
+
world.mounted.update?.(dt);
|
|
1312
|
+
} catch (err) {
|
|
1313
|
+
console.error(
|
|
1314
|
+
`[game] world "${world.id}" (kind: ${world.kind}) update() threw:`,
|
|
1315
|
+
err,
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1295
1318
|
}
|
|
1296
1319
|
}
|
|
1297
1320
|
}
|
|
@@ -1390,6 +1413,27 @@ export function createGame(opts: {
|
|
|
1390
1413
|
profiler.endPhase(phase);
|
|
1391
1414
|
}
|
|
1392
1415
|
|
|
1416
|
+
// An opaque display-cadence root cannot expose phase hooks because its
|
|
1417
|
+
// native advance is inseparable. It still belongs to THIS presentation
|
|
1418
|
+
// frame, once — never to every fixed catch-up substep that preceded it.
|
|
1419
|
+
// A frozen world redraws at dt=0, preserving the established pause rule.
|
|
1420
|
+
for (let i = 0; i < n; i++) {
|
|
1421
|
+
const world = roots[i]!;
|
|
1422
|
+
if (world.disposed) continue;
|
|
1423
|
+
if (world.mounted.drivesOwnLoop) continue;
|
|
1424
|
+
if (world.frame) continue;
|
|
1425
|
+
if (world.mounted.updateCadence !== 'display') continue;
|
|
1426
|
+
const frozen = paused && world.pausable;
|
|
1427
|
+
try {
|
|
1428
|
+
world.mounted.update?.(frozen ? 0 : displayDt);
|
|
1429
|
+
} catch (err) {
|
|
1430
|
+
console.error(
|
|
1431
|
+
`[game] world "${world.id}" (kind: ${world.kind}) update() threw (display frame):`,
|
|
1432
|
+
err,
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1393
1437
|
profiler.endFrame();
|
|
1394
1438
|
if (rngTrapEnabled) rngTrap.disable();
|
|
1395
1439
|
}
|
|
@@ -348,11 +348,10 @@ function threeWorldAdapter(id: string, component: ComponentType): RootAdapter {
|
|
|
348
348
|
if (renderDebugWiring) systemAdapters.renderDebug = renderDebugWiring.adapter;
|
|
349
349
|
|
|
350
350
|
// The engine drives every `useFrame` through the mounted world's
|
|
351
|
-
// `update(dt)` hook, never off a raw
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
// once.
|
|
351
|
+
// `update(dt)` hook, never off a raw Fiber loop. This mount declares
|
|
352
|
+
// that indivisible Fiber advance at DISPLAY cadence below: the real
|
|
353
|
+
// host calls it once per presentation, a paused world gets dt=0, and an
|
|
354
|
+
// explicit `Game.play.step()` still ticks it exactly once.
|
|
356
355
|
//
|
|
357
356
|
// `advance(timestamp, runGlobalEffects, state)`'s `timestamp` is
|
|
358
357
|
// consumed as `THREE.Clock.elapsedTime` DIRECTLY when
|
|
@@ -385,6 +384,14 @@ function threeWorldAdapter(id: string, component: ComponentType): RootAdapter {
|
|
|
385
384
|
return live().camera;
|
|
386
385
|
},
|
|
387
386
|
drivesOwnLoop: false,
|
|
387
|
+
// Fiber's `advance()` cannot separate its `useFrame` callbacks from
|
|
388
|
+
// its WebGL draw. Running this opaque update in every fixed catch-up
|
|
389
|
+
// substep renders N complete frames inside one rAF callback: once a
|
|
390
|
+
// scene costs more than 16.7ms, the accumulator asks for more draws,
|
|
391
|
+
// those draws make the next gap larger, and the loop spirals to its
|
|
392
|
+
// max-substep ceiling. The host's display pass is the one clock that
|
|
393
|
+
// must own this inseparable native advance.
|
|
394
|
+
updateCadence: 'display',
|
|
388
395
|
// Adapter surface: `debug` (the shared game registry) and, when a real
|
|
389
396
|
// WebGL2 context exists, `renderDebug` are engine-seeded. The game's
|
|
390
397
|
// own capabilities arrive as the entry module's declared `systems`.
|