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.

An image showing the player settings and how to disable compression on a Webgl build.

I also like to change the background color to black on the Unity log screen. This is completely personal preference though.

An image showing the player settings and how to update the background color on the Unity logo screen.

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.

  1. Audio mixer
  2. Audio source
  3. Exposing parameters for Audio Channels

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 productVersionset 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.