Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/PhilippMolitor/react-unity-renderer

React Unity Renderer allows to interactively embed Unity WebGL builds into a React powered project.
https://github.com/PhilippMolitor/react-unity-renderer

react react-unity unity unity-webgl unity-webgl-template unity3d webgl webgl2

Last synced: about 2 months ago
JSON representation

React Unity Renderer allows to interactively embed Unity WebGL builds into a React powered project.

Awesome Lists containing this project

README

        

# React Unity Renderer













> This project is heavily inspired by [react-unity-webgl](https://github.com/elraccoone/react-unity-webgl) made by Jeffrey Lanters. This implementation uses function components + hooks, is getting tested continously and has strict linting and formatting rules which are always enforced.

## Installation

NPM

```
npm install --save react-unity-renderer
```

Yarn

```
yarn add react-unity-renderer
```

**Version compatability**

| Unity version | NPM version |
| ----------------- | ----------- |
| `2020` | `2020.*` |
| `2021` | `2020.*` |

## Example usage

TypeScript

```tsx
import { VFC, useState } from 'react';
import {
UnityContext,
UnityRenderer,
UnityLoaderConfig,
} from 'react-unity-renderer';

// get those URLs from your Unity WebGL build.
// you *could* put a JSON in your WebGL template containing this information
// and load that with fetch or axios to assemble your config.
const config: UnityLoaderConfig = {
loaderUrl: '...',
frameworkUrl: '...',
codeUrl: '...',
dataUrl: '...',
// everything from here on is optional
memoryUrl: '',
symbolsUrl: '',
streamingAssetsUrl: '',
companyName: '',
productName: '',
productVersion: '',
};

export const UnityGameComponent: VFC = (): JSX.Element => {
// You need to construct a config or pass it from the props:
const [ctx] = useState(new UnityContext(config));

// Keep track of the game progress and ready state like this:
const [progress, setProgress] = useState(0);
const [ready, setReady] = useState(false);

return (
setProgress(p)}
onUnityReadyStateChange={(s) => setReady(s)}
onUnityError={(e) => console.error(e)}
// has every prop (except ref) from HTMLCanvasElement.
// This means you can use something like style!
// Also it works perfectly with styled-components.
style={{ width: '100%', height: '100%' }} // optional, but a good idea.
/>
);
};
```

:warning: It is recommended to store the `UnityContext`, as well as the progress, ready and error states in a global state. This way you can keep track of the game state in every part of your application. Consider [zustand](https://github.com/pmndrs/zustand) as a lightweight alternative to Redux, MobX & co., as it has every feature needed for this use case and takes way less effort to implement.

## Mitigating the "keyboard capturing issue"

By default, Unity WebGL builds capture the keyboard as soon as they are loaded. This means that all keyboard input on the website is captured by the game, and rendering all ``, `` and similar input methods useless.

To solve this problem, two changes have to be made:

1. Inside your Unity project, add the following code at some point that gets called early in your game:

```cs
#if !UNITY_EDITOR && UNITY_WEBGL
WebGLInput.captureAllKeyboardInput = false;
#endif
```

2. Set the prop `tabIndex={1}` (may need an ESLint ignore rule) on the `` component to enable focus on click.

3. Now clicking the game enables game keyboard input, and clicking the website enables keyboard input on the website.

For more details on the issue, see [this Stack Overflow answer](https://stackoverflow.com/a/60854680).

## Creating a fetchable config from a Unity WebGL template

In order to create a fetchable build config that contains all required keys for `UnityLoaderConfig`, you could add the following to a Unity WebGL template and upload it to a `CORS`-enabled web host (for example Amazon AWS S3).

`build.json`

```json
{
"loaderUrl": "Build/{{{ LOADER_FILENAME }}}",
"frameworkUrl": "Build/{{{ FRAMEWORK_FILENAME }}}",
"codeUrl": "Build/{{{ CODE_FILENAME }}}",
#if MEMORY_FILENAME
"memoryUrl": "Build/{{{ MEMORY_FILENAME }}}",
#endif
#if SYMBOLS_FILENAME
"symbolsUrl": "Build/{{{ SYMBOLS_FILENAME }}}",
#endif
"dataUrl": "Build/{{{ DATA_FILENAME }}}",
"streamingAssetsUrl": "StreamingAssets",
"companyName": "{{{ COMPANY_NAME }}}",
"productName": "{{{ PRODUCT_NAME }}}",
"productVersion": "{{{ PRODUCT_VERSION }}}"
}

```

Take the following example using `fetch`:

`unity-api.ts`

```ts
import { UnityLoaderConfig } from 'react-unity-renderer';

export async function fetchLoaderConfig(
baseUrl: string
): Promise {
// set the URL of where we expect the loader config to be
const url = `${baseUrl}/build.json`;

let response: Response | undefined;

// network or request error
try {
response = await window.fetch(url, { method: 'GET' });
} catch (ex) {
throw new Error('unable to load build info');
}

// invalid response
if (!response || !response.ok) throw new Error('unable to load build info');

// force the type we expect
const data = (await response.json()) as UnityLoaderConfig;

return {
loaderUrl: `${baseUrl}/${data.loaderUrl}`,
frameworkUrl: `${baseUrl}/${data.frameworkUrl}`,
codeUrl: `${baseUrl}/${data.codeUrl}`,
dataUrl: `${baseUrl}/${data.dataUrl}`,
memoryUrl: `${baseUrl}/${data.memoryUrl}`,
symbolsUrl: `${baseUrl}/${data.symbolsUrl}`,
streamingAssetsUrl: `${baseUrl}/${data.streamingAssetsUrl}`,
companyName: `${data.companyName}`,
productName: `${data.productName}`,
productVersion: `${data.productVersion}`,
};
}
```

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

## Receiving events from Unity

### On the Unity side

In order to send events from Unity to the React application, use the global method for that in your `*.jslib` mapping file:

```javascript
mergeInto(LibraryManager.library, {
RunSomeActionInJavaScript: function (message, counter) {
// surround with try/catch to make unity not crash in case the method is
// not defined in the global scope yet
try {
const messageString = Pointer_stringify(message);

// UnityBridge(event: string) returns a callback that calls
// every registered event handler with the provided arguments.
// It also handles unregistered events with a warning!
window.UnityBridge('event-name')(messageString, number);
} catch (e) {}
},
});
```

If the event name has no registered event handlers, the `UnityBridge(event: string)` function will log a warning via `console.warn(...)`.

:warning: Please note that returning values from the `UnityBridge()` method is not supported, as it may call multiple event handlers internally from different `UnityContext`s that are listening for a certain event, e.g. when having two or more renderers in your application. The preferred way to handle this is to emit a message to the correct Unity instance, which this library also supports. This also helps making the communication paths simpler: **Events only go from Unity to JavaScript, Messages only go from JavaScript to Unity.**

### On the React side

```tsx
import { VFC, useState, useEffect } from 'react';
import { UnityContext, UnityRenderer } from 'react-unity-renderer';

export const UnityGameComponent: VFC = (): JSX.Element => {
const [ctx] = useState(new UnityContext({ ... }));

// Register your handlers (make sure your context is valid!)
useEffect(() => {
// No context, no handlers!
if(!ctx) return;

ctx.on('message', (m: string) => console.log(message));
ctx.on('other-message', (n: number) => console.log(message));

// You can also unregister event handlers again!
ctx.off('other-message');
}, [ctx]);

return (

);
};

```

## Emitting messages to Unity

While events are a way to handle actions that were initiated in the Unity game,
messages are a way to communicate the other way, from JavaScript to Unity.

Messages are emitted from the `UnityContext`, the API for emitting then is the same as in the Unity WebGL documentation:

```tsx
import { VFC, useState, useEffect } from 'react';
import { UnityContext, UnityRenderer } from 'react-unity-renderer';

export const UnityGameComponent: VFC = (): JSX.Element => {
const [ctx] = useState(new UnityContext({ ... }));

const [ready, setReady] = useState(false);

// Listen for the Unity instance to be ready
useEffect(() => {
if(ready === true) {
ctx.emit('GameObjectName', 'ScriptMethodName', 'StringOrNumberArgument');
}
}, [ready]);

return (
setReady(s)}
/>
);
};
```

## 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
// The "{} from" part just imports the TypeScript definitions, so
// we do not re-define the whole module, but just augment it.
import {} from '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];

// Note that all parameter names are just labels, so they are fully optional.
// Though, they are displayed when autocompleting, so labels are quite helpful here.
'some-event': [number, string];

// If you want no parameters at all, just supply an empty tuple:
'parameterless-event': [];
}
}
```

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);
});
```

## API

### `UnityRenderer`

```tsx

```

| | | |
|---|---|---|
| context | The context of the game build, which handles loading and event I/O. | `UnityContext` | `undefined` |
| onUnityProgressChange | Callback to execute when the loading progress of the game changes. Ranges from `0.0` to `1.0`. | `(progress: number) => void` | `undefined` |
| onUnityReadyStateChange | Callback to execute when the game build finished loading and begins to render. | `(ready: boolean) => void` | `undefined` |
| onUnityError | Callback which executes when an error occurs while loading the game. Currently Unity limits what errors can be cought, so some errors still appear via `window.alert()`. | `(error: Error) => void` | `undefined` |
| `{...HTMLAttributes}` | All default attributes of a `` element are supported to allow for an atomic component, supporting custom styling and libraries like `styled-components`. | `Omit, 'ref' | 'id'` | |