@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.
194 lines (162 loc) • 6.54 kB
text/typescript
import { type Context, FrameEvent } from "./engine_context.js";
import { ContextEvent } from "./engine_context_registry.js";
export declare type Event = ContextEvent | FrameEvent;
declare type LifecycleHookContext = {
context: Context;
}
/**
* A function that can be called during the Needle Engine frame event at a specific point
* @link https://engine.needle.tools/docs/scripting.html#special-lifecycle-hooks
*/
export declare type LifecycleMethod = (this: LifecycleHookContext, ctx: Context) => void;
/**
* Options for `onStart(()=>{})` etc event hooks
* @link https://engine.needle.tools/docs/scripting.html#special-lifecycle-hooks
*/
export declare type LifecycleMethodOptions = {
/**
* If true, the callback will only be called once
*/
once?: boolean
};
declare type RegisteredLifecycleMethod = { method: LifecycleMethod, options: LifecycleMethodOptions };
const newMethods = new Map<Event, Array<RegisteredLifecycleMethod>>();
const allMethods = new Map<Event, Array<RegisteredLifecycleMethod>>();
let methodsWarningCounter = 0;
/** register a function to be called during the Needle Engine frame event at a specific point
* @param cb the function to call
* @param evt the event to call the function at
*/
export function registerFrameEventCallback(cb: LifecycleMethod, evt: Event, opts?: LifecycleMethodOptions) {
if (!newMethods.has(evt)) {
newMethods.set(evt, new Array());
}
newMethods.get(evt)!.push({
method: cb,
options: { once: false, ...opts }
});
// Warn if there are too many methods registered
if (methodsWarningCounter < 30) {
const existing = allMethods.get(evt);
if (existing && existing?.length > 100) {
methodsWarningCounter += 1;
console.warn(`You have ${existing.length} methods registered for Event ${evt}.
This might be a performance issue!
Consider unregistering the methods when they are not needed anymore!
To unregister you can call the function returned by your event hook (e.g.const unregister = onStart(...))
or by using the once option like onStart(()=>{}, { once:true }).
See https://engine.needle.tools/docs/scripting.html#special-lifecycle-hooks for more information.`);
}
}
}
/**
* unregister a function to be called during the Needle Engine frame event at a specific point
*/
export function unregisterFrameEventCallback(cb: LifecycleMethod, evt: Event) {
const methods = allMethods.get(evt);
if (methods) {
for (let i = 0; i < methods.length; i++) {
if (methods[i].method === cb) {
methods.splice(i, 1);
return;
}
}
}
const newMethodsArray = newMethods.get(evt);
if (newMethodsArray) {
for (let i = 0; i < newMethodsArray.length; i++) {
if (newMethodsArray[i].method === cb) {
newMethodsArray.splice(i, 1);
return;
}
}
}
}
export function invokeLifecycleFunctions(ctx: Context, evt: Event) {
// When a context is created, we need to reset the started state
// Because we want to e.g. invoke `onStart` again (even if it's the same context)
if (evt === ContextEvent.ContextCreated) {
_ignore.delete(ctx);
}
internalInvokeLifecycleFunctions(ctx, evt);
}
function internalInvokeLifecycleFunctions(ctx: Context, evt: Event) {
// handle the initialized event like start.
// This happens e.g. if onInitialized is registered AFTER the context has been created
if (evt === FrameEvent.Start) {
const initializeMethods = newMethods.get(ContextEvent.ContextCreated);
if (initializeMethods) {
internalInvokeLifecycleFunctions(ctx, ContextEvent.ContextCreated);
}
}
const shouldBeInvokedOnce = evt === FrameEvent.Start || evt === ContextEvent.ContextCreated;
const methods = allMethods.get(evt);
if (methods) {
if (methods.length > 0) {
const array = methods;
invoke(ctx, array, shouldBeInvokedOnce);
}
}
const newMethodsArray = newMethods.get(evt);
if (newMethodsArray) {
if (newMethodsArray.length > 0) {
// We copy the array here once because the array might be modified during the invoke
// E.g. if onStart(() => onStart(() => {})) is called
const array = [...newMethodsArray];
newMethodsArray.length = 0;
invoke(ctx, array, shouldBeInvokedOnce);
// if any of the new methods is still in the allMethods array, remove it
if (array.length > 0) {
if (!allMethods.has(evt)) {
allMethods.set(evt, new Array());
}
const methodsArray = allMethods.get(evt)!;
methodsArray.push(...array);
}
}
}
}
const bufferArray = new Array<RegisteredLifecycleMethod>();
const hookContext: LifecycleHookContext = {
context: null as any as Context
};
function invoke(ctx: Context, methods: Array<RegisteredLifecycleMethod>, invokeOnce: boolean) {
bufferArray.length = 0;
for (let i = 0; i < methods.length; i++) {
bufferArray.push(methods[i]);
}
let ignoreSet = _ignore.get(ctx);
for (let i = 0; i < bufferArray.length; i++) {
const entry = bufferArray[i];
let invoke = true;
if (ignoreSet && ignoreSet.has(entry)) {
invoke = false;
}
if (invoke) {
try {
hookContext.context = ctx;
entry.method?.call(hookContext, ctx);
}
catch (e) {
console.error("Error in lifecycle method", e);
}
}
// Remove the method if it's a one time call
if (entry.options?.once) {
for (let j = 0; j < methods.length; j++) {
if (methods[j] === entry) {
methods.splice(j, 1);
break;
}
}
}
else if (invokeOnce) {
if (!ignoreSet) {
ignoreSet = new Set();
_ignore.set(ctx, ignoreSet);
}
ignoreSet!.add(entry);
}
}
}
const _ignore = new WeakMap<Context, Set<RegisteredLifecycleMethod>>();