Skip to content

Commit a0054e5

Browse files
author
Philipp Molitor
authored
Merge pull request #11 from PhilippMolitor/dev
dev 2020.0.2
2 parents 9f01f09 + 557b604 commit a0054e5

File tree

8 files changed

+163
-70
lines changed

8 files changed

+163
-70
lines changed

README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,57 @@ export async function fetchLoaderConfig(
181181
```
182182

183183
You can then use it to construct a `UnityContext` and pass this context to your `UnityRenderer` via the `context` prop.
184+
185+
## Module augmentation
186+
187+
Take the following example:
188+
189+
```typescript
190+
// create some context
191+
const ctx = new UnityContext({ ... });
192+
193+
// handles some "info" event with one parameter of type string
194+
ctx.on('info', (message: string) => {
195+
console.log(message);
196+
});
197+
```
198+
199+
The parameter `message` has to be explicitly defined as `string` each time a handler of for the event name `info` would be registered.
200+
In order to make use of TypeScript to its fullest extent, you can augment an Interface of the library to get autocompletion and type-safety features here.
201+
202+
Put this either in a file importing `react-unity-renderer` or create a new `unity.d.ts` somewhere in your `src` or (if you have that) `typings` directory:
203+
204+
```typescript
205+
// must be imported, else the module will be redefined,
206+
// and this causes all sorts of errors.
207+
import 'react-unity-renderer';
208+
209+
// module augmentation
210+
declare module 'react-unity-renderer' {
211+
// this is the interface providing autocompletion
212+
interface EventSignatures {
213+
// "info" is the event name
214+
// the type on the right side is anything that would match TypeScript's
215+
// Parameters<> helper type
216+
info: [message: string];
217+
218+
// also possible:
219+
info: [string];
220+
'some-event': [number, debug: string];
221+
// note that all parametrs names are just labels, so they are fully optional.
222+
}
223+
}
224+
```
225+
226+
Now, any defined event will be auto-completed with its types for `UnityContext.on(...)`:
227+
228+
```typescript
229+
// create some context
230+
const ctx = new UnityContext({ ... });
231+
232+
// "info" will be suggested by your IDE
233+
// "message" is now of type string
234+
ctx.on('info', (message) => {
235+
console.log(message);
236+
});
237+
```

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-unity-renderer",
3-
"version": "2020.0.1",
3+
"version": "2020.0.2",
44
"description": "React Unity Renderer allows to interactively embed Unity WebGL builds into a React powered project.",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",
@@ -27,10 +27,10 @@
2727
"prepare": "yarn build"
2828
},
2929
"lint-staged": {
30-
"{src,typings}/**/*.{js,jsx,ts,tsx,json}": [
30+
"{src,typings}/**/*.{ts,tsx,json}": [
3131
"prettier --write"
3232
],
33-
"{src,typings}/**/*.{js,jsx,ts,tsx}": [
33+
"{src,typings}/**/*.{ts,tsx}": [
3434
"eslint --fix"
3535
],
3636
"src/**/*.{ts,tsx}": [

src/components/UnityRenderer.ts

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export type UnityRendererProps = Omit<
88
HTMLAttributes<HTMLCanvasElement>,
99
'ref'
1010
> & {
11-
context: UnityContext;
11+
context?: UnityContext;
1212
onUnityProgressChange?: (progress: number) => void;
1313
onUnityReadyStateChange?: (ready: boolean) => void;
1414
onUnityError?: (error: Error) => void;
@@ -31,8 +31,8 @@ export const UnityRenderer: VFC<UnityRendererProps> = ({
3131
onUnityError,
3232
...canvasProps
3333
}: UnityRendererProps): JSX.Element | null => {
34-
const [loader, setLoader] = useState<UnityLoaderService>();
35-
const [ctx, setCtx] = useState<UnityContext>(context);
34+
const [loader] = useState(new UnityLoaderService());
35+
const [ctx, setCtx] = useState<UnityContext | undefined>(context);
3636

3737
// We cannot actually render the `HTMLCanvasElement`, so we need the `ref`
3838
// for Unity and a `JSX.Element` for React rendering.
@@ -70,18 +70,20 @@ export const UnityRenderer: VFC<UnityRendererProps> = ({
7070
* after the unmounting has completed.
7171
*/
7272
function unmount(onComplete?: () => void) {
73-
ctx.shutdown(() => {
74-
// remove the loader script from the DOM
75-
if (loader) loader.unmount();
76-
73+
ctx?.shutdown(() => {
7774
// reset progress / ready state
7875
if (onUnityProgressChange) onUnityProgressChange(0);
7976
if (onUnityReadyStateChange) onUnityReadyStateChange(false);
80-
setLastReadyState(false);
8177

8278
// callbck
8379
if (onComplete) onComplete();
8480
});
81+
82+
setLastReadyState(false);
83+
setCtx(undefined);
84+
85+
// remove the loader script from the DOM
86+
loader.unmount();
8587
}
8688

8789
/**
@@ -92,56 +94,53 @@ export const UnityRenderer: VFC<UnityRendererProps> = ({
9294
* Unity instance.
9395
*/
9496
async function mount(): Promise<void> {
95-
try {
96-
// get the current loader configuration from the UnityContext
97-
const c = ctx.getConfig();
98-
99-
// attach Unity's native JavaScript loader
100-
await loader!.execute(c.loaderUrl);
101-
102-
const instance = await window.createUnityInstance(
103-
renderer!,
104-
{
105-
dataUrl: c.dataUrl,
106-
frameworkUrl: c.frameworkUrl,
107-
codeUrl: c.codeUrl,
108-
streamingAssetsUrl: c.streamingAssetsUrl,
109-
companyName: c.companyName,
110-
productName: c.productName,
111-
productVersion: c.productVersion,
112-
},
113-
(p) => onUnityProgress(p)
97+
if (!ctx || !renderer)
98+
throw new Error(
99+
'cannot mount unity instance without a context or renderer'
114100
);
115101

116-
// set the instance for further JavaScript <--> Unity communication
117-
ctx.setInstance(instance);
118-
} catch (e) {
119-
unmount(() => {
120-
if (onUnityError) onUnityError(e);
121-
});
122-
}
102+
// get the current loader configuration from the UnityContext
103+
const c = ctx.getConfig();
104+
105+
// attach Unity's native JavaScript loader
106+
await loader.execute(c.loaderUrl);
107+
108+
const instance = await window.createUnityInstance(
109+
renderer,
110+
{
111+
dataUrl: c.dataUrl,
112+
frameworkUrl: c.frameworkUrl,
113+
codeUrl: c.codeUrl,
114+
streamingAssetsUrl: c.streamingAssetsUrl,
115+
companyName: c.companyName,
116+
productName: c.productName,
117+
productVersion: c.productVersion,
118+
},
119+
(p) => onUnityProgress(p)
120+
);
121+
122+
// set the instance for further JavaScript <--> Unity communication
123+
ctx.setInstance(instance);
123124
}
124125

125126
// on loader + renderer ready
126127
useEffect(() => {
127-
if (!loader || !renderer) return;
128+
if (!ctx || !renderer) return;
128129

129130
mount().catch((e) => {
130131
if (onUnityError) onUnityError(e);
131-
ctx.shutdown();
132+
ctx?.shutdown();
132133
});
133-
}, [loader, renderer, ctx]);
134+
}, [ctx, renderer]);
134135

135136
// on context change
136137
useEffect(() => {
137-
unmount(() => setCtx(context));
138+
if (context) setCtx(context);
139+
else unmount();
138140
}, [context]);
139141

140142
// on mount
141143
useEffect(() => {
142-
// create the loader service
143-
setLoader(new UnityLoaderService());
144-
145144
// create the renderer and let the ref callback set its handle
146145
setCanvas(
147146
createElement('canvas', {

src/components/__tests__/UnityRenderer.test.tsx

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,6 @@ import { UnityContext } from '../../lib/context';
44
import { UnityRenderer } from '../UnityRenderer';
55

66
describe('<UnityRenderer /> (unconfigured)', () => {
7-
const loaderUrl = 'http://example.com/script.js';
8-
9-
const ctx = new UnityContext({
10-
loaderUrl: loaderUrl,
11-
codeUrl: '',
12-
dataUrl: '',
13-
frameworkUrl: '',
14-
});
15-
167
let renderer: ReactWrapper<typeof UnityRenderer>;
178
let progress = 0;
189
let ready = false;
@@ -21,7 +12,6 @@ describe('<UnityRenderer /> (unconfigured)', () => {
2112
beforeEach(() => {
2213
renderer = mount<typeof UnityRenderer>(
2314
<UnityRenderer
24-
context={ctx}
2515
onUnityProgressChange={(p) => (progress = p)}
2616
onUnityReadyStateChange={(r) => (ready = r)}
2717
onUnityError={() => (error = true)}
@@ -34,10 +24,6 @@ describe('<UnityRenderer /> (unconfigured)', () => {
3424
expect(renderer).toBeDefined();
3525
});
3626

37-
it('uses the context prop', async () => {
38-
expect(renderer.prop('context')).toBe(ctx);
39-
});
40-
4127
it('uses the className prop', async () => {
4228
expect(renderer.prop('className')).toBe('test');
4329
});

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ export {
33
UnityContext,
44
UnityLoaderConfig,
55
UnityInstanceConfig,
6+
EventSignatures,
67
} from './lib/context';
78
export { UnityRenderer, UnityRendererProps } from './components/UnityRenderer';

src/lib/context.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export interface UnityInstanceConfig {
2-
frameworkUrl: string;
32
codeUrl: string;
3+
frameworkUrl: string;
44
dataUrl: string;
55
memoryUrl?: string;
66
symbolsUrl?: string;
@@ -15,7 +15,22 @@ export interface UnityLoaderConfig extends UnityInstanceConfig {
1515
loaderUrl: string;
1616
}
1717

18-
type UnityEventCallback = (...params: any) => void;
18+
/**
19+
* An interface containing event names and their handler parameter signatures.
20+
* This interface is supposed to be augmented via module augmentation by the
21+
* user.
22+
*/
23+
export interface EventSignatures {}
24+
25+
/**
26+
* Refers to a callback function with any parameters.
27+
*/
28+
type EventCallback = (...params: any) => void;
29+
30+
/**
31+
* Defines a weak union type, which can fallback to another type.
32+
*/
33+
type WeakUnion<T, F> = T | (F & {});
1934

2035
/**
2136
* Defines a Unity WebGL context.
@@ -29,7 +44,7 @@ export class UnityContext {
2944

3045
private instance?: UnityInstance;
3146

32-
private eventCallbacks: { [name: string]: UnityEventCallback } = {};
47+
private eventCallbacks: { [name: string]: EventCallback } = {};
3348

3449
/**
3550
* Creates a new `UnityContext` and registers the global event callback.
@@ -92,7 +107,7 @@ export class UnityContext {
92107
}
93108

94109
/**
95-
* Emits a remote procedure call towards the running Unity instance.
110+
* Emits a message to the running Unity instance.
96111
*
97112
* @param {string} objectName The `GameObject` on which to call the method.
98113
* @param {string} methodName The name of the method which should be invoked.
@@ -118,7 +133,12 @@ export class UnityContext {
118133
* @param {UnityEventCallback} callback The callback which should be invoked
119134
* upon the occurence of this event.
120135
*/
121-
public on<T extends UnityEventCallback>(name: string, callback: T): void {
136+
public on<T extends WeakUnion<keyof EventSignatures, string>>(
137+
name: WeakUnion<keyof EventSignatures, T>,
138+
callback: (
139+
...params: T extends keyof EventSignatures ? EventSignatures[T] : any
140+
) => void
141+
): void {
122142
this.eventCallbacks[name] = callback;
123143
}
124144

@@ -141,7 +161,7 @@ export class UnityContext {
141161
* @returns {UnityEventCallback} The callback which should
142162
* handle the event.
143163
*/
144-
private bridgeCallback(name: string): UnityEventCallback {
164+
private bridgeCallback(name: string): EventCallback {
145165
if (this.eventCallbacks && this.eventCallbacks[name])
146166
return this.eventCallbacks[name];
147167

src/lib/loader.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export class UnityLoaderService {
2-
private documentHead: HTMLHeadElement = document.querySelector('head')!;
2+
private head: HTMLHeadElement = document.querySelector('head')!;
33

44
private script?: HTMLScriptElement;
55

@@ -19,7 +19,10 @@ export class UnityLoaderService {
1919
return resolve();
2020

2121
// another script is currently loaded
22-
if (this.script) this.script.remove();
22+
if (this.script) {
23+
this.script.remove();
24+
this.script = undefined;
25+
}
2326

2427
// create script node
2528
this.script = document.createElement('script');
@@ -31,7 +34,7 @@ export class UnityLoaderService {
3134
reject(new Error(`cannot download unity loader from: ${url}`));
3235

3336
// attach
34-
this.documentHead.appendChild(this.script);
37+
this.head.appendChild(this.script);
3538
});
3639
}
3740

@@ -40,5 +43,6 @@ export class UnityLoaderService {
4043
*/
4144
public unmount(): void {
4245
this.script?.remove();
46+
this.script = undefined;
4347
}
4448
}

0 commit comments

Comments
 (0)