Getting Unity to work with Next.js
One of my main hobbies at the moment is game development, and I recently started getting into game jams. In case you don't know, a game jam is an event where you create a game within a time frame. Game jams generally have some kind of theme and limitation.
For example, the first game jam I participated in recently was Boss Rush Jam 2024. For this jam, you had to create a boss rush game and the theme was exchange. In a way this was kind of a double theme. I ended up creating a game with only bosses where you could exchange your hearts for buffs.
If you're interested in checking game jams out, you can find a list of current and upcoming jams here.
Which leads into this article. I thought it would be a neat idea to showcase some of the games I have made for these game jams on this blog and talk about them. Initially, adding a Unity project to React seemed fairly straightforward, but there are definitely some issues and extra steps to make the user experience better.
You can find a link to the repo for this project here. This repo is a continuation from my previous blog post on how I made this blog with Next.js and markdown here.
Imports
First we'll need to import a few libraries. Mainly the React Unity library, but I'm also going to use react icons for some buttons.
npm install react-unity-webgl
npm install react-icons --save
Exporting your Unity project
We'll need a few files for the UnityContext
. By default, the UnityContext
needs the following four properties: dataURL
, frameworkURL
, codeURL
and loaderURL
. You'll need to export your Unity project for the Webgl platform and grab the four files in the Build
folder. Move these four files into your public folder on your server. We won't need any of the other files that Unity exports, so you can discard them. You can read more here about how exporting works in Unity. I'm also including a very simple game in my repo here if you want to grab the files from that to test.
You may need to disable compression for the build to get this to work, shown in the picture below. This can be found in the player settings in the Unity editor.
I also like to change the background color to black on the Unity log screen. This is completely personal preference though.
Creating our Unity Component
Let's start by creating a component and setting up a basic implementation. By default, the Unity component will load when the user visits the page. This means the game will play immediately, this is not the user experience I want. So we're going to hide the game behind a button, then the user can click to load the game when they are ready to play it. If the user clicks the load game button we'll update the state of loadGame
and show the Unity component.
Currently this component only has one attribute gameLink
and will be used in the paths to the Unity build files that you added to your public folder.
Let's also add a size for our unity canvas. We add this by adding a style prop to the Unity component. I'm going with a width of 1000 pixels here because the content size of my blog is currently 1024 pixels. We can use this site to calculate the height based on a 16:9 ratio.
'use client';
import { useState } from 'react';
import { Unity, useUnityContext } from 'react-unity-webgl';
export default function UnityGameLoader({gameLink}:{gameLink:string}) {
const [loadGame, setLoadGame] = useState(false);
const {unityProvider} = useUnityContext({
dataUrl: `/game_builds/${gameLink}/game.data`,
frameworkUrl: `/game_builds/${gameLink}/game.framework.js`,
codeUrl: `/game_builds/${gameLink}/game.wasm`,
loaderUrl: `/game_builds/${gameLink}/game.loader.js`
});
return (
<div className='rounded-sm p-[6px] bg-black w-[1024px] h-[574px] flex'>
{(loadGame) ?
<Unity unityProvider={unityProvider} style={{width: 1000, height: 562}} />
:
<div className='flex flex-col w-full items-center'>
<button className='btn btn-outline btn-info m-auto rounded-sm' onClick={(e)=>{setLoadGame(true)}}>Load game</button>
</div>
}
</div>
);
Unloading the component
Once you have the above code set up and working, the next thing we need to do is correctly unload the game when we navigate away from the page. I'm currently using Next.js (14.2.1) and Unity React library (9.5.1) and these versions have an issue where if you use a Link
component to navigate away, the Unity component doesn't unload and the game can be heard on different pages after navigating away.
You'll need to update your UnityContext
to include isLoaded
and unload
like this const { unityProvider, unload, isLoaded } = useUnityContext({});
. The UnityContext has a lot of variables and functions that we're going to be using over the course of this article.
Then we can add a useEffect to unmount the component. This should run the unLoad
function when we navigate away.
useEffect(()=>{
const stopGame = async () => {
await unload();
};
return(()=>{
if(isLoaded){
stopGame();
}
});
}, [isLoaded]);
Here is the updated code so far:
'use client';
import { useEffect, useState } from 'react';
import { Unity, useUnityContext } from 'react-unity-webgl';
export default function UnityGameLoader({gameLink}:{gameLink:string}) {
const [loadGame, setLoadGame] = useState(false);
const {unityProvider,unload,isLoaded} = useUnityContext({
dataUrl: `/game_builds/${gameLink}/game.data`,
frameworkUrl: `/game_builds/${gameLink}/game.framework.js`,
codeUrl: `/game_builds/${gameLink}/game.wasm`,
loaderUrl: `/game_builds/${gameLink}/game.loader.js`
});
useEffect(()=>{
const stopGame = async () => {
await unload();
};
return(()=>{
if(isLoaded){
stopGame();
}
});
}, [isLoaded]);
return (
<div className='rounded-sm p-[6px] bg-black w-[1024px] h-[574px] flex'>
{(loadGame) ?
<Unity unityProvider={unityProvider} style={{width: 1000, height: 562}} />
:
<div className='flex flex-col w-full items-center'>
<button className='btn btn-outline btn-info m-auto rounded-sm' onClick={(e)=>{setLoadGame(true)}}>Load game</button>
</div>
}
</div>
);
Adding fullscreen
Next thing we'll add is a fullscreen button. Update the Unity Context to include the requestFullscreen
function like this const { unityProvider, unload, isLoaded, requestFullscreen } = useUnityContext({})
. When the user clicks the button, call the requestFullscreen
function.
Here is the updated code so far:
'use client';
import { useEffect, useState } from 'react';
import { Unity, useUnityContext } from 'react-unity-webgl';
import { AiOutlineFullscreen } from 'react-icons/ai';
export default function UnityGameLoader({gameLink}:{gameLink:string}) {
const [loadGame, setLoadGame] = useState(false);
const {unityProvider,unload,isLoaded,requestFullscreen} = useUnityContext({
dataUrl: `/game_builds/${gameLink}/game.data`,
frameworkUrl: `/game_builds/${gameLink}/game.framework.js`,
codeUrl: `/game_builds/${gameLink}/game.wasm`,
loaderUrl: `/game_builds/${gameLink}/game.loader.js`
});
useEffect(()=>{
const stopGame = async () => {
await unload();
};
return(()=>{
if(isLoaded){
stopGame();
}
});
}, [isLoaded]);
return (
<div className='rounded-sm p-[6px] bg-black w-[1024px] h-[574px] flex'>
{(loadGame) ?
<div className='relative'>
<div className='absolute flex flex-col top-0 right-[-98px]'>
<button key='fullscreen' className='btn btn-info mb-4 rounded-sm' onClick={(e)=>{requestFullscreen(true)}}>
<AiOutlineFullscreen size={32}/>
</button>
</div>
<Unity unityProvider={unityProvider} style={{width: 1000, height: 562}} />
</div>
:
<div className='flex flex-col w-full items-center'>
<button className='btn btn-outline btn-info m-auto rounded-sm' onClick={(e)=>{setLoadGame(true)}}>Load game</button>
</div>
}
</div>
);
Muting audio outside of Unity
The last big improvement I would like to make is to add a mute. I want a button outside of the game so the user can click it to mute the game without having to go into the settings of the game. Let's update the UnityContext
again to get access to the sendMessage
function.
This function is pretty neat, it allows you to send data directly into Unity. The function takes in 2 or 3 parameters. The first parameter is for the Unity script name, the second one is the function name and the last parameter is whatever you want to send into the Unity function.
Let's update our UnityContext
to include the sendMessage
function like this const { unityProvider, unload, isLoaded, requestFullscreen, sendMessage } = useUnityContext({});
. We can then add a new state for muted and create a function in our component to handle the button click.
const handleMute = () => {
if(muted) {
sendMessage('AudioManager','Webgl_Unmute');
setMute(false);
} else {
sendMessage('AudioManager','Webgl_Mute');
setMute(true);
}
};
As for how my AudioManager
script is set up in Unity, I'll include a basic example. Basically, I have a script named AudioManager
that I have set up to be a singleton. The AudioManager is also set to not destroy with DontDestroyOnLoad(gameObject);
. This means the singleton object will persist through Unity game scenes. I then have an Audio mixer that has a master channel and then has two sub channels - music and sfx. These each have an exposed parameter that controls the volume of the three channels. So for the Webgl_Mute
and Webgl_Unmute
functions, I'm changing the volume to 0 or back to its original state via the channel volumes.
I'm including some links here to the Unity documents in case you want to read more about these topics.
What I'm not including, is the creation of audio sources and the functions that interact with these audio sources. This is outside of the scope of this post and would be better suited for its own article.
Example below for the Unity script AudioManager
:
using System.Collections;
using UnityEngine;
using UnityEngine.Audio;
public class AudioManager : MonoBehaviour
{
[SerializeField] private AudioMixer audioMixer;
private bool muted = false;
public static AudioManager Instance { get; private set; }
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
private void OnDestroy()
{
Debug.Log("AudioManager is being destoryed.");
}
public void Webgl_Mute()
{
// Mute audiomixer
muted = true;
}
public void Webgl_Unmute()
{
muted = false;
// Unmute audiomixer
}
}
We can then a mute button next to the fullscreen button with two react icons that show based on the muted
state.
Here is the updated code so far:
'use client';
import { useEffect, useState } from 'react';
import { Unity, useUnityContext } from 'react-unity-webgl';
import { AiOutlineFullscreen } from 'react-icons/ai';
import { IoVolumeHighOutline, IoVolumeMuteOutline } from 'react-icons/io5';
export default function UnityGameLoader({gameLink}:{gameLink:string}) {
const [loadGame, setLoadGame] = useState(false);
const [muted, setMute] = useState(false);
const {unityProvider,unload,isLoaded,requestFullscreen,sendMessage} = useUnityContext({
dataUrl: `/game_builds/${gameLink}/game.data`,
frameworkUrl: `/game_builds/${gameLink}/game.framework.js`,
codeUrl: `/game_builds/${gameLink}/game.wasm`,
loaderUrl: `/game_builds/${gameLink}/game.loader.js`
});
const handleMute = () => {
if(muted) {
sendMessage('AudioManager','Webgl_Unmute');
setMute(false);
} else {
sendMessage('AudioManager','Webgl_Mute');
setMute(true);
}
};
useEffect(()=>{
const stopGame = async () => {
await unload();
};
return(()=>{
if(isLoaded){
stopGame();
}
});
}, [isLoaded]);
return (
<div className='rounded-sm p-[6px] bg-black w-[1024px] h-[574px] flex'>
{(loadGame) ?
<div className='relative'>
<div className='absolute flex flex-col top-0 right-[-98px]'>
<button key='fullscreen' className='btn btn-info mb-4 rounded-sm' onClick={(e)=>{requestFullscreen(true)}}>
<AiOutlineFullscreen size={32}/>
</button>
<button key='mute' className='btn btn-info rounded-sm' onClick={handleMute}>
{muted?<IoVolumeHighOutline size={32}/>:<IoVolumeMuteOutline size={32}/>}
</button>
</div>
<Unity unityProvider={unityProvider} style={{width: 1000, height: 562}} />
</div>
:
<div className='flex flex-col w-full items-center'>
<button className='btn btn-outline btn-info m-auto rounded-sm' onClick={(e)=>{setLoadGame(true)}}>Load game</button>
</div>
}
</div>
);
Battling Unity console.logs
Last thing I want to talk about is the crazy amount of console logs Unity Webgl outputs, most of these logs are not necessary at all and mainly talk about progress and states of the Webgl player. If you boot up a game, start interacting with it and then check the console in your browser - it will be loaded up with a lot of logs. There didn't really seem to be a good way to go about turning these off. There were a few 'solutions' I tried, but they either didn't work or there was a forum post saying to add this line console.log = function(){}
which we're going to not do. There are actually some settings in player settings over in the Unity editor, but they don't actually do anything for these console logs. If you open up the game.framework.js
and game.loader.js
files that the Unity export generates, you'll notice that the console logs have no if-statements around them to turn them off.
Unfortunately, the only way to deal with these console logs is to open up game.framework.js
and game.loader.js
and manually remove them yourself. This is a little advanced, but even if you mess something up you can generate the files again or revert back. This step is definitely optional, but I think it looks bad to have them in the console in production.
However, some of the warnings we'll see have to do with the game name, version and company for your game not being set. We can handle that easily.
I have a type file at app/types/global.d.ts
that I'm going to add a new type to.
export interface UnityContextData {
gameLink:string,
gameName:string,
gameVerion:string,
gameCompany:string
}
We can then import that type to our Unity component and update the UnityContext
with the new options.
const {unityProvider,unload,isLoaded,requestFullscreen,sendMessage } = useUnityContext({
dataUrl: `/game_builds/${gameLink}/game.data`,
frameworkUrl: `/game_builds/${gameLink}/game.framework.js`,
codeUrl: `/game_builds/${gameLink}/game.wasm`,
loaderUrl: `/game_builds/${gameLink}/game.loader.js`,
companyName: gameCompany,
productName: gameName,
productVersion: gameVerion
});
Now that we have the companyName
, productName
and productVersion
set we won't see the warnings for these in the console anymore.
Here is the updated code so far:
'use client';
import { useEffect, useState } from 'react';
import { Unity, useUnityContext } from 'react-unity-webgl';
import { UnityContextData } from '../types/global';
import { AiOutlineFullscreen } from 'react-icons/ai';
import { IoVolumeHighOutline, IoVolumeMuteOutline } from 'react-icons/io5';
export default function UnityGameLoader({gameLink,gameName,gameVerion,gameCompany}:UnityContextData) {
const [loadGame, setLoadGame] = useState(false);
const [muted, setMute] = useState(false);
const {unityProvider,unload,isLoaded,requestFullscreen,sendMessage } = useUnityContext({
dataUrl: `/game_builds/${gameLink}/game.data`,
frameworkUrl: `/game_builds/${gameLink}/game.framework.js`,
codeUrl: `/game_builds/${gameLink}/game.wasm`,
loaderUrl: `/game_builds/${gameLink}/game.loader.js`,
companyName: gameCompany,
productName: gameName,
productVersion: gameVerion
});
const handleMute = () => {
if(muted) {
sendMessage('AudioManager','Webgl_Unmute');
setMute(false);
} else {
sendMessage('AudioManager','Webgl_Mute');
setMute(true);
}
};
useEffect(()=>{
const stopGame = async () => {
await unload();
};
return(()=>{
if(isLoaded){
stopGame();
}
});
}, [isLoaded]);
return (
<div className='rounded-sm p-[6px] bg-black w-[1024px] h-[574px] flex'>
{(loadGame) ?
<div className='relative'>
<div className='absolute flex flex-col top-0 right-[-98px]'>
<button key='fullscreen' className='btn btn-info mb-4 rounded-sm' onClick={(e)=>{requestFullscreen(true)}}>
<AiOutlineFullscreen size={32}/>
</button>
<button key='mute' className='btn btn-info rounded-sm' onClick={handleMute}>
{muted?<IoVolumeHighOutline size={32}/>:<IoVolumeMuteOutline size={32}/>}
</button>
</div>
<Unity unityProvider={unityProvider} style={{width: 1000, height: 562}} />
</div>
:
<div className='flex flex-col w-full items-center'>
<button className='btn btn-outline btn-info m-auto rounded-sm' onClick={(e)=>{setLoadGame(true)}}>Load game</button>
</div>
}
</div>
);
And that is it, hopefully this article helped you implement Unity in your Next.js or React project.