Skip to content

Commit 978ed3e

Browse files
committed
refactor!: Refactoring event system and core functions
- 重写 UnityWebglEvent 类,将注册事件方式分开; - 重构 web 和 unity 通信方式; - 优化 create 方法; - 移除遗留的 event type;
1 parent 5deba6b commit 978ed3e

File tree

6 files changed

+798
-0
lines changed

6 files changed

+798
-0
lines changed

lib/core/src/event.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { isBrowser } from './utils'
2+
3+
interface EventListener {
4+
(...args: any[]): void
5+
_?: EventListener
6+
}
7+
type EventListenerOptions = {
8+
once?: boolean
9+
}
10+
type EventMap = Record<string, EventListener[]>
11+
12+
export class UnityWebglEvent {
13+
private _e: EventMap // event map
14+
15+
constructor() {
16+
this._e = {}
17+
if (isBrowser) {
18+
// Register Unity event trigger to the window object
19+
window.dispatchUnityEvent = (name: string, ...args: any[]) => {
20+
if (!name.startsWith('unity:')) {
21+
name = `unity:${name}`
22+
}
23+
this.emit.apply(this, [name, ...args])
24+
}
25+
}
26+
}
27+
28+
/**
29+
* Register event listener
30+
* @param name event name
31+
* @param listener event listener
32+
* @param options event listener options
33+
*/
34+
on(name: string, listener: EventListener, options?: EventListenerOptions) {
35+
if (typeof listener !== 'function') {
36+
throw new TypeError('listener must be a function')
37+
}
38+
39+
if (!this._e[name]) {
40+
this._e[name] = []
41+
}
42+
43+
if (options?.once) {
44+
const onceListener = (...args: any[]) => {
45+
this.off(name, onceListener)
46+
listener.apply(this, args)
47+
}
48+
onceListener._ = listener
49+
50+
this._e[name].push(onceListener)
51+
} else {
52+
this._e[name].push(listener)
53+
}
54+
return this
55+
}
56+
57+
/**
58+
* Remove event listener
59+
* @param name event name
60+
* @param listener event listener
61+
*/
62+
off(name: string, listener?: EventListener) {
63+
if (!listener) {
64+
delete this._e[name]
65+
} else {
66+
const listeners = this._e[name]
67+
if (listeners) {
68+
this._e[name] = listeners.filter((l) => l !== listener && l._ !== listener)
69+
}
70+
}
71+
return this
72+
}
73+
74+
/**
75+
* Dispatch event
76+
* @param name event name
77+
* @param args event args
78+
*/
79+
emit(name: string, ...args: any[]) {
80+
if (!this._e[name]) {
81+
console.warn(`No listener for event ${name}`)
82+
return this
83+
}
84+
85+
this._e[name].forEach((listener) => listener(...args))
86+
return this
87+
}
88+
89+
/**
90+
* clear all event listeners
91+
*/
92+
protected clear() {
93+
this._e = {}
94+
}
95+
96+
/**
97+
* Register event listener for unity client
98+
* @param name event name
99+
* @param listener event listener
100+
*/
101+
addUnityListener(name: string, listener: EventListener, options?: EventListenerOptions) {
102+
if (!name.startsWith('unity:')) {
103+
name = `unity:${name}`
104+
}
105+
return this.on(name, listener, options)
106+
}
107+
108+
/**
109+
* Remove event listener from unity client
110+
* @param name event name
111+
* @param listener event listener
112+
*/
113+
removeUnityListener(name: string, listener?: EventListener) {
114+
if (!name.startsWith('unity:')) {
115+
name = `unity:${name}`
116+
}
117+
return this.off(name, listener)
118+
}
119+
}

lib/core/src/global.d.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { UnityArguments, UnityInstance } from './types'
2+
3+
declare global {
4+
/**
5+
* Dispatches an event that has been registered to all event systems.
6+
* @param eventName The name of the event.
7+
* @param parameters The parameters to pass to the event.
8+
*/
9+
function dispatchUnityEvent(eventName: string, ...parameters: any[]): void
10+
11+
/**
12+
* Creates a new UnityInstance.
13+
* @param canvas The target html canvas element.
14+
* @param config The config object contains the build configuration.
15+
* @param onProgress The on progress event listener.
16+
* @returns A promise resolving when instantiated successfully.
17+
*/
18+
function createUnityInstance(
19+
canvas: HTMLCanvasElement,
20+
config: UnityArguments,
21+
onProgress?: (progress: number) => void
22+
): Promise<UnityInstance>
23+
24+
/**
25+
* Due to some developers wanting to use the window object as a global scope
26+
* in order to invoke the create Unity Instance and dispatch React Unity Event
27+
* functions, we need to declare the window object as a global type.
28+
*/
29+
interface Window {
30+
/**
31+
* Creates a new UnityInstance.
32+
*/
33+
createUnityInstance: typeof createUnityInstance
34+
35+
/**
36+
* Dispatches an event that has been registered to all event systems.
37+
*/
38+
dispatchUnityEvent: typeof dispatchUnityEvent
39+
}
40+
}
41+
42+
export {}

lib/core/src/index.ts

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import { UnityWebglEvent } from './event'
2+
import { unityLoader } from './loader'
3+
import { isBrowser, isObject, omit, queryCanvas } from './utils'
4+
import { UnityConfig, UnityArguments, UnityInstance } from './types'
5+
6+
type CanvasElementOrString = HTMLCanvasElement | string
7+
8+
function createUnityArgs(ctx: UnityWebgl, config: UnityConfig): UnityArguments {
9+
const unityArgs: UnityArguments = omit(config, ['loaderUrl'])
10+
unityArgs.print = function (msg: string) {
11+
ctx.emit('debug', msg)
12+
}
13+
unityArgs.printError = function (msg: string) {
14+
ctx.emit('error', msg)
15+
}
16+
return unityArgs
17+
}
18+
19+
class UnityWebgl extends UnityWebglEvent {
20+
private _config: UnityConfig
21+
private _unity: UnityInstance | null = null
22+
private _loader: (() => void) | null = null
23+
private _canvas: HTMLCanvasElement | null = null
24+
25+
constructor(canvas: CanvasElementOrString, config: UnityConfig)
26+
constructor(config: UnityConfig)
27+
constructor(canvas: CanvasElementOrString | UnityConfig, config?: UnityConfig) {
28+
super()
29+
if (!(typeof canvas === 'string' || canvas instanceof HTMLCanvasElement || isObject(canvas))) {
30+
throw new TypeError('Parameter canvas is not valid')
31+
}
32+
33+
// config
34+
if (isObject(canvas)) {
35+
config = canvas as UnityConfig
36+
}
37+
if (
38+
!config ||
39+
!config.loaderUrl ||
40+
!config.dataUrl ||
41+
!config.frameworkUrl ||
42+
!config.codeUrl
43+
) {
44+
throw new TypeError('UnityConfig is not valid')
45+
}
46+
this._config = config
47+
48+
// canvas
49+
if (typeof canvas === 'string' || canvas instanceof HTMLCanvasElement) {
50+
this.create(canvas)
51+
}
52+
}
53+
54+
/**
55+
* Creating Unity Instance
56+
* @param canvas The target html canvas element.
57+
*/
58+
create(canvas: CanvasElementOrString): Promise<void> {
59+
if (!isBrowser) return Promise.resolve()
60+
61+
if (this._unity && this._canvas && this._loader) {
62+
console.warn('Unity instance already created')
63+
return Promise.resolve()
64+
}
65+
66+
return new Promise((resolve, reject) => {
67+
try {
68+
const $canvas = queryCanvas(canvas)
69+
if (!$canvas) {
70+
throw new Error('CanvasElement is not found')
71+
}
72+
this._canvas = $canvas
73+
const ctx = this
74+
// Create Unity instantiation Arguments
75+
const unityArgs = createUnityArgs(this, this._config)
76+
77+
this.emit('beforeMount', this)
78+
79+
this._loader = unityLoader(this._config.loaderUrl, {
80+
resolve() {
81+
window
82+
.createUnityInstance($canvas, unityArgs, (val: number) => ctx.emit('progress', val))
83+
.then((ins: UnityInstance) => {
84+
ctx._unity = ins
85+
ctx.emit('mounted', ctx, ins)
86+
resolve()
87+
})
88+
.catch((err) => {
89+
throw err
90+
})
91+
},
92+
reject(err) {
93+
throw err
94+
},
95+
})
96+
} catch (err) {
97+
this._unity = null
98+
this.emit('error', err)
99+
reject(err)
100+
}
101+
})
102+
}
103+
104+
/**
105+
* Sends a message to the UnityInstance to invoke a public method.
106+
* @param {string} objectName Unity scene name.
107+
* @param {string} methodName public method name.
108+
* @param {any} value an optional method parameter.
109+
* @returns
110+
*/
111+
sendMessage(objectName: string, methodName: string, value?: any) {
112+
if (!this._unity) {
113+
console.warn('Unable to Send Message while Unity is not Instantiated.')
114+
return this
115+
}
116+
117+
if (value === undefined || value === null) {
118+
this._unity.SendMessage(objectName, methodName)
119+
} else {
120+
const _value = typeof value === 'object' ? JSON.stringify(value) : value
121+
this._unity.SendMessage(objectName, methodName, _value)
122+
}
123+
return this
124+
}
125+
/**
126+
* @deprecated Use sendMessage instead.
127+
*/
128+
send(objectName: string, methodName: string, value?: any) {
129+
return this.sendMessage(objectName, methodName, value)
130+
}
131+
132+
/**
133+
* Asynchronously ask for the pointer to be locked on current canvas.
134+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Element/requestPointerLock
135+
*/
136+
requestPointerLock(): void {
137+
if (!this._unity || !this._unity.Module.canvas) {
138+
console.warn('Unable to requestPointerLock while Unity is not Instantiated.')
139+
return
140+
}
141+
this._unity.Module.canvas.requestPointerLock()
142+
}
143+
144+
/**
145+
* Takes a screenshot of the canvas and returns a base64 encoded string.
146+
* @param {string} dataType Defines the type of screenshot, e.g "image/jpeg"
147+
* @param {number} quality Defines the quality of the screenshot, e.g 0.92
148+
* @returns A base 64 encoded string of the screenshot.
149+
*/
150+
takeScreenshot(dataType?: string, quality?: any): string | undefined {
151+
if (!this._unity || !this._unity.Module.canvas) {
152+
console.warn('Unable to take Screenshot while Unity is not Instantiated.')
153+
return
154+
}
155+
156+
return this._unity.Module.canvas.toDataURL(dataType, quality)
157+
}
158+
159+
/**
160+
* Enables or disabled the Fullscreen mode of the Unity Instance.
161+
* @param {boolean} enabled
162+
*/
163+
setFullscreen(enabled: boolean) {
164+
if (!this._unity) {
165+
console.warn('Unable to set Fullscreen while Unity is not Instantiated.')
166+
return
167+
}
168+
169+
this._unity.SetFullscreen(enabled ? 1 : 0)
170+
}
171+
172+
/**
173+
* Quits the Unity instance and clears it from memory so that Unmount from the DOM.
174+
*/
175+
unload(): Promise<void> {
176+
if (!this._unity) {
177+
console.warn('Unable to Quit Unity while Unity is not Instantiated.')
178+
return Promise.reject()
179+
}
180+
this.emit('beforeUnmount', this)
181+
182+
// Unmount unity.loader.js from DOM
183+
if (typeof this._loader === 'function') {
184+
this._loader()
185+
this._loader = null
186+
}
187+
// Unmount unityInstance from memory
188+
return this._unity
189+
.Quit()
190+
.then(() => {
191+
this._unity = null
192+
this._canvas = null
193+
this.clear()
194+
195+
this.emit('unmounted')
196+
})
197+
.catch((err) => {
198+
console.error('Unable to Unload Unity')
199+
this.emit('error', err)
200+
throw err
201+
})
202+
}
203+
204+
/**
205+
* 保障Unity组件可以安全卸载. 在unity实例从内存中销毁之前保障Dom存在.
206+
*
207+
* Warning! This is a workaround for the fact that the Unity WebGL instances
208+
* which are build with Unity 2021.2 and newer cannot be unmounted before the
209+
* Unity Instance is unloaded.
210+
*/
211+
unsafe_unload(): Promise<void> {
212+
try {
213+
if (!this._unity || !this._unity.Module.canvas) {
214+
console.warn('No Unity Instance found.')
215+
return Promise.reject()
216+
}
217+
// Re-attaches the canvas to the body element of the document. This way it
218+
// wont be removed from the DOM when the component is unmounted. Then the
219+
// canvas will be hidden while it is being unloaded.
220+
const canvas = this._unity.Module.canvas as HTMLCanvasElement
221+
document.body.appendChild(canvas)
222+
canvas.style.display = 'none'
223+
return this.unload().then(() => {
224+
canvas.remove()
225+
})
226+
} catch (e) {
227+
return Promise.reject(e)
228+
}
229+
}
230+
}
231+
232+
export default UnityWebgl
233+
export * from './types'

0 commit comments

Comments
 (0)