UNPKG

unity-webgl

Version:

Unity-WebGL provides an easy solution for embedding Unity WebGL builds in your web projects, with two-way communication between your webApp and Unity application with advanced API's.

377 lines (372 loc) 12.3 kB
/*! * unity-webgl v4.4.2 * Copyright (c) 2025 Mariner<mengqing723@gmail.com> * Released under the Apache-2.0 License. */ const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; const getType = (value) => Object.prototype.toString.call(value).slice(8, -1).toLowerCase(); const isObject = (value) => getType(value) === 'object'; function omit(obj, keys) { const result = Object.assign({}, obj); keys.forEach((key) => { delete result[key]; }); return result; } /** * query CanvasElement */ function queryCanvas(canvas) { if (canvas instanceof HTMLCanvasElement) { return canvas; } return document.querySelector(canvas); } const log = (() => { const prefix = '[unity-webgl]'; const _log = (...args) => console.log(prefix, ...args); _log.warn = (...args) => console.warn(prefix, ...args); _log.error = (...args) => console.error(prefix, ...args); return _log; })(); class UnityWebglEvent { constructor() { this._e = {}; if (isBrowser) { // Register Unity event trigger to the window object window.dispatchUnityEvent = (name, ...args) => { if (!name.startsWith('unity:')) { name = `unity:${name}`; } this.emit.call(this, name, ...args); }; } } on(name, listener, options) { if (typeof listener !== 'function') { throw new TypeError('listener must be a function'); } if (!this._e[name]) { this._e[name] = []; } if (options === null || options === void 0 ? void 0 : options.once) { const onceListener = (...args) => { this.off(name, onceListener); listener.apply(this, args); }; onceListener._ = listener; this._e[name].push(onceListener); } else { this._e[name].push(listener); } return this; } off(name, listener) { if (!listener) { delete this._e[name]; } else { const listeners = this._e[name]; if (listeners) { this._e[name] = listeners.filter((l) => l !== listener && l._ !== listener); } } return this; } /** * Dispatch event * @param name event name * @param args event args */ emit(name, ...args) { if (!this._e[name]) { // log.warn(`No listener for event ${name}`) return this; } this._e[name].forEach((listener) => listener.apply(this, args)); return this; } /** * clear all event listeners */ clear() { this._e = {}; } /** * Register event listener for unity client * @param name event name * @param listener event listener */ addUnityListener(name, listener, options) { if (!name.startsWith('unity:')) { name = `unity:${name}`; } return this.on(name, listener, options); } /** * Remove event listener from unity client * @param name event name * @param listener event listener */ removeUnityListener(name, listener) { if (!name.startsWith('unity:')) { name = `unity:${name}`; } return this.off(name, listener); } } /** * Loading Unity Loader Scripts * @param src script src * @param callbacks callbacks */ function unityLoader(src, callbacks = {}) { const { resolve, reject } = callbacks; if (!isBrowser) return null; if (!src) { reject && reject(new Error(`${src} not found.`)); return null; } function handler(code) { if (code === 'loaded') { resolve && resolve(); } else { reject && reject(new Error(`${src} loading failure.`)); } } let script = window.document.querySelector(`script[src="${src}"]`); if (!script) { script = window.document.createElement('script'); script.async = true; script.setAttribute('data-status', 'loading'); function setAttrListener(status) { script === null || script === void 0 ? void 0 : script.setAttribute('data-status', status); handler(status); } script.addEventListener('load', () => setAttrListener('loaded')); script.addEventListener('error', () => setAttrListener('error')); script.src = src; window.document.body.appendChild(script); } else { handler(script.getAttribute('data-status') === 'loaded' ? 'loaded' : 'error'); } // Return cleanup function return function remove() { if (script && script.parentNode) { script.parentNode.removeChild(script); } }; } function createUnityArgs(ctx, config) { const unityArgs = omit(config, ['loaderUrl']); unityArgs.print = function (msg) { ctx.emit('debug', msg); }; unityArgs.printError = function (msg) { ctx.emit('error', msg); }; return unityArgs; } class UnityWebgl extends UnityWebglEvent { constructor(canvas, config) { super(); this._unity = null; this._loader = null; this._canvas = null; if (!(typeof canvas === 'string' || canvas instanceof HTMLCanvasElement || isObject(canvas))) { throw new TypeError('Parameter canvas is not valid'); } // config if (isObject(canvas)) { config = canvas; } if (!config || !config.loaderUrl || !config.dataUrl || !config.frameworkUrl || !config.codeUrl) { throw new TypeError('UnityConfig is not valid'); } this._config = config; // canvas if (typeof canvas === 'string' || canvas instanceof HTMLCanvasElement) { this.render(canvas); } } /** * @deprecated Use `render()` instead. */ create(canvas) { return this.render(canvas); } /** * Renders the UnityInstance into the target html canvas element. * @param canvas The target html canvas element. */ render(canvas) { if (!isBrowser) return Promise.resolve(); if (this._unity && this._canvas && this._loader) { log.warn('UnityInstance already created'); return Promise.resolve(); } return new Promise((resolve, reject) => { try { const $canvas = queryCanvas(canvas); if (!$canvas) { throw new Error('CanvasElement is not found'); } this._canvas = $canvas; const ctx = this; // Create UnityInstance Arguments const unityArgs = createUnityArgs(this, this._config); this.emit('beforeMount', this); this._loader = unityLoader(this._config.loaderUrl, { resolve() { window .createUnityInstance($canvas, unityArgs, (val) => ctx.emit('progress', val)) .then((ins) => { ctx._unity = ins; ctx.emit('mounted', ctx, ins); resolve(); }) .catch((err) => { throw err; }); }, reject(err) { throw err; }, }); } catch (err) { this._unity = null; this.emit('error', err); reject(err); } }); } /** * Sends a message to the UnityInstance to invoke a public method. * @param {string} objectName Unity scene name. * @param {string} methodName public method name. * @param {any} value an optional method parameter. * @returns */ sendMessage(objectName, methodName, value) { if (!this._unity) { log.warn('Unable to Send Message while Unity is not Instantiated.'); return this; } if (value === undefined || value === null) { this._unity.SendMessage(objectName, methodName); } else { const _value = typeof value === 'object' ? JSON.stringify(value) : value; this._unity.SendMessage(objectName, methodName, _value); } return this; } /** * @deprecated Use `sendMessage()` instead. */ send(objectName, methodName, value) { return this.sendMessage(objectName, methodName, value); } /** * Asynchronously ask for the pointer to be locked on current canvas. * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/requestPointerLock */ requestPointerLock() { if (!this._unity || !this._unity.Module.canvas) { log.warn('Unable to requestPointerLock while Unity is not Instantiated.'); return; } this._unity.Module.canvas.requestPointerLock(); } /** * Takes a screenshot of the canvas and returns a base64 encoded string. * @param {string} dataType Defines the type of screenshot, e.g "image/jpeg" * @param {number} quality Defines the quality of the screenshot, e.g 0.92 * @returns A base 64 encoded string of the screenshot. */ takeScreenshot(dataType, quality) { if (!this._unity || !this._unity.Module.canvas) { log.warn('Unable to take Screenshot while Unity is not Instantiated.'); return; } return this._unity.Module.canvas.toDataURL(dataType, quality); } /** * Enables or disabled the Fullscreen mode of the Unity Instance. * @param {boolean} enabled */ setFullscreen(enabled) { if (!this._unity) { log.warn('Unable to set Fullscreen while Unity is not Instantiated.'); return; } this._unity.SetFullscreen(enabled ? 1 : 0); } /** * Quits the Unity instance and clears it from memory so that Unmount from the DOM. */ unload() { if (!this._unity) { log.warn('Unable to Quit Unity while Unity is not Instantiated.'); return Promise.reject(); } this.emit('beforeUnmount', this); // Unmount unity.loader.js from DOM if (typeof this._loader === 'function') { this._loader(); this._loader = null; } // Unmount unityInstance from memory return this._unity .Quit() .then(() => { this._unity = null; this._canvas = null; this.clear(); this.emit('unmounted'); }) .catch((err) => { log.error('Unable to Unload Unity'); this.emit('error', err); throw err; }); } /** * 保障Unity组件可以安全卸载. 在unity实例从内存中销毁之前保障Dom存在. * * Warning! This is a workaround for the fact that the Unity WebGL instances * which are build with Unity 2021.2 and newer cannot be unmounted before the * Unity Instance is unloaded. */ unsafe_unload() { try { if (!this._unity || !this._unity.Module.canvas) { log.warn('No UnityInstance found.'); return Promise.reject(); } // Re-attaches the canvas to the body element of the document. This way it // wont be removed from the DOM when the component is unmounted. Then the // canvas will be hidden while it is being unloaded. const canvas = this._unity.Module.canvas; document.body.appendChild(canvas); canvas.style.display = 'none'; return this.unload().then(() => { canvas.remove(); }); } catch (e) { return Promise.reject(e); } } } export { UnityWebgl as default };