@needle-tools/engine 5.0.3 → 5.1.0-alpha.1
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/CHANGELOG.md +31 -4
- package/README.md +6 -7
- package/components.needle.json +1 -1
- package/dist/{needle-engine.bundle-BXk8jYW3.js → needle-engine.bundle-BGyKqxBH.js} +12394 -11786
- package/dist/needle-engine.bundle-CiYtOO2O.min.js +1732 -0
- package/dist/needle-engine.bundle-DzVx9Z8D.umd.cjs +1732 -0
- package/dist/needle-engine.d.ts +660 -63
- package/dist/needle-engine.js +579 -566
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/dist/{vendor-vHLk8sXu.js → vendor-CAcsI0eU.js} +116 -115
- package/dist/{vendor-CntUvmJu.umd.cjs → vendor-CEM38hLE.umd.cjs} +2 -2
- package/dist/{vendor-DPbfJJ4d.min.js → vendor-HRlxIBga.min.js} +2 -2
- package/lib/engine/api.d.ts +2 -0
- package/lib/engine/api.js +2 -0
- package/lib/engine/api.js.map +1 -1
- package/lib/engine/engine_addressables.js +5 -1
- package/lib/engine/engine_addressables.js.map +1 -1
- package/lib/engine/engine_animation.d.ts +14 -7
- package/lib/engine/engine_animation.js +49 -9
- package/lib/engine/engine_animation.js.map +1 -1
- package/lib/engine/engine_components.js +33 -4
- package/lib/engine/engine_components.js.map +1 -1
- package/lib/engine/engine_context.d.ts +7 -2
- package/lib/engine/engine_context.js +10 -2
- package/lib/engine/engine_context.js.map +1 -1
- package/lib/engine/engine_gameobject.d.ts +4 -0
- package/lib/engine/engine_gameobject.js.map +1 -1
- package/lib/engine/engine_init.js +4 -0
- package/lib/engine/engine_init.js.map +1 -1
- package/lib/engine/engine_input.js +4 -1
- package/lib/engine/engine_input.js.map +1 -1
- package/lib/engine/engine_materialpropertyblock.js +0 -19
- package/lib/engine/engine_materialpropertyblock.js.map +1 -1
- package/lib/engine/engine_networking.d.ts +11 -8
- package/lib/engine/engine_networking.js +43 -26
- package/lib/engine/engine_networking.js.map +1 -1
- package/lib/engine/engine_networking_instantiate.d.ts +100 -5
- package/lib/engine/engine_networking_instantiate.js +150 -16
- package/lib/engine/engine_networking_instantiate.js.map +1 -1
- package/lib/engine/engine_networking_prefabs.d.ts +59 -0
- package/lib/engine/engine_networking_prefabs.js +67 -0
- package/lib/engine/engine_networking_prefabs.js.map +1 -0
- package/lib/engine/engine_physics_rapier.d.ts +3 -0
- package/lib/engine/engine_physics_rapier.js +13 -9
- package/lib/engine/engine_physics_rapier.js.map +1 -1
- package/lib/engine/engine_utils.js +2 -2
- package/lib/engine/engine_utils.js.map +1 -1
- package/lib/engine/postprocessing/api.d.ts +2 -0
- package/lib/engine/postprocessing/api.js +2 -0
- package/lib/engine/postprocessing/api.js.map +1 -0
- package/lib/engine/postprocessing/index.d.ts +2 -0
- package/lib/engine/postprocessing/index.js +2 -0
- package/lib/engine/postprocessing/index.js.map +1 -0
- package/lib/engine/postprocessing/postprocessing.d.ts +83 -0
- package/lib/engine/postprocessing/postprocessing.js +280 -0
- package/lib/engine/postprocessing/postprocessing.js.map +1 -0
- package/lib/engine/postprocessing/types.d.ts +39 -0
- package/lib/engine/postprocessing/types.js +2 -0
- package/lib/engine/postprocessing/types.js.map +1 -0
- package/lib/engine/webcomponents/WebXRButtons.js +17 -3
- package/lib/engine/webcomponents/WebXRButtons.js.map +1 -1
- package/lib/engine/xr/NeedleXRSession.d.ts +2 -0
- package/lib/engine/xr/NeedleXRSession.js +47 -14
- package/lib/engine/xr/NeedleXRSession.js.map +1 -1
- package/lib/engine/xr/events.d.ts +30 -3
- package/lib/engine/xr/events.js +38 -0
- package/lib/engine/xr/events.js.map +1 -1
- package/lib/engine/xr/init.d.ts +4 -0
- package/lib/engine/xr/init.js +43 -0
- package/lib/engine/xr/init.js.map +1 -0
- package/lib/engine-components/AnimationUtils.d.ts +4 -1
- package/lib/engine-components/AnimationUtils.js +7 -19
- package/lib/engine-components/AnimationUtils.js.map +1 -1
- package/lib/engine-components/AnimatorController.d.ts +135 -2
- package/lib/engine-components/AnimatorController.js +216 -13
- package/lib/engine-components/AnimatorController.js.map +1 -1
- package/lib/engine-components/SeeThrough.d.ts +0 -2
- package/lib/engine-components/SeeThrough.js +0 -89
- package/lib/engine-components/SeeThrough.js.map +1 -1
- package/lib/engine-components/SyncedRoom.d.ts +4 -0
- package/lib/engine-components/SyncedRoom.js +23 -8
- package/lib/engine-components/SyncedRoom.js.map +1 -1
- package/lib/engine-components/SyncedTransform.js +5 -5
- package/lib/engine-components/SyncedTransform.js.map +1 -1
- package/lib/engine-components/Voip.d.ts +46 -0
- package/lib/engine-components/Voip.js +126 -2
- package/lib/engine-components/Voip.js.map +1 -1
- package/lib/engine-components/api.d.ts +1 -0
- package/lib/engine-components/api.js +1 -0
- package/lib/engine-components/api.js.map +1 -1
- package/lib/engine-components/codegen/components.d.ts +1 -0
- package/lib/engine-components/codegen/components.js +1 -0
- package/lib/engine-components/codegen/components.js.map +1 -1
- package/lib/engine-components/postprocessing/Effects/Tonemapping.d.ts +5 -2
- package/lib/engine-components/postprocessing/Effects/Tonemapping.js +11 -18
- package/lib/engine-components/postprocessing/Effects/Tonemapping.js.map +1 -1
- package/lib/engine-components/postprocessing/PostProcessingEffect.d.ts +3 -4
- package/lib/engine-components/postprocessing/PostProcessingEffect.js +6 -15
- package/lib/engine-components/postprocessing/PostProcessingEffect.js.map +1 -1
- package/lib/engine-components/postprocessing/PostProcessingHandler.d.ts +2 -1
- package/lib/engine-components/postprocessing/PostProcessingHandler.js.map +1 -1
- package/lib/engine-components/postprocessing/Volume.d.ts +18 -11
- package/lib/engine-components/postprocessing/Volume.js +61 -140
- package/lib/engine-components/postprocessing/Volume.js.map +1 -1
- package/lib/engine-components/postprocessing/index.d.ts +1 -0
- package/lib/engine-components/postprocessing/index.js +1 -0
- package/lib/engine-components/postprocessing/index.js.map +1 -1
- package/lib/engine-components/postprocessing/utils.d.ts +2 -0
- package/lib/engine-components/postprocessing/utils.js +2 -0
- package/lib/engine-components/postprocessing/utils.js.map +1 -1
- package/lib/engine-components/ui/Canvas.js +2 -2
- package/lib/engine-components/ui/Canvas.js.map +1 -1
- package/lib/engine-components/ui/Graphic.d.ts +3 -3
- package/lib/engine-components/ui/Graphic.js +6 -2
- package/lib/engine-components/ui/Graphic.js.map +1 -1
- package/lib/engine-components/ui/Text.d.ts +64 -11
- package/lib/engine-components/ui/Text.js +154 -45
- package/lib/engine-components/ui/Text.js.map +1 -1
- package/lib/engine-components/ui/index.d.ts +1 -0
- package/lib/engine-components/ui/index.js +1 -0
- package/lib/engine-components/ui/index.js.map +1 -1
- package/lib/engine-components-experimental/networking/PlayerSync.d.ts +25 -3
- package/lib/engine-components-experimental/networking/PlayerSync.js +60 -11
- package/lib/engine-components-experimental/networking/PlayerSync.js.map +1 -1
- package/package.json +5 -4
- package/plugins/common/logger.js +42 -19
- package/plugins/vite/ai.d.ts +11 -10
- package/plugins/vite/ai.js +305 -31
- package/plugins/vite/logger.client.js +4 -3
- package/src/engine/api.ts +3 -0
- package/src/engine/engine_addressables.ts +4 -1
- package/src/engine/engine_animation.ts +47 -9
- package/src/engine/engine_components.ts +36 -7
- package/src/engine/engine_context.ts +11 -2
- package/src/engine/engine_gameobject.ts +5 -0
- package/src/engine/engine_init.ts +4 -0
- package/src/engine/engine_input.ts +2 -1
- package/src/engine/engine_materialpropertyblock.ts +0 -19
- package/src/engine/engine_networking.ts +46 -23
- package/src/engine/engine_networking_instantiate.ts +160 -18
- package/src/engine/engine_networking_prefabs.ts +80 -0
- package/src/engine/engine_physics_rapier.ts +14 -9
- package/src/engine/engine_utils.ts +2 -2
- package/src/engine/postprocessing/api.ts +2 -0
- package/src/engine/postprocessing/index.ts +2 -0
- package/src/engine/postprocessing/postprocessing.ts +322 -0
- package/src/engine/postprocessing/types.ts +43 -0
- package/src/engine/webcomponents/WebXRButtons.ts +21 -4
- package/src/engine/xr/NeedleXRSession.ts +55 -20
- package/src/engine/xr/events.ts +44 -1
- package/src/engine/xr/init.ts +49 -0
- package/src/engine-components/AnimationUtils.ts +7 -17
- package/src/engine-components/AnimatorController.ts +288 -18
- package/src/engine-components/SeeThrough.ts +0 -116
- package/src/engine-components/SyncedRoom.ts +28 -9
- package/src/engine-components/SyncedTransform.ts +5 -5
- package/src/engine-components/Voip.ts +129 -2
- package/src/engine-components/api.ts +1 -0
- package/src/engine-components/codegen/components.ts +1 -0
- package/src/engine-components/postprocessing/Effects/Tonemapping.ts +16 -24
- package/src/engine-components/postprocessing/PostProcessingEffect.ts +9 -16
- package/src/engine-components/postprocessing/PostProcessingHandler.ts +2 -1
- package/src/engine-components/postprocessing/Volume.ts +72 -163
- package/src/engine-components/postprocessing/index.ts +1 -0
- package/src/engine-components/postprocessing/utils.ts +2 -0
- package/src/engine-components/ui/Canvas.ts +2 -2
- package/src/engine-components/ui/Graphic.ts +7 -3
- package/src/engine-components/ui/Text.ts +170 -52
- package/src/engine-components/ui/index.ts +2 -1
- package/src/engine-components-experimental/networking/PlayerSync.ts +64 -11
- package/dist/needle-engine.bundle-CNH61kLA.umd.cjs +0 -1730
- package/dist/needle-engine.bundle-Dvh1jROn.min.js +0 -1730
|
@@ -8,6 +8,7 @@ import { activeInHierarchyFieldName } from "./engine_constants.js";
|
|
|
8
8
|
import { editorGuidKeyName } from "./engine_constants.js";
|
|
9
9
|
import { $isUsingInstancing, InstancingUtil } from "./engine_instancing.js";
|
|
10
10
|
import { processNewScripts } from "./engine_mainloop_utils.js";
|
|
11
|
+
import type { syncInstantiate } from "./engine_networking_instantiate.js";
|
|
11
12
|
import { InstantiateIdProvider } from "./engine_networking_instantiate.js";
|
|
12
13
|
import { assign, ISerializable } from "./engine_serialization_core.js";
|
|
13
14
|
import { Context, registerComponent } from "./engine_setup.js";
|
|
@@ -19,7 +20,11 @@ import { apply } from "./js-extensions/index.js";
|
|
|
19
20
|
const debug = getParam("debuggetcomponent");
|
|
20
21
|
const debugInstantiate = getParam("debuginstantiate");
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Options for instantiating a GameObject, used in {@link instantiate} and {@link syncInstantiate}
|
|
25
|
+
*/
|
|
22
26
|
export type IInstantiateOptions = {
|
|
27
|
+
/** The ID provider for generating unique IDs / guids */
|
|
23
28
|
idProvider?: UIDProvider;
|
|
24
29
|
//** parent guid or object */
|
|
25
30
|
parent?: string | Object3D;
|
|
@@ -12,6 +12,8 @@ import { patchLayers } from "./js-extensions/Layers.js";
|
|
|
12
12
|
import { initObject3DExtensions } from "./js-extensions/Object3D.js";
|
|
13
13
|
import { initVectorExtensions } from "./js-extensions/Vector.js";
|
|
14
14
|
import { initWebComponents } from "./webcomponents/init.js";
|
|
15
|
+
import { initPhysics } from "./engine_physics_rapier.js";
|
|
16
|
+
import { initXR } from "./xr/init.js";
|
|
15
17
|
|
|
16
18
|
let initialized = false;
|
|
17
19
|
|
|
@@ -42,4 +44,6 @@ export function initEngine() {
|
|
|
42
44
|
initAnimationAutoplay();
|
|
43
45
|
initSkyboxAttributes();
|
|
44
46
|
initSceneSwitcherAttributes();
|
|
47
|
+
initPhysics();
|
|
48
|
+
initXR();
|
|
45
49
|
}
|
|
@@ -1088,7 +1088,8 @@ export class Input implements IInput {
|
|
|
1088
1088
|
if (this.context.isInAR) return;
|
|
1089
1089
|
if (this.canReceiveInput(evt) === false) return;
|
|
1090
1090
|
if (evt.target instanceof HTMLElement) {
|
|
1091
|
-
evt.target.setPointerCapture(evt.pointerId);
|
|
1091
|
+
try { evt.target.setPointerCapture(evt.pointerId); }
|
|
1092
|
+
catch { /* may fail during pointer lock */ }
|
|
1092
1093
|
}
|
|
1093
1094
|
const id = this.getPointerId(evt);
|
|
1094
1095
|
if (debug) showBalloonMessage(`pointer down #${id}, identifier:${evt.pointerId}`);
|
|
@@ -560,25 +560,6 @@ const $savedTextureTransforms = Symbol("savedTextureTransforms");
|
|
|
560
560
|
*/
|
|
561
561
|
type ObjectRenderCallback = (this: Object3D, renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, material: Material, group: Group) => void;
|
|
562
562
|
|
|
563
|
-
/**
|
|
564
|
-
* Collect all materials from an object and its children
|
|
565
|
-
* @internal
|
|
566
|
-
*/
|
|
567
|
-
function collectMaterials(object: Object3D, materials: Set<Material>): void {
|
|
568
|
-
const obj = object as Object3D & { material?: Material | Material[] };
|
|
569
|
-
if (obj.material) {
|
|
570
|
-
if (Array.isArray(obj.material)) {
|
|
571
|
-
obj.material.forEach(mat => materials.add(mat));
|
|
572
|
-
} else {
|
|
573
|
-
materials.add(obj.material);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// For Groups, collect materials from children too
|
|
578
|
-
if (object.type === "Group") {
|
|
579
|
-
object.children.forEach(child => collectMaterials(child, materials));
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
563
|
|
|
583
564
|
/**
|
|
584
565
|
* Find property block by checking this object and parent (if parent is a Group).
|
|
@@ -248,6 +248,7 @@ export class OwnershipModel {
|
|
|
248
248
|
private _gainSubscription: (response: GainedOwnershipBroadcastResponse) => void;
|
|
249
249
|
private _lostSubscription: (guid: string) => void;
|
|
250
250
|
private _hasOwnerResponse: (response: OwnershipResponse) => void;
|
|
251
|
+
private _pendingOwnershipResolve: ((value: boolean) => void) | null = null;
|
|
251
252
|
|
|
252
253
|
constructor(connection: NetworkConnection, guid: string) {
|
|
253
254
|
this.connection = connection;
|
|
@@ -291,7 +292,6 @@ export class OwnershipModel {
|
|
|
291
292
|
}
|
|
292
293
|
|
|
293
294
|
private waitForHasOwnershipRequestResponse(res: OwnershipResponse) {
|
|
294
|
-
// console.log(res);
|
|
295
295
|
if (res.guid === this.guid) {
|
|
296
296
|
if (this._isWaitingForOwnershipResponseCallback) {
|
|
297
297
|
this.connection.stopListen(OwnershipEvent.ResponseHasOwner, this._isWaitingForOwnershipResponseCallback);
|
|
@@ -301,47 +301,58 @@ export class OwnershipModel {
|
|
|
301
301
|
if (!res.value) {
|
|
302
302
|
if (debugOwner)
|
|
303
303
|
console.log("request ownership", this.guid)
|
|
304
|
-
this.
|
|
304
|
+
this.connection.send(OwnershipEvent.RequestOwnership, { guid: this.guid });
|
|
305
305
|
}
|
|
306
306
|
}
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
|
|
310
|
+
|
|
310
311
|
/**
|
|
311
312
|
* Requests ownership and waits asynchronously until ownership is granted or timeout occurs.
|
|
312
313
|
* @returns Promise that resolves with this OwnershipModel when ownership is gained
|
|
313
314
|
* @throws Rejects with "Timeout" if ownership is not gained within ~1 second
|
|
314
315
|
* @example
|
|
315
316
|
* ```ts
|
|
316
|
-
*
|
|
317
|
-
*
|
|
317
|
+
* const owned = await ownership.requestOwnership();
|
|
318
|
+
* if (owned) {
|
|
318
319
|
* // Ownership granted, safe to modify object
|
|
319
|
-
* } catch(e) {
|
|
320
|
-
* console.warn("Could not gain ownership:", e);
|
|
321
320
|
* }
|
|
322
321
|
* ```
|
|
323
322
|
*/
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
323
|
+
/**
|
|
324
|
+
* Requests ownership of this object from the networking server.
|
|
325
|
+
* Returns a Promise that resolves with `true` when ownership is granted, or `false` on timeout/failure.
|
|
326
|
+
* If ownership is already held, resolves immediately with `true`.
|
|
327
|
+
* @returns Promise that resolves with `true` if ownership was gained, `false` otherwise
|
|
328
|
+
*/
|
|
329
|
+
public requestOwnership(): Promise<boolean> {
|
|
330
|
+
if (this._hasOwnership) return Promise.resolve(true);
|
|
331
|
+
|
|
332
|
+
// Cancel any previous pending request
|
|
333
|
+
this._pendingOwnershipResolve?.(false);
|
|
334
|
+
this._pendingOwnershipResolve = null;
|
|
335
|
+
|
|
336
|
+
if (debugOwner) console.log("Request ownership", this.guid);
|
|
337
|
+
this.connection.send(OwnershipEvent.RequestOwnership, { guid: this.guid });
|
|
338
|
+
|
|
339
|
+
return new Promise((resolve) => {
|
|
340
|
+
this._pendingOwnershipResolve = resolve;
|
|
341
|
+
|
|
342
|
+
// Timeout after ~2 seconds
|
|
343
|
+
setTimeout(() => {
|
|
344
|
+
if (this._pendingOwnershipResolve === resolve) {
|
|
345
|
+
this._pendingOwnershipResolve = null;
|
|
346
|
+
resolve(false);
|
|
347
|
+
}
|
|
348
|
+
}, 2000);
|
|
336
349
|
});
|
|
337
350
|
}
|
|
338
351
|
|
|
339
352
|
/**
|
|
340
|
-
*
|
|
341
|
-
* Ownership may not be granted immediately - check `hasOwnership` property or use `requestOwnershipAsync()`.
|
|
342
|
-
* @returns this OwnershipModel instance for method chaining
|
|
353
|
+
* @deprecated Use `requestOwnership()` instead for a more reliable way to gain ownership
|
|
343
354
|
*/
|
|
344
|
-
public
|
|
355
|
+
public async requestOwnershipAsync(): Promise<OwnershipModel> {
|
|
345
356
|
if (debugOwner) console.log("Request ownership", this.guid);
|
|
346
357
|
this.connection.send(OwnershipEvent.RequestOwnership, { guid: this.guid });
|
|
347
358
|
return this;
|
|
@@ -374,16 +385,23 @@ export class OwnershipModel {
|
|
|
374
385
|
this.connection.stopListen(OwnershipEvent.ResponseHasOwner, this._isWaitingForOwnershipResponseCallback);
|
|
375
386
|
this._isWaitingForOwnershipResponseCallback = null;
|
|
376
387
|
}
|
|
388
|
+
// Clean up pending ownership request
|
|
389
|
+
this._pendingOwnershipResolve?.(false);
|
|
390
|
+
this._pendingOwnershipResolve = null;
|
|
377
391
|
}
|
|
378
392
|
|
|
379
393
|
private onGainedOwnership(res: GainedOwnershipBroadcastResponse) {
|
|
380
394
|
if (res.guid === this.guid) {
|
|
381
395
|
this._isOwned = true;
|
|
382
|
-
// console.log(res.owner, connection.connectionId)
|
|
383
396
|
if (this.connection.connectionId === res.owner) {
|
|
384
397
|
if (debugOwner)
|
|
385
398
|
console.log("GAINED OWNERSHIP", this.guid)
|
|
386
399
|
this._hasOwnership = true;
|
|
400
|
+
// Resolve pending ownership request
|
|
401
|
+
if (this._pendingOwnershipResolve) {
|
|
402
|
+
this._pendingOwnershipResolve(true);
|
|
403
|
+
this._pendingOwnershipResolve = null;
|
|
404
|
+
}
|
|
387
405
|
}
|
|
388
406
|
else this._hasOwnership = false;
|
|
389
407
|
}
|
|
@@ -394,6 +412,11 @@ export class OwnershipModel {
|
|
|
394
412
|
console.log("LOST OWNERSHIP", this.guid)
|
|
395
413
|
this._hasOwnership = false;
|
|
396
414
|
this._isOwned = false;
|
|
415
|
+
// Resolve pending ownership request as failed
|
|
416
|
+
if (this._pendingOwnershipResolve) {
|
|
417
|
+
this._pendingOwnershipResolve(false);
|
|
418
|
+
this._pendingOwnershipResolve = null;
|
|
419
|
+
}
|
|
397
420
|
}
|
|
398
421
|
}
|
|
399
422
|
}
|
|
@@ -11,7 +11,7 @@ import type { INetworkConnection } from "./engine_networking_types.js";
|
|
|
11
11
|
import type { IModel } from "./engine_networking_types.js";
|
|
12
12
|
import { SendQueue } from "./engine_networking_types.js";
|
|
13
13
|
import { Context } from "./engine_setup.js"
|
|
14
|
-
import type { IComponent as Component, IGameObject as GameObject } from "./engine_types.js"
|
|
14
|
+
import type { IComponent as Component, IContext,IGameObject as GameObject } from "./engine_types.js"
|
|
15
15
|
import type { UIDProvider } from "./engine_types.js";
|
|
16
16
|
import * as utils from "./engine_utils.js"
|
|
17
17
|
|
|
@@ -154,13 +154,46 @@ export function sendDestroyed(guid: string, con: INetworkConnection, opts?: Sync
|
|
|
154
154
|
con.send(InstantiateEvent.InstanceDestroyed, model, SendQueue.Queued);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
declare type SyncDestroyCallback = (guid: string, object: Object3D) => void;
|
|
158
|
+
const _onSyncDestroyCallbacks: SyncDestroyCallback[] = [];
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Register a callback that fires when a remote `syncDestroy` event is received.
|
|
162
|
+
* The callback receives the guid and the resolved Object3D (or null if not found in the scene).
|
|
163
|
+
* The callback fires **before** the object is destroyed, so you can still access its state.
|
|
164
|
+
* @param callback Called with the guid and the Object3D about to be destroyed
|
|
165
|
+
* @returns An unsubscribe function
|
|
166
|
+
* @category Networking
|
|
167
|
+
* @example
|
|
168
|
+
* ```ts
|
|
169
|
+
* const unsub = onSyncDestroy((guid, obj) => {
|
|
170
|
+
* console.log("Remote object destroyed:", guid, obj?.name);
|
|
171
|
+
* });
|
|
172
|
+
* // later: unsub();
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
export function onSyncDestroy(callback: SyncDestroyCallback): () => void {
|
|
176
|
+
_onSyncDestroyCallbacks.push(callback);
|
|
177
|
+
return () => {
|
|
178
|
+
const idx = _onSyncDestroyCallbacks.indexOf(callback);
|
|
179
|
+
if (idx >= 0) _onSyncDestroyCallbacks.splice(idx, 1);
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
157
183
|
export function beginListenDestroy(context: Context) {
|
|
158
184
|
context.connection.beginListen(InstantiateEvent.InstanceDestroyed, (data: DestroyInstanceModel) => {
|
|
159
185
|
if (debug)
|
|
160
186
|
console.log("[Remote] Destroyed", context.scene, data);
|
|
161
187
|
// TODO: create global lookup table for guids
|
|
162
188
|
const obj = findByGuid(data.guid, context.scene);
|
|
163
|
-
if (obj)
|
|
189
|
+
if (obj) {
|
|
190
|
+
// Notify listeners before destroying
|
|
191
|
+
for (const cb of _onSyncDestroyCallbacks) {
|
|
192
|
+
try { cb(data.guid, obj as Object3D); }
|
|
193
|
+
catch (err) { console.error("Error in onSyncDestroy callback", err); }
|
|
194
|
+
}
|
|
195
|
+
destroy(obj);
|
|
196
|
+
}
|
|
164
197
|
});
|
|
165
198
|
}
|
|
166
199
|
|
|
@@ -215,27 +248,125 @@ export class NewInstanceModel implements IModel {
|
|
|
215
248
|
|
|
216
249
|
/**
|
|
217
250
|
* Instantiation options for {@link syncInstantiate}
|
|
251
|
+
* @category Networking
|
|
252
|
+
* @see {@link syncInstantiate} - Instantiate objects across the network
|
|
218
253
|
*/
|
|
219
254
|
export type SyncInstantiateOptions = IInstantiateOptions & Pick<IModel, "deleteOnDisconnect">;
|
|
220
255
|
|
|
221
256
|
// #region Sync Instantiate
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Callback type for {@link onSyncInstantiate}
|
|
260
|
+
* @param instance The instantiated object
|
|
261
|
+
* @param model The network model data sent with the instantiate event
|
|
262
|
+
* @param context The network context in which the instantiate event was received
|
|
263
|
+
* @category Networking
|
|
264
|
+
*/
|
|
265
|
+
declare type SyncInstantiateCallback = (instance: GameObject, model: NewInstanceModel, context: IContext) => void;
|
|
266
|
+
/** Registered callbacks for remote instantiation events */
|
|
267
|
+
const _onSyncInstantiateCallbacks: SyncInstantiateCallback[] = [];
|
|
268
|
+
|
|
222
269
|
/**
|
|
223
|
-
*
|
|
270
|
+
* Register a callback that fires when a remote `syncInstantiate` object is created on this client.
|
|
271
|
+
* Use this to get references to objects spawned by other users.
|
|
272
|
+
* @param callback Called with the instantiated Object3D, the network model data, and the Needle Engine context in which the instantiate event was received
|
|
273
|
+
* @returns An unsubscribe function
|
|
224
274
|
* @category Networking
|
|
275
|
+
* @example
|
|
276
|
+
* ```ts
|
|
277
|
+
* const unsub = onSyncInstantiate((instance, model, context) => {
|
|
278
|
+
* console.log("Remote object created:", instance.name, model.originalGuid, context);
|
|
279
|
+
* });
|
|
280
|
+
* // later: unsub();
|
|
281
|
+
* ```
|
|
282
|
+
* @see {@link syncInstantiate} - Instantiate objects across the network
|
|
283
|
+
* @see {@link syncDestroy} - Destroy objects across the network
|
|
284
|
+
*/
|
|
285
|
+
export function onSyncInstantiate(callback: SyncInstantiateCallback): () => void {
|
|
286
|
+
_onSyncInstantiateCallbacks.push(callback);
|
|
287
|
+
return () => {
|
|
288
|
+
const idx = _onSyncInstantiateCallbacks.indexOf(callback);
|
|
289
|
+
if (idx >= 0) _onSyncInstantiateCallbacks.splice(idx, 1);
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Instantiate an object across the network. The object is cloned locally and a network message
|
|
295
|
+
* is sent so all connected clients create the same clone. Late joiners receive the message
|
|
296
|
+
* via room state replay (unless `deleteOnDisconnect` is set or `save` is false).
|
|
297
|
+
*
|
|
298
|
+
* ## How it works internally
|
|
299
|
+
* 1. The prefab is cloned locally using a seeded {@link InstantiateIdProvider}
|
|
300
|
+
* 2. The seed ensures all clients generate **identical deterministic guids** for the clone
|
|
301
|
+
* and all its children — no need to send individual guids over the network
|
|
302
|
+
* 3. A {@link NewInstanceModel} message is sent containing the prefab's `originalGuid`,
|
|
303
|
+
* the clone's `guid`, the `seed`, and transform data
|
|
304
|
+
* 4. On receiving clients, the prefab is resolved via {@link registerPrefabProvider} or
|
|
305
|
+
* by searching the scene for an object with matching guid, then cloned with the same seed
|
|
306
|
+
*
|
|
307
|
+
* ## Runtime-created prefabs (no GLB)
|
|
308
|
+
* If the object has a `guid` but no prefab provider is registered for it, `syncInstantiate`
|
|
309
|
+
* will **auto-register** the object as a prefab provider. This means for code-only prefabs
|
|
310
|
+
* you just need to set a `guid` — no manual `registerPrefabProvider` call needed, as long as
|
|
311
|
+
* all clients run the same setup code that creates the same prefab with the same guid.
|
|
312
|
+
*
|
|
313
|
+
* @param object The object to instantiate. Must have a `guid` property (set one for runtime objects).
|
|
314
|
+
* @param opts Options for the instantiation, including the network context to send the instantiate event to
|
|
315
|
+
* @param hostData Optional data about a file to download when this object is instantiated (e.g. when instantiated via file drop)
|
|
316
|
+
* @param save When false, the state of this instance will not be saved in the networking backend. Default is true.
|
|
317
|
+
* @returns The instantiated object, or null if instantiation failed (e.g. missing guid or network context)
|
|
318
|
+
* @see {@link syncDestroy} - Destroy objects across the network
|
|
319
|
+
* @see {@link onSyncInstantiate} - Register a callback to get references to remotely instantiated objects
|
|
320
|
+
* @see {@link registerPrefabProvider} - Manually register a prefab provider (auto-registered by syncInstantiate)
|
|
321
|
+
* @see {@link unregisterPrefabProvider} - Remove a registered prefab provider
|
|
322
|
+
* @category Networking
|
|
323
|
+
*
|
|
324
|
+
* @example Basic usage with a runtime-created prefab
|
|
325
|
+
* ```ts
|
|
326
|
+
* const cookie = ObjectUtils.createPrimitive("Cube", { color: 0xff8c00 });
|
|
327
|
+
* cookie.guid = "cookie-prefab";
|
|
328
|
+
* // No need to call registerPrefabProvider — syncInstantiate auto-registers it
|
|
329
|
+
* syncInstantiate(cookie, { parent: ctx.scene, deleteOnDisconnect: false });
|
|
330
|
+
* ```
|
|
331
|
+
*
|
|
332
|
+
* @example With deterministic seed (advanced)
|
|
333
|
+
* ```ts
|
|
334
|
+
* const idProvider = new InstantiateIdProvider("my-seed");
|
|
335
|
+
* const instance = syncInstantiate(prefab, { context, idProvider });
|
|
336
|
+
* ```
|
|
337
|
+
* The seed generates deterministic guids via UUID v5, so all clients produce identical
|
|
338
|
+
* identifiers for the clone and its children without sending them over the network.
|
|
225
339
|
*/
|
|
226
340
|
export function syncInstantiate(object: GameObject | Object3D, opts: SyncInstantiateOptions, hostData?: HostData, save?: boolean): GameObject | null {
|
|
227
341
|
|
|
228
342
|
const obj: GameObject = object as GameObject;
|
|
229
343
|
|
|
230
344
|
if (!obj.guid) {
|
|
231
|
-
|
|
232
|
-
|
|
345
|
+
// Auto-assign guid from object name if available.
|
|
346
|
+
// The name must be the same on all clients (which it is when both run the same setup code).
|
|
347
|
+
if (obj.name) {
|
|
348
|
+
obj.guid = obj.name;
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
console.error("[syncInstantiate] Can not instantiate: a 'guid' is required. For runtime-created objects, either set a name (`obj.name = 'my-prefab'`) or a guid (`obj.guid = 'my-prefab-id'`). The identifier must be the same on all clients.", obj);
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
233
354
|
}
|
|
234
355
|
|
|
235
|
-
if
|
|
356
|
+
// Auto-register the prefab provider if none exists for this guid.
|
|
357
|
+
// This allows runtime-created objects to work with syncInstantiate without
|
|
358
|
+
// manual Prefabs.register calls, as long as all clients create the
|
|
359
|
+
// same prefab with the same guid in their setup code.
|
|
360
|
+
if (!Prefabs.has(obj.guid)) {
|
|
361
|
+
Prefabs.register(obj.guid, async () => obj);
|
|
362
|
+
}
|
|
236
363
|
|
|
237
364
|
if (!opts.context) {
|
|
238
|
-
|
|
365
|
+
opts.context = Context.Current;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!opts.context) {
|
|
369
|
+
console.error("[syncInstantiate] Missing network instantiate options / reference to network connection in sync instantiate. Please pass in the Needle Engine context.");
|
|
239
370
|
return null;
|
|
240
371
|
}
|
|
241
372
|
|
|
@@ -318,7 +449,6 @@ const syncedInstantiated = new Array<WeakRef<Object3D>>();
|
|
|
318
449
|
|
|
319
450
|
export function beginListenInstantiate(context: Context) {
|
|
320
451
|
|
|
321
|
-
|
|
322
452
|
const cb1 = context.connection.beginListen(InstantiateEvent.NewInstanceCreated, async (model: NewInstanceModel) => {
|
|
323
453
|
const obj: GameObject | null = await tryResolvePrefab(model.originalGuid, context.scene) as GameObject;
|
|
324
454
|
if (model.preventCreation === true) {
|
|
@@ -350,6 +480,11 @@ export function beginListenInstantiate(context: Context) {
|
|
|
350
480
|
context.scene.add(inst);
|
|
351
481
|
if (debug)
|
|
352
482
|
console.log("[Remote] new instance", "gameobject:", inst?.guid, obj);
|
|
483
|
+
// Notify listeners about the remote instantiation
|
|
484
|
+
for (const cb of _onSyncInstantiateCallbacks) {
|
|
485
|
+
try { cb(inst, model, context); }
|
|
486
|
+
catch (err) { console.error("Error in onSyncInstantiate callback", err); }
|
|
487
|
+
}
|
|
353
488
|
}
|
|
354
489
|
});
|
|
355
490
|
const cb2 = context.connection.beginListen("left-room", () => {
|
|
@@ -377,21 +512,28 @@ function instantiateSeeded(obj: GameObject, opts: IInstantiateOptions | null): {
|
|
|
377
512
|
return { seed: seed, instance: instance };
|
|
378
513
|
}
|
|
379
514
|
|
|
380
|
-
export
|
|
515
|
+
export { type PrefabProviderCallback,Prefabs } from "./engine_networking_prefabs.js";
|
|
516
|
+
import { Prefabs } from "./engine_networking_prefabs.js";
|
|
381
517
|
|
|
382
|
-
|
|
518
|
+
/**
|
|
519
|
+
* Register a prefab provider. Forwards to {@link Prefabs.register}.
|
|
520
|
+
* @category Networking
|
|
521
|
+
*/
|
|
522
|
+
export function registerPrefabProvider(key: string, fn: (guid: string) => Promise<Object3D | null>) {
|
|
523
|
+
Prefabs.register(key, fn);
|
|
524
|
+
}
|
|
383
525
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
526
|
+
/**
|
|
527
|
+
* Unregister a prefab provider. Forwards to {@link Prefabs.unregister}.
|
|
528
|
+
* @category Networking
|
|
529
|
+
*/
|
|
530
|
+
export function unregisterPrefabProvider(key: string) {
|
|
531
|
+
Prefabs.unregister(key);
|
|
387
532
|
}
|
|
388
533
|
|
|
389
534
|
async function tryResolvePrefab(guid: string, obj: Object3D): Promise<Object3D | null> {
|
|
390
|
-
const
|
|
391
|
-
if (
|
|
392
|
-
const res = await prov(guid);
|
|
393
|
-
if (res) return res;
|
|
394
|
-
}
|
|
535
|
+
const res = await Prefabs.resolve(guid);
|
|
536
|
+
if (res) return res;
|
|
395
537
|
return tryFindObjectByGuid(guid, obj) as Object3D;
|
|
396
538
|
}
|
|
397
539
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Object3D } from "three";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Callback type for prefab providers.
|
|
5
|
+
* @param guid The guid of the prefab to resolve
|
|
6
|
+
* @returns The prefab Object3D, or null if not found
|
|
7
|
+
*/
|
|
8
|
+
export declare type PrefabProviderCallback = (guid: string) => Promise<Object3D | null>;
|
|
9
|
+
|
|
10
|
+
const registeredProviders: { [key: string]: PrefabProviderCallback } = {};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Prefab registry for networked instantiation.
|
|
14
|
+
*
|
|
15
|
+
* When a remote {@link syncInstantiate} event is received, the engine looks up the prefab
|
|
16
|
+
* by its guid using this registry. Both GLB-loaded objects and runtime-created objects
|
|
17
|
+
* can be registered here.
|
|
18
|
+
*
|
|
19
|
+
* Note: {@link syncInstantiate} auto-registers prefabs if no provider exists for their guid,
|
|
20
|
+
* so manual registration is only needed for custom resolution logic.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { Prefabs, ObjectUtils } from "@needle-tools/engine";
|
|
25
|
+
*
|
|
26
|
+
* const cookie = ObjectUtils.createPrimitive("Cube", { color: 0xff8c00 });
|
|
27
|
+
* cookie.guid = "cookie-prefab";
|
|
28
|
+
* Prefabs.register("cookie-prefab", async () => cookie);
|
|
29
|
+
*
|
|
30
|
+
* console.log(Prefabs.list()); // ["cookie-prefab"]
|
|
31
|
+
* Prefabs.unregister("cookie-prefab");
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @category Networking
|
|
35
|
+
*/
|
|
36
|
+
export const Prefabs = {
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Register a prefab provider that resolves objects by guid for networked instantiation.
|
|
40
|
+
* When a remote `syncInstantiate` event is received, the engine uses this to find the prefab
|
|
41
|
+
* to clone on the receiving client.
|
|
42
|
+
*
|
|
43
|
+
* @param key The guid to register the provider for
|
|
44
|
+
* @param fn Callback that returns the prefab Object3D for the given guid
|
|
45
|
+
*/
|
|
46
|
+
register(key: string, fn: PrefabProviderCallback) {
|
|
47
|
+
registeredProviders[key] = fn;
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Unregister a previously registered prefab provider.
|
|
52
|
+
* @param key The guid to unregister
|
|
53
|
+
*/
|
|
54
|
+
unregister(key: string) {
|
|
55
|
+
delete registeredProviders[key];
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns a list of all registered prefab provider keys (guids).
|
|
60
|
+
* Useful for debugging to see which prefabs are available for networked instantiation.
|
|
61
|
+
*/
|
|
62
|
+
list(): string[] {
|
|
63
|
+
return Object.keys(registeredProviders);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if a prefab provider is registered for the given guid.
|
|
68
|
+
* @param key The guid to check
|
|
69
|
+
*/
|
|
70
|
+
has(key: string): boolean {
|
|
71
|
+
return key in registeredProviders;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
/** @internal Resolve a prefab by guid. Used by the networking system. */
|
|
75
|
+
async resolve(guid: string): Promise<Object3D | null> {
|
|
76
|
+
const prov = registeredProviders[guid];
|
|
77
|
+
if (prov) return prov(guid);
|
|
78
|
+
return null;
|
|
79
|
+
},
|
|
80
|
+
} as const;
|
|
@@ -46,16 +46,21 @@ const $colliderRigidbody = Symbol("rigidbody");
|
|
|
46
46
|
|
|
47
47
|
declare const NEEDLE_USE_RAPIER: boolean;
|
|
48
48
|
globalThis["NEEDLE_USE_RAPIER"] = globalThis["NEEDLE_USE_RAPIER"] !== undefined ? globalThis["NEEDLE_USE_RAPIER"] : true;
|
|
49
|
-
if (debugPhysics)
|
|
50
|
-
console.log("Use Rapier", NEEDLE_USE_RAPIER, globalThis["NEEDLE_USE_RAPIER"])
|
|
51
49
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
/** Register the Rapier physics backend. Called from {@link initEngine}
|
|
51
|
+
* to ensure it runs regardless of tree-shaking. */
|
|
52
|
+
export function initPhysics() {
|
|
53
|
+
if (debugPhysics)
|
|
54
|
+
console.log("Use Rapier", NEEDLE_USE_RAPIER, globalThis["NEEDLE_USE_RAPIER"])
|
|
55
|
+
|
|
56
|
+
if (NEEDLE_USE_RAPIER) {
|
|
57
|
+
ContextRegistry.registerCallback(ContextEvent.ContextCreationStart, evt => {
|
|
58
|
+
if (debugPhysics)
|
|
59
|
+
console.log("Register rapier physics backend")
|
|
60
|
+
evt.context.physics.engine = new RapierPhysics(evt.context);
|
|
61
|
+
// We do not initialize physics immediately to avoid loading the physics engine if it is not needed
|
|
62
|
+
});
|
|
63
|
+
}
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
|
|
@@ -184,12 +184,12 @@ export function setOrAddParamsToUrl(url: URLSearchParams, paramName: string, par
|
|
|
184
184
|
|
|
185
185
|
/** Adds an entry to the browser history. Internally uses `window.history.pushState` */
|
|
186
186
|
export function pushState(title: string, urlParams: URLSearchParams, state?: any) {
|
|
187
|
-
window.history.pushState(state, title, "?" + urlParams.toString());
|
|
187
|
+
window.history.pushState(state, title, "?" + urlParams.toString() + window.location.hash);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
/** Replaces the current entry in the browser history. Internally uses `window.history.replaceState` */
|
|
191
191
|
export function setState(title: string, urlParams: URLSearchParams, state?: any) {
|
|
192
|
-
window.history.replaceState(state, title, "?" + urlParams.toString());
|
|
192
|
+
window.history.replaceState(state, title, "?" + urlParams.toString() + window.location.hash);
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
// for room id
|