UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.

346 lines • 12.9 kB
import { Quaternion, Vector3 } from "three"; // https://github.com/uuidjs/uuid // v5 takes string and namespace import { v5 } from 'uuid'; import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js"; import { isDevEnvironment } from "./debug/index.js"; import { destroy, findByGuid, instantiate } from "./engine_gameobject.js"; import { InstantiateOptions } from "./engine_gameobject.js"; import { SendQueue } from "./engine_networking_types.js"; import { Context } from "./engine_setup.js"; import * as utils from "./engine_utils.js"; ContextRegistry.registerCallback(ContextEvent.ContextCreated, evt => { const context = evt.context; beginListenInstantiate(context); beginListenDestroy(context); }); const debug = utils.getParam("debugcomponents"); const ID_NAMESPACE = 'eff8ba80-635d-11ec-90d6-0242ac120003'; export class InstantiateIdProvider { get seed() { return this._seed; } set seed(val) { this._seed = val; } _originalSeed; _seed; constructor(seed) { if (typeof seed === "string") { seed = InstantiateIdProvider.hash(seed); } this._originalSeed = seed; this._seed = seed; } reset() { this._seed = this._originalSeed; } generateUUID(str) { if (typeof str === "string") { return v5(str, ID_NAMESPACE); } const s = this._seed; this._seed -= 1; // console.log(s); return v5(s.toString(), ID_NAMESPACE); } initialize(strOrNumber) { if (typeof strOrNumber === "string") { this._seed = InstantiateIdProvider.hash(strOrNumber); } else { this._seed = strOrNumber; } } static createFromString(str) { return new InstantiateIdProvider(this.hash(str)); } static hash(str) { let hash = 0; for (let i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); } return hash; } } export var InstantiateEvent; (function (InstantiateEvent) { InstantiateEvent["NewInstanceCreated"] = "new-instance-created"; InstantiateEvent["InstanceDestroyed"] = "instance-destroyed"; })(InstantiateEvent || (InstantiateEvent = {})); class DestroyInstanceModel { guid; dontSave; constructor(guid) { this.guid = guid; } } /** * Destroy an object across the network. See also {@link syncInstantiate}. * @param obj The object or component to be destroyed * @param con The network connection to send the destroy event to * @param recursive If true, all children will be destroyed as well. Default is true * @param opts Options for the destroy operation * @category Networking */ export function syncDestroy(obj, con, recursive = true, opts) { if (!obj) return; const go = obj; destroy(obj, recursive); if (!con) { console.warn("Can not send destroy: No networking connection provided", obj.guid); return; } if (!con.isConnected) { if (isDevEnvironment()) console.debug("Can not send destroy: not connected", obj.guid); return; } let guid = obj.guid; if (!guid && go.uuid) { guid = go.uuid; } if (!guid) { console.warn("Can not send destroy: failed to find guid", obj); return; } sendDestroyed(guid, con, opts); } export function sendDestroyed(guid, con, opts) { const model = new DestroyInstanceModel(guid); if (opts?.saveInRoom === false) { model.dontSave = true; } con.send(InstantiateEvent.InstanceDestroyed, model, SendQueue.Queued); } export function beginListenDestroy(context) { context.connection.beginListen(InstantiateEvent.InstanceDestroyed, (data) => { if (debug) console.log("[Remote] Destroyed", context.scene, data); // TODO: create global lookup table for guids const obj = findByGuid(data.guid, context.scene); if (obj) destroy(obj); }); } // /** * When a file is instantiated via some server (e.g. via file drop) we also want to send the info where the file can be downloaded. * @internal */ export class HostData { /** File to download */ filename; /** Checksum to verify its the correct file */ hash; /** Expected size of the referenced file and its dependencies */ size; constructor(filename, hash, size) { this.filename = filename; this.hash = hash; this.size = size; } } export class NewInstanceModel { guid; originalGuid; seed; visible; hostData; dontSave; parent; position; rotation; scale; /** Set to true to prevent this model from being instantiated */ preventCreation = undefined; /** * When set this will delete the server state when the user disconnects */ deleteStateOnDisconnect; constructor(originalGuid, newGuid) { this.originalGuid = originalGuid; this.guid = newGuid; } } /** * Instantiate an object across the network. See also {@link syncDestroy}. * @category Networking */ export function syncInstantiate(object, opts, hostData, save) { const obj = object; if (!obj.guid) { console.warn("Can not instantiate: No guid", obj); return null; } if (!opts.context) opts.context = Context.Current; if (!opts.context) { console.error("Missing network instantiate options / reference to network connection in sync instantiate"); return null; } const originalOpts = opts ? { ...opts } : null; const { instance, seed } = instantiateSeeded(obj, opts); if (instance) { const go = instance; // if (go.guid) { // const listener = GameObject.addNewComponent(go, DestroyListener); // listener.target = go; // } if (go.guid) { if (debug) console.log("[Local] new instance", "gameobject:", instance?.guid); const model = new NewInstanceModel(obj.guid, go.guid); model.seed = seed; if (opts.deleteOnDisconnect === true) model.deleteStateOnDisconnect = true; if (originalOpts) { if (originalOpts.position) model.position = { x: originalOpts.position.x, y: originalOpts.position.y, z: originalOpts.position.z }; if (originalOpts.rotation) model.rotation = { x: originalOpts.rotation.x, y: originalOpts.rotation.y, z: originalOpts.rotation.z, w: originalOpts.rotation.w }; if (originalOpts.scale) model.scale = { x: originalOpts.scale.x, y: originalOpts.scale.y, z: originalOpts.scale.z }; } if (!model.position) model.position = { x: go.position.x, y: go.position.y, z: go.position.z }; if (!model.rotation) model.rotation = { x: go.quaternion.x, y: go.quaternion.y, z: go.quaternion.z, w: go.quaternion.w }; if (!model.scale) model.scale = { x: go.scale.x, y: go.scale.y, z: go.scale.z }; model.visible = obj.visible; if (originalOpts?.parent) { if (typeof originalOpts.parent === "string") model.parent = originalOpts.parent; else model.parent = originalOpts.parent["guid"]; } model.hostData = hostData; if (save === false) model.dontSave = true; const con = opts?.context?.connection; if (!con && isDevEnvironment()) console.debug("Object will be instantiated but it will not be synced: not connected", obj.guid); if (opts.context.connection.isInRoom) syncedInstantiated.push(new WeakRef(go)); opts?.context?.connection.send(InstantiateEvent.NewInstanceCreated, model); } else console.warn("Missing guid, can not send new instance event", go); } return instance; } export function generateSeed() { return Math.random() * 9_999_999; // Number.MAX_VALUE;; } const syncedInstantiated = new Array(); export function beginListenInstantiate(context) { context.connection.beginListen(InstantiateEvent.NewInstanceCreated, async (model) => { const obj = await tryResolvePrefab(model.originalGuid, context.scene); if (model.preventCreation === true) { return; } if (!obj) { console.warn("could not find object that was instantiated: " + model.guid); return; } const options = new InstantiateOptions(); if (model.position) options.position = new Vector3(model.position.x, model.position.y, model.position.z); if (model.rotation) options.rotation = new Quaternion(model.rotation.x, model.rotation.y, model.rotation.z, model.rotation.w); if (model.scale) options.scale = new Vector3(model.scale.x, model.scale.y, model.scale.z); options.parent = model.parent; if (model.seed) options.idProvider = new InstantiateIdProvider(model.seed); options.visible = model.visible; options.context = context; if (debug && context.alias) console.log("[Remote] instantiate in: " + context.alias); const inst = instantiate(obj, options); syncedInstantiated.push(new WeakRef(inst)); if (inst) { if (model.parent === "scene") context.scene.add(inst); if (debug) console.log("[Remote] new instance", "gameobject:", inst?.guid, obj); } }); context.connection.beginListen("left-room", () => { if (syncedInstantiated.length > 0) console.debug(`Left networking room, cleaning up ${syncedInstantiated.length} instantiated objects`); for (const prev of syncedInstantiated) { const obj = prev.deref(); if (obj) obj.destroy(); } syncedInstantiated.length = 0; }); } function instantiateSeeded(obj, opts) { const seed = generateSeed(); const options = opts ?? new InstantiateOptions(); options.idProvider = new InstantiateIdProvider(seed); const instance = instantiate(obj, options); return { seed: seed, instance: instance }; } const registeredPrefabProviders = {}; //** e.g. provide a function that can return a instantiated object when instantiation event is received */ export function registerPrefabProvider(key, fn) { registeredPrefabProviders[key] = fn; } async function tryResolvePrefab(guid, obj) { const prov = registeredPrefabProviders[guid]; if (prov !== null && prov !== undefined) { const res = await prov(guid); if (res) return res; } return tryFindObjectByGuid(guid, obj); } function tryFindObjectByGuid(guid, obj) { if (obj === null) return null; if (!guid) return null; if (obj["guid"] === guid) { return obj; } if (obj.children) { for (const ch of obj.children) { const found = tryFindObjectByGuid(guid, ch); if (found) { return found; } } } return null; } // class DestroyListener extends Behaviour { // target: GameObject | Component | null = null; // private destroyCallback: any; // awake(): void { // if (!this.target) { // console.log("Missing target to watch", this); // return; // } // this.destroyCallback = this.onObjectDestroyed.bind(this); // this.context.connection.beginListen(InstantiateEvent.InstanceDestroyed, this.destroyCallback); // } // onDestroy(): void { // this.context.connection.stopListening(InstantiateEvent.InstanceDestroyed, this.destroyCallback); // if (this.target && this.target.guid && this.gameObject.guid === this.target.guid) { // sendDestroyed(this.target.guid, this.context.connection); // } // } // private onObjectDestroyed(evt: DestroyInstanceModel) { // if (evt.guid === this.target?.guid) { // if (debug) // console.log("RECEIVED destroyed event", evt.guid); // GameObject.destroy(this.target); // } // } // } //# sourceMappingURL=engine_networking_instantiate.js.map