11import '../global' ;
2- import {
3- createElement ,
4- HTMLAttributes ,
5- useLayoutEffect ,
6- useState ,
7- VFC ,
8- } from 'react' ;
92
10- import { UnityLoaderService } from '../loader ' ;
3+ import { createElement , HTMLAttributes , useEffect , useState , VFC } from 'react ' ;
114
125import { UnityContext } from '..' ;
136
14- export type UnityRendererProps = HTMLAttributes <
15- Omit < HTMLCanvasElement , 'ref' >
7+ import { UnityLoaderService } from '../loader' ;
8+
9+ export type UnityRendererProps = Omit <
10+ HTMLAttributes < HTMLCanvasElement > ,
11+ 'ref'
1612> & {
1713 context : UnityContext ;
1814 onUnityProgressChange ?: ( progress : number ) => void ;
1915 onUnityReadyStateChange ?: ( ready : boolean ) => void ;
2016} ;
2117
18+ /**
19+ * A components that renders a Unity WebGL build from a given configuration
20+ * context. Allows bidirectional communication and loading progress tracking.
21+ *
22+ * @param {UnityRendererProps } props Configurtion context, Unity-specific
23+ * callback handlers and default React props for a `HTMLCanvasElement`.
24+ * Note that `ref` is not available due to internal use.
25+ * @returns {(JSX.Element | null) } A `JSX.Element` containing the renderer,
26+ * or `null` if not initialized yet.
27+ */
2228export const UnityRenderer : VFC < UnityRendererProps > = ( {
2329 context,
2430 onUnityProgressChange,
2531 onUnityReadyStateChange,
2632 ...canvasProps
27- } ) : JSX . Element | null => {
28- const [ lastState , setLastState ] = useState < boolean > ( false ) ;
33+ } : UnityRendererProps ) : JSX . Element | null => {
34+ const [ service ] = useState < UnityLoaderService > ( new UnityLoaderService ( ) ) ;
35+
36+ // We cannot actually render the `HTMLCanvasElement`, so we need the `ref`
37+ // for Unity and a `JSX.Element` for React rendering.
2938 const [ canvas , setCanvas ] = useState < JSX . Element > ( ) ;
3039 const [ renderer , setRenderer ] = useState < HTMLCanvasElement > ( ) ;
31- const [ service ] = useState < UnityLoaderService > ( new UnityLoaderService ( ) ) ;
40+
41+ // This is the last state the game was in, either ready or not ready.
42+ // It is used to trigger `onUnityReadyStateChange` reliably.
43+ const [ lastReadyState , setLastReadyState ] = useState < boolean > ( false ) ;
3244
3345 /**
3446 * The callback which will be called from the `unityInstance` while
3547 * the game is loading.
36- * @param {number } progress
48+ * @param {number } progress The progress ranging from `0.0` to `1.0`
3749 */
3850 function onUnityProgress ( progress : number ) : void {
3951 if ( onUnityProgressChange ) onUnityProgressChange ( progress ) ;
4052
4153 // if loading has completed, change ready state
42- if ( lastState === false && progress >= 1.0 ) {
54+ if ( lastReadyState === false && progress >= 1.0 ) {
4355 if ( onUnityReadyStateChange ) onUnityReadyStateChange ( true ) ;
44- setLastState ( true ) ;
45- } else if ( lastState === true ) {
56+ setLastReadyState ( true ) ;
57+ } else if ( lastReadyState === true ) {
4658 // if ready state changed back to false, trigger again
4759 if ( onUnityReadyStateChange ) onUnityReadyStateChange ( false ) ;
48- setLastState ( false ) ;
60+ setLastReadyState ( false ) ;
4961 }
5062 }
5163
52- /**
53- * Creates the `<canvas>` element in which the unity build will be rendered.
54- */
55- function createRendererCanvas ( ) : void {
56- const c = createElement ( 'canvas' , {
57- ...canvasProps ,
58- ref : ( r : HTMLCanvasElement ) => setRenderer ( r ) ,
59- } ) ;
60- setCanvas ( c ) ;
61- }
62-
6364 /**
6465 * Uses the native Unity loader method to attach the Unity instance to
6566 * the renderer `canvas`.
67+ *
68+ * @returns {Promise<void> } A Promise resolving on successful mount of the
69+ * Unity instance.
6670 */
6771 async function mountUnityInstance ( ) : Promise < void > {
6872 if ( ! renderer ) return ;
6973
74+ // get the current loader configuration from the UnityContext
7075 const c = context . getConfig ( ) ;
7176
72- // attach
77+ // attach Unity's native JavaScript loader
7378 await service . attachLoader ( c . loaderUrl ) ;
74-
7579 const nativeUnityInstance = await window . createUnityInstance (
7680 renderer ,
7781 {
@@ -86,30 +90,40 @@ export const UnityRenderer: VFC<UnityRendererProps> = ({
8690 ( p ) => onUnityProgress ( p )
8791 ) ;
8892
93+ // set the instance for further JavaScript <--> Unity communication
8994 context . setInstance ( nativeUnityInstance ) ;
9095 }
9196
97+ // on canvas change
98+ useEffect ( ( ) => {
99+ if ( renderer )
100+ mountUnityInstance ( ) . catch ( ( e ) => {
101+ // eslint-disable-next-line no-console
102+ console . error ( 'failed to mount unity instance: ' , e ) ;
103+ } ) ;
104+ } , [ renderer ] ) ;
105+
92106 // on mount
93- useLayoutEffect ( ( ) => {
94- createRendererCanvas ( ) ;
107+ useEffect ( ( ) => {
108+ // create the renderer and let the ref callback set its handle
109+ setCanvas (
110+ createElement ( 'canvas' , {
111+ ref : ( r : HTMLCanvasElement ) => setRenderer ( r ) ,
112+ ...canvasProps ,
113+ } )
114+ ) ;
95115
96116 // on unmount
97117 return ( ) => {
98118 context . shutdown ( ( ) => {
119+ // remove the loader script from the DOM
99120 service . detachLoader ( ) ;
121+ // reset progress / ready state
122+ if ( onUnityProgressChange ) onUnityProgressChange ( 0 ) ;
100123 if ( onUnityReadyStateChange ) onUnityReadyStateChange ( false ) ;
101124 } ) ;
102125 } ;
103126 } , [ ] ) ;
104127
105- // on canvas change
106- useLayoutEffect ( ( ) => {
107- if ( renderer )
108- mountUnityInstance ( ) . catch ( ( e ) => {
109- // eslint-disable-next-line no-console
110- console . error ( 'failed to mount unity instance: ' , e ) ;
111- } ) ;
112- } , [ renderer ] ) ;
113-
114128 return canvas || null ;
115129} ;
0 commit comments