Lessons

Running Unity WebGL Inside a React App

In this tutorial, I walk you through running a Unity game (built with WebGL) inside a React app. This will allow you run a browser game and surround it with React.

At the start of 2021, I wanted to very much this. I want to build a game in Unity but have all additional screens (settings, help screens, setup screens) all built using React. I want the game to work on most web browser. I believe internet speeds are getting faster, and most computers are moving to browser being the platform.

I found this library called unity-react-webgl. It is very well maintain and works. Please star it if you use it. This will help the creator know it's value (At least one of the ways).

In this tutorial we will follow a few steps:

  1. Setup a new Unity Game
  2. Build the Unity Game via command line
  3. Setup the React app
  4. Trigger functions in Unity from the React app
  5. Trigger functions in React from the Unity game

I will try to keep this up to date but if things change let me know. I'm using Unity 2019.4.17f1. I'm going to do this project on a Mac OSx computer.

Setup a New Unity Game

Create Game

Let's start by opening up Unity and building a basic game. It doesn't have to do anything, other than render a plane. I'm going to create a new game.

Screenshot of Github Workflow Running

I'm using no spaces in my name or file path because it often just causes issues.

Screenshot of Github Workflow Running

Add Plane

Add a plane object to provide our scene with a focal point.

Screenshot of Github Workflow Running

In the inspector on the right, adjust some of the values. Take a look at my position 0, 0, 0.

Screenshot of Github Workflow Running

Adjust Camera

On the left side under the scene name, select Main Camera. In the inspector on the left, set the position to be 0, 0, -7. Set the rotation to be 20, 0, 0.

Screenshot of Github Workflow Running

Test Run

We should be all setup now. Let's make sure this runs inside of Unity. Hit the play button in the centre at the top.

Screenshot of Github Workflow Running

Setup Build

We are going to add a new C# file called Assets/Editor/WebGLBuilder.cs. Your bottom folder structure starts in Assets. You may need to create Editor. You just need to right click to add a new Folder and C# Script.

Screenshot of Github Workflow Running

Screenshot of Github Workflow Running

Screenshot of Github Workflow Running

Screenshot of Github Workflow Running

Now that the file is created, open it in an editor. I've configured my Unity app to default to VSCode.

Put this code in the script:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 using UnityEngine; using UnityEditor; class WebGLBuilder { static void build() { // Place all your scenes here string[] scenes = {"Assets/Scenes/SampleScene.unity"}; string pathToDeploy = "builds/WebGLversion/"; BuildPipeline.BuildPlayer(scenes, pathToDeploy, BuildTarget.WebGL, BuildOptions.None); } }

Don't know what your scenes are? You can go back in your Assets folder and click on the scene. It will show you the path.

Screenshot of Github Workflow Running

It is an array of scenes, you can add multiple. Everything you want to be included in the build. For me, I have just the one. Save that file. Save your scene. Let's run it. This is the slightly annoying part, you can't run two instances of Unity at the same time. To compile it, you will be running it without a UI and in the background. This will stop you from keeping your editor open.

You will have to find the path to your Unity app on your local machine. For me, mine is:

1 /Applications/Unity/Hub/Editor/2019.4.17f1/Unity.app/Contents/MacOS/Unity

My folder structure is as follows:

1 2 3 4 unity-bash-demo/ DemoGame/ Assets/ ... Other folders...

I'm running this command from inside unity-bash-demo. You will have to update $pwd/DemoGame/ to be your game folder. You will also have to update the additional arguments if you are not using WebGL. With Unity closed, I run:

1 /Applications/Unity/Hub/Editor/2019.4.17f1/Unity.app/Contents/MacOS/Unity -quit -batchmode -logFile stdout.log -projectPath "$pwd/DemoGame/" -executeMethod WebGLBuilder.build

I'm currently mid-development on another game and this takes me about 5 minutes to run. Building the game is not the fast command, so please be patient. If you want to see what is going on in the build process, in another terminal window you can run:

1 tail -f stdout.log

This will let you watch the logs file. Just do CTRL + C to exit it.

Screenshot of Github Workflow Running

Screenshot of Github Workflow Running

You should now see a builds folder in the your game folder. For me:

1 unity-bash-demo/DemoGame/builds/WebGLversion/

Screenshot of Github Workflow Running

You will see the output of the build and a runnable WebGL game.

Screenshot of Github Workflow Running

Setup React app

We have the game setup, now it's time to set up the React app.

Create React App

We will use the Create React app as a starting point. I'm going to create it using npx:

1 npx create-react-app <ProjectName>

For me, I'm running:

1 npx create-react-app frontend

Once it's done installing, I'm going to switch into the directory and run it.

1 2 cd frontend npm run start

Screenshot of Github Workflow Running

React Router

I'm going to add React Router because I want a multi-page app. I'm going to have a home page, and a page with the game on it. I need to install React Router with:

1 2 npm install react-router --save npm install react-router-dom --save

I'm going to reorganize the files. It will be in this structure:

1 2 3 4 5 6 7 8 9 10 11 12 package.json src/ containers/ index.js reportWebVitals.js setupTests.js App/ App.js Home/ Home.js Game/ InGame.js

The contents of index.js is:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import React from 'react' import ReactDOM from 'react-dom' import { Route, Switch } from 'react-router' import { BrowserRouter } from 'react-router-dom' import App from './containers/App/App' import Home from './containers/Home/Home' import InGame from './containers/Game/InGame' import reportWebVitals from './reportWebVitals' ReactDOM.render( <React.StrictMode> <BrowserRouter> <App> <Switch> <Route exact path="/" component={Home} /> <Route exact path="/game" component={InGame} /> </Switch> </App> </BrowserRouter> </React.StrictMode>, document.getElementById('root') ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals();

We are setting up two routes: / and /game/. App wraps the whole application. The App.js file is:

1 2 3 4 5 6 7 8 9 10 11 import React from 'react' const App = ({children}) => { return ( <div className="App"> {children} </div> ); } export default App

The Home.js is:

1 2 3 4 5 6 7 8 9 10 11 import React from 'react' const Home = () => { return ( <div> <p>Home page</p> </div> ) } export default Home

The InGame.js is:

1 2 3 4 5 6 7 8 9 10 11 import React from 'react' const InGame = () => { return ( <div> <p>Game</p> </div> ) } export default InGame

Let's test this all out. Run npm run start. Open http://localhost:3000. You should see:

Screenshot of Github Workflow Running

Then go to http://localhost:3000/game/ and you should see:

Screenshot of Github Workflow Running

Setup Unity WebGL

With our game now running, it's time to setup React Unity WebGL library. Once again, show your support to the creator if you use it.

I am using a Unity version that starts with 2019. You can find this information in your Unity Hub.

Screenshot of Github Workflow Running

Following the installation instructions on the README, install the corresponding version:

Screenshot of Github Workflow Running

For me, its:

1 npm install react-unity-webgl@7.x

Once it's complete, we are going to move our build files from the game directory (Probably under Assets) into public/ on the React project. I created a folder called build with public.

Screenshot of Github Workflow Running

Screenshot of Github Workflow Running

Copy all of the files, and do not rename any of them. I did this and it stopped it from working. The config file (For me, WebGLversion.json) is referencing the other file names. It's just easier if you do not rename it.

Your Unity WebGL build files are now in the project. The README example from the repository wasn't perfect. I found I had to do some digging around Github for other examples. If it doesn't work for you, let me know.

We are going to update our src/containers/Game/InGame.js with:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import React from 'react' import Unity, { UnityContent, UnityContext } from 'react-unity-webgl' const InGame = () => { const unityContent = new UnityContent( '../../../build/WebGLversion.json', '../../../build/UnityLoader.js' ) unityContent.on("quitted", () => { console.log('Game quit') }) unityContent.on("loaded", () => { console.log('Game loaded') }) unityContent.on("progress", progression => { console.log('Game loading', progression) }) unityContent.on("error", message => { console.log('Game errored', message) }) unityContent.on("DemoUnityToReact", (params) => { console.log('DemoUnityToReact', params) }) return ( <div> <p>Game</p> <Unity unityContent={unityContent} width="100%" height="100%" /> </div> ) } export default InGame

I've included all of the default React Unity WebGL library lifecycle methods (error, progress, loaded, etc.). The DemoUnityToReact is method that we will create in Unity and trigger that method on the React side. If you run this, you should see:

Screenshot of Github Workflow Running

If you open your console, you can see the lifecycle methods get trigger.

Screenshot of Github Workflow Running

You can add your own loading screens and such.

Your Debug.Log will appear in the console which is very nice for debugging.

Communicating Between Unity & React

Let's setup some communication between Unity and React. I have used this to share player information or pass game specific information (Ex. Game id of the game they just joined).

Unity to React

We want to trigger a React function from Unity. We have already declared DemoUnityToReact in our unityContent.on("DemoUnityToReact". This is is method we will be triggering.

We will need to add a plugin. Open your game and in Assets/Plugins/WebGL/MyPlugin.jslib. You probably will not have this file path or this file, create both. Unity didn't let me create a non-C# file, so I did. I just removed the extension of .cs.

Screenshot of Github Workflow Running

Here are the contents:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 mergeInto(LibraryManager.library, { // Create a new function with the same name as // the event listeners name and make sure the // parameters match as well. DemoUnityToReact: function(busStr) { // Within the function we're going to trigger // the event within the ReactUnityWebGL object // which is exposed by the library to the window. // https://answers.unity.com/questions/1504065/jslib-is-passing-string-as-number.html ReactUnityWebGL.DemoUnityToReact(Pointer_stringify(busStr)); } })

The name needs to match the event listener. For me, it is DemoUnityToReact. This file compiles down to Javascript and not C# so it is Javascript syntax.

Why the Pointer_stringify? The managed string gets modified as it gets compiled. If we didn't have this, we would receive an integer on the other side. I'm not the best person to be explaining this but I've linked out to the Unity Forums.

Why one argument? This was an interesting catch. I just assumed more arguments would work but they don't. You can see in the source of UnityContext model that it takes 0 or 1 arguments. If you want to pass more than one argument or an argument that is not a string, boolean or integer, you should JSON stringify it. This is why I call the variable busStr. It's just acting as a shuttle bus. It isn't anything but the whole object of parameters.

The last step is actually triggering this method. I'm going to trigger this method when left click is used. The script will be in C# and attached to the main camera. Select your Main Camera, scroll to the bottom of the insepctor, hit Add Component, do a search for script, select New Script, and give it a name.

Screenshot of Github Workflow Running

Screenshot of Github Workflow Running

The new script will be in the root of the project. I'm going to create a Scripts/ folder and move it in there.

Screenshot of Github Workflow Running

Screenshot of Github Workflow Running

We will need a second script. This will not be a Unity specific but more a class to stringify and send to React. It will be called SendMessage.cs and should be in the scripts folder. It will NOT be attached to the Main Camera or any other component.

Screenshot of Github Workflow Running

The cotents of SendMessage.cs is:

1 2 3 4 5 6 7 8 9 using System.Runtime.Serialization; using System; using System.Collections.Generic; [System.Serializable] public class SendMessage { public string message; public int secondField; }

We are passing through two fields: message and secondField. The class needs to be serializable (for stringify). I don't believe you need to install anything but this isn't my first time using this package. If you have package issues, let me know and I can update the lesson.

In the HandleClick.cs method:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Runtime.InteropServices; // for DllImport public class HandleClick : MonoBehaviour { // React functions [DllImport("__Internal")] private static extern void DemoUnityToReact (string jsonifyStr); // This function is the one in MyPlugin.jslib // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { if (Input.GetMouseButtonDown(0)) { leftClickDown(); } } void leftClickDown() { Debug.Log("Left click"); if (Application.platform == RuntimePlatform.WebGLPlayer) { SendMessage bus = new SendMessage(); bus.message = "Hello from Unity"; bus.secondField = 123; string busStr = JsonUtility.ToJson(bus); DemoUnityToReact(busStr); } } }

We handle capture the input of the mouse in the update. We call left click, and if they are using the game within a React app then we pass the message through. This makes testing harder. We need to compile to a WebGL build, move the files into our React project and run it.

Let's test this out. I recommend checking your Unity editor for any syntax errors. It will say at the bottom. I have the one warning:

Screenshot of Github Workflow Running

Save the scene and close unity.

We are going to write a build script that builds the Unity game and moves into the React folder. Reminder my folder structure is a mono-repo that looks like:

1 2 3 4 5 6 7 8 frontend/ src/ public/ ... DemoGame/ Assets/ builds/ ...

Screenshot of Github Workflow Running

I'm going to add another folder called scripts to the root. Now it will be:

1 2 3 frontend/ DemoGame/ scripts/

In scripts, I'm going to add build.sh. The script will be:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 GAME_ROOT="./DemoGame" PATH_TO_UNITY_BUILD="./DemoGame/builds/WebGLversion/Build" PATH_TO_REACT_BUILD="./frontend/public/build" echo "--- Beginning to build the frontend" rm -rf $PATH_TO_UNITY_BUILD echo "-------- WebGL build started :: $(date +%T)" /Applications/Unity/Hub/Editor/2019.4.17f1/Unity.app/Contents/MacOS/Unity -quit -batchmode -logFile stdout.log -projectPath "$pwd/$GAME_ROOT/" -executeMethod WebGLBuilder.build echo "-------- WebGL build completed :: $(date +%T)" echo "-------- Move build started :: $(date +%T)" rm -rf $PATH_TO_REACT_BUILD mkdir $PATH_TO_REACT_BUILD # TODO - Add check for build and throw exit 1 if doesnt exist, this means the build failed with Unity cp $PATH_TO_UNITY_BUILD/WebGLversion.data.unityweb $PATH_TO_REACT_BUILD/WebGLversion.data.unityweb cp $PATH_TO_UNITY_BUILD/WebGLversion.json $PATH_TO_REACT_BUILD/WebGLversion.json cp $PATH_TO_UNITY_BUILD/WebGLversion.wasm.code.unityweb $PATH_TO_REACT_BUILD/WebGLversion.wasm.code.unityweb cp $PATH_TO_UNITY_BUILD/WebGLversion.wasm.framework.unityweb $PATH_TO_REACT_BUILD/WebGLversion.wasm.framework.unityweb cp $PATH_TO_UNITY_BUILD/UnityLoader.js $PATH_TO_REACT_BUILD/UnityLoader.js echo "-------- Move build completed :: $(date +%T)"

You will need to update the paths, Unity build path, and some of the naming. I added timestamps since the build can take 5 to 10 minutes. If you are having issues, run each step one at a time. Run this with:

1 bash scripts/build.sh

Screenshot of Github Workflow Running

Open http://localhost:3000/game/, and you should see the game render. If you left click after the game loads, you should see the Debug log and a print out of the parameters (SendMessage instance) coming from Unity.

Screenshot of Github Workflow Running

Just like that you are sending information from Unity to React. On the React side, you would JSON.parse(params) and it would give you the object that you sent through.

React to Unity

Next, we are going to communicate React to Unity. We will add a button to the top of the page and when it is clicked, a method in Unity will print a Debug.Log.

I'm going to rename the camera from Main Camera to MainCamera.

Screenshot of Github Workflow Running

Add a new script. This will be used to catch the new input. It doesn't need to be in a new script, but this is how I'm handling it.

Screenshot of Github Workflow Running

Screenshot of Github Workflow Running

I'm going to move this new script into my scripts/ folder. The contents of HandleInput.cs:

1 2 3 4 5 6 7 8 9 10 using System.Collections; using System.Collections.Generic; using UnityEngine; public class HandleInput : MonoBehaviour { void ReactToUnityMethod(string busStr) { Debug.Log("ReactToUnityMethod :: " + busStr); } }

In the React app, src/containers/Game/InGame.js, and add (to the render method):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const onBtnClick = (e) => { e.preventDefault() unityContent.send( "MainCamera", "ReactToUnityMethod", JSON.stringify({ "foo": "bar", }), ) } return ( <div> <p>Game</p> <button onClick={onBtnClick}>Click</button>`` <Unity unityContent={unityContent} width="100%" height="100%" /> </div> )

For a brief explanation:

1 2 3 4 5 unityContent.send( "<Game Object>", "<Function Name>", "<Argument>", )

Close the your Unity editor and run the build script:

1 bash scripts/build.sh

Screenshot of Github Workflow Running

Now, open the game at http://localhost:3000/game/. Press the click button.

Screenshot of Github Workflow Running

Screenshot of Github Workflow Running

Screenshot of Github Workflow Running

If you open the console log, you should see the Debug log from the Unity method.

Conclusion

That's it for now! You now have a Unity Game running inside your React.js app.

Thanks for reading!