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.

467 lines 18.5 kB
import { CubeCamera, Scene, WebGLCubeRenderTarget } from 'three'; import { isDevEnvironment } from "./debug/index.js"; import * as constants from "./engine_constants.js"; import { ContextRegistry } from "./engine_context_registry.js"; import { isActiveSelf } from './engine_gameobject.js'; import { safeInvoke } from "./engine_generic_utils.js"; import { getParam } from './engine_utils.js'; const debug = getParam("debugnewscripts"); const debugHierarchy = getParam("debughierarchy"); // if some other script adds new scripts in onEnable or awake // the original array should be cleared before processing it // so we use this copy buffer const new_scripts_buffer = []; /** @internal */ export function hasNewScripts() { return new_scripts_buffer.length > 0; } /** * This method is called by the engine to process new scripts that were added to the scene. * It will call the registering method on the script, then the awake method and finally the onEnable method. * @internal */ export function processNewScripts(context) { if (debug) console.log("Register new components", context.new_scripts.length, [...context.new_scripts], context.alias ? ("element: " + context.alias) : context["hash"], context); if (context.new_scripts_pre_setup_callbacks.length > 0) { for (const cb of context.new_scripts_pre_setup_callbacks) { if (!cb) continue; cb(); } context.new_scripts_pre_setup_callbacks.length = 0; } if (context.new_scripts.length <= 0) return; // TODO: update all the code from above to use this logic // basically code gen should add the scripts to new scripts // and this code below should go into some util method new_scripts_buffer.length = 0; if (context.new_scripts.length > 0) { new_scripts_buffer.push(...context.new_scripts); } context.new_scripts.length = 0; // Check valid scripts and add all valid to the scripts array for (let i = 0; i < new_scripts_buffer.length; i++) { try { const script = new_scripts_buffer[i]; if (script.isComponent !== true) { if (isDevEnvironment() || debug) console.error("Registered script is not a Needle Engine component. \nThe script will be ignored. Please make sure your component extends \"Behaviour\" imported from \"@needle-tools/engine\"\n", script); new_scripts_buffer.splice(i, 1); i--; continue; } if (script.destroyed) continue; if (!script.gameObject) { console.warn("Component can not be initialized: no GameObject assigned.\nDid you add and remove a component in the same frame?"); new_scripts_buffer.splice(i, 1); i--; continue; } script.context = context; updateActiveInHierarchyWithoutEventCall(script.gameObject); addScriptToArrays(script, context); } catch (err) { console.error(err); removeScriptFromContext(new_scripts_buffer[i], context); new_scripts_buffer.splice(i, 1); i--; } } // Awake for (let i = 0; i < new_scripts_buffer.length; i++) { try { const script = new_scripts_buffer[i]; if (script.destroyed) { removeScriptFromContext(new_scripts_buffer[i], context); new_scripts_buffer.splice(i, 1); i--; continue; } if (script.registering) { try { script.registering(); } catch (err) { console.error(err); } } // console.log(script, script.gameObject) // TODO: we should not call awake on components with inactive gameobjects if (script.__internalAwake !== undefined) { if (!script.gameObject) { console.error("Calling awake for a component without a GameObject", script, script.gameObject); } updateActiveInHierarchyWithoutEventCall(script.gameObject); if (script.activeAndEnabled) safeInvoke(script.__internalAwake.bind(script)); // registerPrewarmObject(script.gameObject, context); } } catch (err) { console.error(err); removeScriptFromContext(new_scripts_buffer[i], context); new_scripts_buffer.splice(i, 1); i--; } } // OnEnable for (let i = 0; i < new_scripts_buffer.length; i++) { try { const script = new_scripts_buffer[i]; if (script.destroyed) continue; // console.log(script, script.enabled, script.activeAndEnabled); if (script.enabled === false) continue; updateActiveInHierarchyWithoutEventCall(script.gameObject); if (script.activeAndEnabled === false) continue; if (script.__internalEnable !== undefined) { script.enabled = true; safeInvoke(script.__internalEnable.bind(script)); } } catch (err) { console.error(err); removeScriptFromContext(new_scripts_buffer[i], context); new_scripts_buffer.splice(i, 1); i--; } } // Enqueue Start for (let i = 0; i < new_scripts_buffer.length; i++) { try { const script = new_scripts_buffer[i]; if (script.destroyed) continue; if (!script.gameObject) continue; context.new_script_start.push(script); } catch (err) { console.error(err); removeScriptFromContext(new_scripts_buffer[i], context); new_scripts_buffer.splice(i, 1); i--; } } // for (const script of new_scripts_buffer) { // if (script.destroyed) continue; // context.scripts.push(script); // } new_scripts_buffer.length = 0; // if(new_scripts_post_setup_callbacks.length > 0) console.log(new_scripts_post_setup_callbacks); for (const cb of context.new_scripts_post_setup_callbacks) { if (cb) cb(); } context.new_scripts_post_setup_callbacks.length = 0; } /** @internal */ export function processRemoveFromScene(script) { if (!script) return; script.__internalDisable(true); removeScriptFromContext(script, script.context); } /** @internal */ export function processStart(context, object) { // Call start on scripts for (let i = 0; i < context.new_script_start.length; i++) { try { const script = context.new_script_start[i]; if (object !== undefined && script.gameObject !== object) continue; if (script.destroyed) continue; if (script.activeAndEnabled === false) { continue; } // keep them in queue until script has started // call awake if the script was inactive before safeInvoke(script.__internalAwake.bind(script)); if (script.enabled) { safeInvoke(script.__internalEnable.bind(script)); // now call start safeInvoke(script.__internalStart.bind(script)); context.new_script_start.splice(i, 1); i--; } } catch (err) { console.error(err); removeScriptFromContext(context.new_script_start[i], context); context.new_script_start.splice(i, 1); i--; } } } /** @internal */ export function addScriptToArrays(script, context) { // TODO: not sure if this is ideal - maybe we should add a map if we have many scripts? const index = context.scripts.indexOf(script); if (index !== -1) return; context.scripts.push(script); if (script.earlyUpdate) context.scripts_earlyUpdate.push(script); if (script.update) context.scripts_update.push(script); if (script.lateUpdate) context.scripts_lateUpdate.push(script); if (script.onBeforeRender) context.scripts_onBeforeRender.push(script); if (script.onAfterRender) context.scripts_onAfterRender.push(script); if (script.onPausedChanged) context.scripts_pausedChanged.push(script); if (isNeedleXRSessionEventReceiver(script, null)) context.new_scripts_xr.push(script); // do we want to check if a XR session is active before adding scripts here? if (isNeedleXRSessionEventReceiver(script, "immersive-vr")) context.scripts_immersive_vr.push(script); if (isNeedleXRSessionEventReceiver(script, "immersive-ar")) context.scripts_immersive_ar.push(script); } /** @internal */ export function removeScriptFromContext(script, context) { removeFromArray(script, context.new_scripts); removeFromArray(script, context.new_script_start); removeFromArray(script, context.scripts); removeFromArray(script, context.scripts_earlyUpdate); removeFromArray(script, context.scripts_update); removeFromArray(script, context.scripts_lateUpdate); removeFromArray(script, context.scripts_onBeforeRender); removeFromArray(script, context.scripts_onAfterRender); removeFromArray(script, context.scripts_pausedChanged); removeFromArray(script, context.new_scripts_xr); removeFromArray(script, context.scripts_immersive_vr); removeFromArray(script, context.scripts_immersive_ar); context.stopAllCoroutinesFrom(script); } function removeFromArray(script, array) { const index = array.indexOf(script); if (index >= 0) array.splice(index, 1); } /** @internal */ export function isNeedleXRSessionEventReceiver(script, mode) { if (script) { const i = script; if (i.onBeforeXR || i.onEnterXR || i.onUpdateXR || i.onLeaveXR || i.onXRControllerAdded || i.onXRControllerRemoved) { if (mode != null) { if (i.supportsXR?.(mode) === false) return false; } return true; } } return false; } /** @internal */ export function updateIsActive(obj) { if (!obj) obj = ContextRegistry.Current.scene; if (!obj) { console.trace("Invalid call - no current context."); return; } const activeSelf = isActiveSelf(obj); const wasSuccessful = updateIsActiveInHierarchyRecursiveRuntime(obj, activeSelf, true); if (!wasSuccessful) { if (debug || isDevEnvironment()) { console.error("Error updating hierarchy\nDo you have circular references in your project? <a target=\"_blank\" href=\"https://docs.needle.tools/circular-reference\"> Click here for more information.", obj); } else console.error("Failed to update active state in hierarchy of \"" + obj.name + "\"", obj); console.warn(" ↑ this error might be caused by circular references. Please make sure you don't have files with circular references (e.g. one GLB 1 is loading GLB 2 which is then loading GLB 1 again)."); } } function updateIsActiveInHierarchyRecursiveRuntime(go, activeInHierarchy, allowEventCall, level = 0) { if (level > 1000) { console.warn("Hierarchy is too deep (> 1000 level) - will abort updating active state"); return false; } const isActive = isActiveSelf(go); if (activeInHierarchy) { activeInHierarchy = isActive; // IF we update activeInHierarchy within a disabled hierarchy we need to check the parent if (activeInHierarchy && go.parent) { const parent = go.parent; activeInHierarchy = parent[constants.activeInHierarchyFieldName]; if (activeInHierarchy === undefined) { // TODO: come up with a better solution for this. When we are in a r3f hierarchy (externally managed) and the parent flag is undefined we set it to true if the parent is NOT a scene. This activates the object by default. We should probably walk up the stack and check if we can find either the root Scene or any object that is disabled and use that to set the activeInHierarchy flag. if (parent instanceof Scene) { } else { activeInHierarchy = true; } } } } const prevActive = go[constants.activeInHierarchyFieldName]; const changed = prevActive !== activeInHierarchy; go[constants.activeInHierarchyFieldName] = activeInHierarchy; // only raise events here if we didnt call enable etc already if (changed) { if (debugHierarchy) console.warn("ACTIVE CHANGE", go.name, isActive, go.visible, activeInHierarchy, "changed?" + changed, go); if (allowEventCall) { perComponent(go, comp => { if (activeInHierarchy) { if (comp.enabled) { safeInvoke(comp.__internalAwake.bind(comp)); if (comp.enabled) { comp.__internalEnable(); } } } else { if (comp["__didAwake"] && comp.enabled) { comp["__didEnable"] = false; comp.onDisable(); } } }); } } let success = true; if (go.children) { for (const ch of go.children) { const res = updateIsActiveInHierarchyRecursiveRuntime(ch, activeInHierarchy, allowEventCall, level + 1); if (res === false) success = false; } } return success; } // function tryFindActiveStateInParent(obj: Object3D): boolean { // let current: Object3D | undefined | null = obj; // while (current) { // const activeState = current[constants.activeInHierarchyFieldName]; // if (activeState !== undefined) return activeState; // if (current instanceof Scene && !current.parent) { // return true; // } // current = current.parent; // } // return false; // } // let isRunning = false; // // Prevent: https://github.com/needle-tools/needle-tiny/issues/641 // const temporyChildArrayBuffer: Array<Array<Object3D>> = []; // export function* iterateChildrenSafe(obj: Object3D) { // if (!obj || !obj.children) yield null; // // if(isRunning) return; // // isRunning = true; // const arr = temporyChildArrayBuffer.pop() || []; // arr.push(...obj.children); // for (const ch of arr) { // yield ch; // } // // isRunning = false; // arr.length = 0; // temporyChildArrayBuffer.push(arr); // } /** @internal */ export function updateActiveInHierarchyWithoutEventCall(go) { let activeInHierarchy = true; let current = go; let foundScene = false; while (current) { if (!current) break; if (current.type === "Scene") foundScene = true; if (!isActiveSelf(current)) { activeInHierarchy = false; break; } current = current.parent; } if (!go) { console.error("GO is null"); return; } go[constants.activeInHierarchyFieldName] = activeInHierarchy && foundScene; } function perComponent(go, evt) { if (go.userData?.components) { for (const comp of go.userData.components) { evt(comp); } } } const prewarmList = new Map(); const $prewarmedFlag = Symbol("prewarmFlag"); const $waitingForPrewarm = Symbol("waitingForPrewarm"); const debugPrewarm = getParam("debugprewarm"); /** @internal */ export function registerPrewarmObject(obj, context) { if (!obj) return; // allow objects to be marked as prewarmed in which case we dont need to register them again if (obj[$prewarmedFlag] === true) return; if (obj[$waitingForPrewarm] === true) return; if (!prewarmList.has(context)) { prewarmList.set(context, []); } obj[$waitingForPrewarm] = true; const list = prewarmList.get(context); list.push(obj); if (debugPrewarm) console.debug("register prewarm", obj.name); } let prewarmTarget = null; let prewarmCamera = null; /** @internal called by the engine to remove scroll or animation hiccup when objects are rendered/compiled for the first time */ export function runPrewarm(context) { if (!context) return; const list = prewarmList.get(context); if (!list?.length) return; const cam = context.mainCamera; if (cam) { if (debugPrewarm) console.log("prewarm", list.length, "objects", [...list]); const renderer = context.renderer; if (renderer.compile) { const scene = context.scene; renderer.compile(scene, cam); prewarmTarget ??= new WebGLCubeRenderTarget(64); prewarmCamera ??= new CubeCamera(0.001, 9999999, prewarmTarget); prewarmCamera.update(renderer, scene); for (const obj of list) { obj[$prewarmedFlag] = true; obj[$waitingForPrewarm] = false; } list.length = 0; if (debugPrewarm) console.log("prewarm done"); } } } /** @internal */ export function clearPrewarmList(context) { const list = prewarmList.get(context); if (list) { for (const obj of list) { obj[$waitingForPrewarm] = false; } list.length = 0; } prewarmList.delete(context); } //# sourceMappingURL=engine_mainloop_utils.js.map