Skip to content

Commit

Permalink
Merge pull request #11 from PhilippMolitor/dev
Browse files Browse the repository at this point in the history
dev 2020.0.2
  • Loading branch information
Philipp Molitor authored Mar 6, 2021
2 parents 9f01f09 + 557b604 commit a0054e5
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 70 deletions.
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,57 @@ export async function fetchLoaderConfig(
```

You can then use it to construct a `UnityContext` and pass this context to your `UnityRenderer` via the `context` prop.

## Module augmentation

Take the following example:

```typescript
// create some context
const ctx = new UnityContext({ ... });

// handles some "info" event with one parameter of type string
ctx.on('info', (message: string) => {
console.log(message);
});
```

The parameter `message` has to be explicitly defined as `string` each time a handler of for the event name `info` would be registered.
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.

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:

```typescript
// must be imported, else the module will be redefined,
// and this causes all sorts of errors.
import 'react-unity-renderer';

// module augmentation
declare module 'react-unity-renderer' {
// this is the interface providing autocompletion
interface EventSignatures {
// "info" is the event name
// the type on the right side is anything that would match TypeScript's
// Parameters<> helper type
info: [message: string];

// also possible:
info: [string];
'some-event': [number, debug: string];
// note that all parametrs names are just labels, so they are fully optional.
}
}
```

Now, any defined event will be auto-completed with its types for `UnityContext.on(...)`:

```typescript
// create some context
const ctx = new UnityContext({ ... });

// "info" will be suggested by your IDE
// "message" is now of type string
ctx.on('info', (message) => {
console.log(message);
});
```
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-unity-renderer",
"version": "2020.0.1",
"version": "2020.0.2",
"description": "React Unity Renderer allows to interactively embed Unity WebGL builds into a React powered project.",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand All @@ -27,10 +27,10 @@
"prepare": "yarn build"
},
"lint-staged": {
"{src,typings}/**/*.{js,jsx,ts,tsx,json}": [
"{src,typings}/**/*.{ts,tsx,json}": [
"prettier --write"
],
"{src,typings}/**/*.{js,jsx,ts,tsx}": [
"{src,typings}/**/*.{ts,tsx}": [
"eslint --fix"
],
"src/**/*.{ts,tsx}": [
Expand Down
81 changes: 40 additions & 41 deletions src/components/UnityRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type UnityRendererProps = Omit<
HTMLAttributes<HTMLCanvasElement>,
'ref'
> & {
context: UnityContext;
context?: UnityContext;
onUnityProgressChange?: (progress: number) => void;
onUnityReadyStateChange?: (ready: boolean) => void;
onUnityError?: (error: Error) => void;
Expand All @@ -31,8 +31,8 @@ export const UnityRenderer: VFC<UnityRendererProps> = ({
onUnityError,
...canvasProps
}: UnityRendererProps): JSX.Element | null => {
const [loader, setLoader] = useState<UnityLoaderService>();
const [ctx, setCtx] = useState<UnityContext>(context);
const [loader] = useState(new UnityLoaderService());
const [ctx, setCtx] = useState<UnityContext | undefined>(context);

// We cannot actually render the `HTMLCanvasElement`, so we need the `ref`
// for Unity and a `JSX.Element` for React rendering.
Expand Down Expand Up @@ -70,18 +70,20 @@ export const UnityRenderer: VFC<UnityRendererProps> = ({
* after the unmounting has completed.
*/
function unmount(onComplete?: () => void) {
ctx.shutdown(() => {
// remove the loader script from the DOM
if (loader) loader.unmount();

ctx?.shutdown(() => {
// reset progress / ready state
if (onUnityProgressChange) onUnityProgressChange(0);
if (onUnityReadyStateChange) onUnityReadyStateChange(false);
setLastReadyState(false);

// callbck
if (onComplete) onComplete();
});

setLastReadyState(false);
setCtx(undefined);

// remove the loader script from the DOM
loader.unmount();
}

/**
Expand All @@ -92,56 +94,53 @@ export const UnityRenderer: VFC<UnityRendererProps> = ({
* Unity instance.
*/
async function mount(): Promise<void> {
try {
// get the current loader configuration from the UnityContext
const c = ctx.getConfig();

// attach Unity's native JavaScript loader
await loader!.execute(c.loaderUrl);

const instance = await window.createUnityInstance(
renderer!,
{
dataUrl: c.dataUrl,
frameworkUrl: c.frameworkUrl,
codeUrl: c.codeUrl,
streamingAssetsUrl: c.streamingAssetsUrl,
companyName: c.companyName,
productName: c.productName,
productVersion: c.productVersion,
},
(p) => onUnityProgress(p)
if (!ctx || !renderer)
throw new Error(
'cannot mount unity instance without a context or renderer'
);

// set the instance for further JavaScript <--> Unity communication
ctx.setInstance(instance);
} catch (e) {
unmount(() => {
if (onUnityError) onUnityError(e);
});
}
// get the current loader configuration from the UnityContext
const c = ctx.getConfig();

// attach Unity's native JavaScript loader
await loader.execute(c.loaderUrl);

const instance = await window.createUnityInstance(
renderer,
{
dataUrl: c.dataUrl,
frameworkUrl: c.frameworkUrl,
codeUrl: c.codeUrl,
streamingAssetsUrl: c.streamingAssetsUrl,
companyName: c.companyName,
productName: c.productName,
productVersion: c.productVersion,
},
(p) => onUnityProgress(p)
);

// set the instance for further JavaScript <--> Unity communication
ctx.setInstance(instance);
}

// on loader + renderer ready
useEffect(() => {
if (!loader || !renderer) return;
if (!ctx || !renderer) return;

mount().catch((e) => {
if (onUnityError) onUnityError(e);
ctx.shutdown();
ctx?.shutdown();
});
}, [loader, renderer, ctx]);
}, [ctx, renderer]);

// on context change
useEffect(() => {
unmount(() => setCtx(context));
if (context) setCtx(context);
else unmount();
}, [context]);

// on mount
useEffect(() => {
// create the loader service
setLoader(new UnityLoaderService());

// create the renderer and let the ref callback set its handle
setCanvas(
createElement('canvas', {
Expand Down
14 changes: 0 additions & 14 deletions src/components/__tests__/UnityRenderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,6 @@ import { UnityContext } from '../../lib/context';
import { UnityRenderer } from '../UnityRenderer';

describe('<UnityRenderer /> (unconfigured)', () => {
const loaderUrl = 'http://example.com/script.js';

const ctx = new UnityContext({
loaderUrl: loaderUrl,
codeUrl: '',
dataUrl: '',
frameworkUrl: '',
});

let renderer: ReactWrapper<typeof UnityRenderer>;
let progress = 0;
let ready = false;
Expand All @@ -21,7 +12,6 @@ describe('<UnityRenderer /> (unconfigured)', () => {
beforeEach(() => {
renderer = mount<typeof UnityRenderer>(
<UnityRenderer
context={ctx}
onUnityProgressChange={(p) => (progress = p)}
onUnityReadyStateChange={(r) => (ready = r)}
onUnityError={() => (error = true)}
Expand All @@ -34,10 +24,6 @@ describe('<UnityRenderer /> (unconfigured)', () => {
expect(renderer).toBeDefined();
});

it('uses the context prop', async () => {
expect(renderer.prop('context')).toBe(ctx);
});

it('uses the className prop', async () => {
expect(renderer.prop('className')).toBe('test');
});
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export {
UnityContext,
UnityLoaderConfig,
UnityInstanceConfig,
EventSignatures,
} from './lib/context';
export { UnityRenderer, UnityRendererProps } from './components/UnityRenderer';
32 changes: 26 additions & 6 deletions src/lib/context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export interface UnityInstanceConfig {
frameworkUrl: string;
codeUrl: string;
frameworkUrl: string;
dataUrl: string;
memoryUrl?: string;
symbolsUrl?: string;
Expand All @@ -15,7 +15,22 @@ export interface UnityLoaderConfig extends UnityInstanceConfig {
loaderUrl: string;
}

type UnityEventCallback = (...params: any) => void;
/**
* An interface containing event names and their handler parameter signatures.
* This interface is supposed to be augmented via module augmentation by the
* user.
*/
export interface EventSignatures {}

/**
* Refers to a callback function with any parameters.
*/
type EventCallback = (...params: any) => void;

/**
* Defines a weak union type, which can fallback to another type.
*/
type WeakUnion<T, F> = T | (F & {});

/**
* Defines a Unity WebGL context.
Expand All @@ -29,7 +44,7 @@ export class UnityContext {

private instance?: UnityInstance;

private eventCallbacks: { [name: string]: UnityEventCallback } = {};
private eventCallbacks: { [name: string]: EventCallback } = {};

/**
* Creates a new `UnityContext` and registers the global event callback.
Expand Down Expand Up @@ -92,7 +107,7 @@ export class UnityContext {
}

/**
* Emits a remote procedure call towards the running Unity instance.
* Emits a message to the running Unity instance.
*
* @param {string} objectName The `GameObject` on which to call the method.
* @param {string} methodName The name of the method which should be invoked.
Expand All @@ -118,7 +133,12 @@ export class UnityContext {
* @param {UnityEventCallback} callback The callback which should be invoked
* upon the occurence of this event.
*/
public on<T extends UnityEventCallback>(name: string, callback: T): void {
public on<T extends WeakUnion<keyof EventSignatures, string>>(
name: WeakUnion<keyof EventSignatures, T>,
callback: (
...params: T extends keyof EventSignatures ? EventSignatures[T] : any
) => void
): void {
this.eventCallbacks[name] = callback;
}

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

Expand Down
10 changes: 7 additions & 3 deletions src/lib/loader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export class UnityLoaderService {
private documentHead: HTMLHeadElement = document.querySelector('head')!;
private head: HTMLHeadElement = document.querySelector('head')!;

private script?: HTMLScriptElement;

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

// another script is currently loaded
if (this.script) this.script.remove();
if (this.script) {
this.script.remove();
this.script = undefined;
}

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

// attach
this.documentHead.appendChild(this.script);
this.head.appendChild(this.script);
});
}

Expand All @@ -40,5 +43,6 @@ export class UnityLoaderService {
*/
public unmount(): void {
this.script?.remove();
this.script = undefined;
}
}
Loading

0 comments on commit a0054e5

Please sign in to comment.