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
JavaScript
/*!
* 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 };