Creating a Unity Discord Activity

Creating a Unity Discord Activity

Recently I was trying to set up a simple Discord activity that houses a Unity Game. The docs and tutorials I found were pretty sparse and often a bit outdated. After a bit of hair pulling, I was able to get things running. I wanted to share a very simple example in case anyone else struggled getting it going. Let's walk through how to do it.

On the surface, the architecture is pretty straightforward. You have a Unity WebGL build that is made available to the web. Discord then proxies that web URL into a sandboxed container in the app. Using the Discord API you can communicate between the web app, Unity build, and the Discord client.

There are three parts to setting up a basic Discord Unity activity.

  1. Building a small web application to host the activity.
  2. Setting up the Unity WebGL build
  3. Creating and configuring the activity in Discord

Setting up the Web Application

This tutorial assumes you have Node and NPM installed on your system. Let's start by scaffolding a web application using Vite.

When prompted, give it a name for your project. For simplicity, we'll choose the Vanilla template and basic Javascript.

npm create vite@latest
Shell Output from Vite Create Wizard

Go ahead and drop into the project directory and run npm install. While we are doing that, let's go ahead and install the Discord SDK tools, as we'll need them later.

cd unity-discord-example
npm install

npm install @discord/embedded-app-sdk

To test the activity from your local development environment, you will need a way to expose the app publicly. Discord recommends using the Cloudflared Tunnel Client. Follow the instructions at https://github.com/cloudflare/cloudflared to install the client on your development environment.

To make things easier, lets also add a NPM command to our config to automatically create the tunnel for us. In your package.json file, update the scripts block to add a tunnel command.

{
  ...,
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "tunnel": "cloudflared tunnel --url http://localhost:5173/"
  },
  ...
}

Setting & Configuring the Discord Activity

Log into Discord in a web browser. If you haven't already, go into your user settings and enable developer mode.

User Settings -> Advanced -> Developer Mode (toggle on)

Discord Advanced Settings, Developer Mode Toggle

Next visit the discord developer portal at https://discord.com/developers/applications. You should see a button in the top right labeled "New Application". Click on that. Give your app a name and agree to the terms. Click create, solve the captcha, and you should then have a shiny new Discord app.

Discord App Creation Dialog

There is a lot of information, and settings inside the app configuration screens. For now, all that we need is the application ID that you'll find on the General Information screen. Lets go ahead and copy that.

Application ID Display from the Discord App Configuration

We can now add this to our project. Lets do that via an environment variable. Create a file in the root of your web project called .env and add the follow, substituting your application key.

VITE_DISCORD_CLIENT_ID=1376633238646292520

Creating the Unity Build

This tutorial isn't going to go deep into any of the Unity Project setup. I'm going to assume you already have a Unity based game or app that can be exported as a WebGL build. For the purposes of this demo, I'm going to create a new project using the 2D Platformer Microgame learning templates.

Unity Hub Project Creation Window

Once the project loads, open the Build Profiles settings under File -> Build Profiles. Select Web from the platforms on the left and click the "Switch Platform" button at the top right. It will take a few minutes to update and recompile scripts.

(If you don't see web as an available platform, make sure your Unity install has the Web Build Support module installed.)

Unity Build Profiles Modal

Once the platform switch has completed, click the Build button. It will prompt you to select a location for the project. It doesn't really matter where you put this as we'll be copying files from it into our project. To keep things simple, I'm going to create a folder called "build" in the root of our web project and unity save the build output there. When that's done you should see something like the following:

Unity WebGL Template Output

Updating the Web App to Serve the Unity Build

Now that we have a the Unity WebGL build ready, lets configure the web app to serve it.

Starting with the html, replace the contents of index.html with the following.

<!DOCTYPE html>
<html lang="en-us">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Unity Discord Activity</title>
  </head>
  <body style="text-align: center; padding: 0; border: 0; margin: 0;">
    <div id="unity-container" class="unity-desktop">
      <canvas id="unity-canvas" width=960 height=600 scrolling="no" tabindex="-1"></canvas>
      <div id="unity-loading-bar">
        <div id="unity-logo"></div>
        <div id="unity-progress-bar-empty">
          <div id="unity-progress-bar-full"></div>
        </div>
      </div>
      <div id="unity-warning"> </div>
    </div>
    <script type="module" src="src/main.js"></script>
  </body>
</html>

This is a modified version of the index.html you'll find in your build folder. I've removed the mobile container, and we'll move all the Javascript into our main.js file. You can see that is included via script tag towards the bottom of the document. We'll also move the styles to a stylesheet to keep things clean.

From the location where you saved the build output from Unity, locate a folder named Build. It should contain four files that follow the pattern, where build_name is what you saved your Unity output as.

  • build_name.data.gz
  • build_name.framework.js.gz
  • build_name.loader.js
  • build_name.wasm.gz

Copy or move this entire folder into the public folder in the root of the web application.

From the location where you saved the build output from Unity, locate the folder named TemplateData. Copy or move that folder into the root of the web application.

It the web application open src/styles.css, remove all the contents, and replace it with the following.

body { padding: 0; margin: 0 }
#unity-container { position: absolute }
#unity-container.unity-desktop { height: 100vh; width: 100vw; overflow: hidden; }
#unity-canvas { background: #231F20;  height: 100vh !important; width: 100vw!important; }
#unity-loading-bar { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); display: none }
#unity-logo { width: 154px; height: 130px; background: url('../TemplateData/unity-logo-dark.png') no-repeat center }
#unity-progress-bar-empty { width: 141px; height: 18px; margin-top: 10px; margin-left: 6.5px; background: url('../TemplateData/progress-bar-empty-dark.png') no-repeat center }
#unity-progress-bar-full { width: 0%; height: 18px; margin-top: 10px; background: url('../TemplateData/progress-bar-full-dark.png') no-repeat center }
#unity-footer { position: relative }
.unity-mobile #unity-footer { display: none }
#unity-logo-title-footer { float:left; width: 102px; height: 38px; background: url('../TemplateData/unity-logo-title-footer.png') no-repeat center }
#unity-build-title { float: right; margin-right: 10px; line-height: 38px; font-family: arial; font-size: 18px }
#unity-fullscreen-button { cursor:pointer; float: right; width: 38px; height: 38px; background: url('../TemplateData/fullscreen-button.png') no-repeat center }
#unity-warning { position: absolute; left: 50%; top: 5%; transform: translate(-50%); background: white; padding: 10px; display: none }

What we've done here is copy the styles from the unity web build template with a few changes.

  1. All of the image URLs have been updated to reference our TemplateData folder.
  2. We've updated the styles on the container and canvas elements to force the unity app to take up the entire screen.

Now locate our main script file in src/main.js. Go ahead and remove everything except the line that imports the CSS file. Feel free to also delete counter.js and javscript.svg from the src directory. We will not need those.

Add the following to your main.js file after the css import line.

var canvas = document.querySelector("#unity-canvas");

function unityShowBanner(msg, type) {
  var warningBanner = document.querySelector("#unity-warning");
  function updateBannerVisibility() {
    warningBanner.style.display = warningBanner.children.length ? 'block' : 'none';
  }
  var div = document.createElement('div');
  div.innerHTML = msg;
  warningBanner.appendChild(div);
  if (type == 'error') div.style = 'background: red; padding: 10px;';
  else {
    if (type == 'warning') div.style = 'background: yellow; padding: 10px;';
    setTimeout(function() {
      warningBanner.removeChild(div);
      updateBannerVisibility();
    }, 5000);
  }
  updateBannerVisibility();
}

var buildUrl = "Build";
var loaderUrl = buildUrl + "/build.loader.js";
var config = {
  arguments: [],
  dataUrl: buildUrl + "/build.data.gz",
  frameworkUrl: buildUrl + "/build.framework.js.gz",
  codeUrl: buildUrl + "/build.wasm.gz",
  streamingAssetsUrl: "StreamingAssets",
  companyName: "DefaultCompany",
  productName: "Sample_Platformer",
  productVersion: "5.0.2",
  showBanner: unityShowBanner,
};

document.querySelector("#unity-loading-bar").style.display = "block";

var script = document.createElement("script");
script.src = loaderUrl;
script.onload = () => {
  createUnityInstance(canvas, config, (progress) => {
    document.querySelector("#unity-progress-bar-full").style.width = 100 * progress + "%";
        }).then((unityInstance) => {
          document.querySelector("#unity-loading-bar").style.display = "none";
        }).catch((message) => {
          alert(message);
        });
      };

document.body.appendChild(script);

This code again, comes from the Unity build template. If you look at the index.html file in the Unity build directory, you'll find this same code there. Once again, I've removed logic for mobile handling.

Now if you start the development server with npm run dev you should be able to play the game in the browser at http://localhost:5173/

Game Running in browser on Localhost

Awesome! We've got a web app that serves up our game. We're almost ready to play it inside Discord!

Enabeling the app in Discord and Testing

Next lets create a tunnel to expose this app on a public URL that Discord can proxy for us. Open up another terminal, making sure to leave the dev server running, and run that tunnel command we created earlier. npm run tunnel.

You'll see a lot of output and debug info. The important bit we are looking for has a ASCII box surrounding it. It will tell you what the public URL for your tunnel is.

Cloudflare Tunnel Output

Let's go ahead and copy that URL and try it out in the browser. You will probably get a message saying that the request has been blocked and that we need to update the config. Back in the root of the web project create a file called vite.config.js and add the following. Make sure to substitute your Cloudflare URL.

import { defineConfig } from 'vite'

export default defineConfig({
    server: {
        allowedHosts: ["tools-carl-exclusively-thank.trycloudflare.com"]
    }
})

You will need to restart the Vite dev server. In that terminal, use control + C to kill that process and simply run npm run dev again. Now if you visit your tunnel URL, the game should load just as it did on the localhost URL.

Before we can view our app in Discord, we'll need to tell it where it can find our app. Back in the Discord development panel, inside our activity's config, look towards the bottom of the left hand menu for a menu item that says URL Mappings. Click that and on the screen that follows, paste in your tunnel URL under root mapping. Make sure to remember to save the changes.

Discord Activity URL Mapping Settings

Next go to the Settings tab, under Activities header in that lefthand menu. There you'll see an option to Enable Activities. Lets toggle that on.

Discord Activity Settings

Now lets head to Discord to test things out. Open the Discord client in a browser window. Make sure to refresh the tab if you already had it open. Enter a voice channel and click the Choose Activity button.

Important Note: Until an activity has gone through a verification process, it can't be installed in servers with more than 25 users. You can make a private personal server for development if needed.

You should now see your activity listed at the top of the shelf.

Ok, we got the loading screen, but it doesn't load. Lets check the console in the developer tools to see what's going on.

Content Security Policy Error Messages

The Discord sandbox has very strict content security policies. What we see happening here is your web app code is trying to load the Unity code and asset bundles and those files are being blocked by these security policies. Though it's not readily apparent what you need to do here, it is fortunately very easy to fix.

Go ahead and exit the activity in discord.

Head back to our main.js in our web app, and lets make one little change. In the line where you specify the build URL, change it as follows:

// This URL won't work
// var buildUrl = "Build";

// We need to tell it to use the proxy path
var buildUrl = "/.proxy/Build";

Now head back to Discord and renter your activity. And huzzah! There's your game running right in Discord!

Unity Game running as a Discord Activity

Congratulations! You've set up your first Unity project to run as a Discord Activity.

Enabling the Discord Embedded SDK

Discord is a social platform, and chances are you are going to want to use some of those features. Lets go ahead and add some very basic Discord SDK integration into our project. Exit the activity and head back to our main.js in our web app.

At the top of the script, just after the style import, add the following lines.

import { DiscordSDK } from "@discord/embedded-app-sdk";

// This gets our activity ID from the .env file we created earlier.
const discordSdk = new DiscordSDK(import.meta.env.VITE_DISCORD_CLIENT_ID);

After the unityShowBanner function, add the following function.

async function setupDiscordSdk(){

  await discordSdk.ready();

  // Authorize with Discord Client
  const { code } = await discordSdk.commands.authorize({
    client_id: import.meta.env.VITE_DISCORD_CLIENT_ID,
    response_type: "code",
    state: "",
    prompt: "none",
    scope: [
      "identify",
      "guilds",
      "guilds.members.read"
    ],
  });

   // Retrieve an access_token from your application's server
  const response = await fetch('/.proxy/api/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      code,
    }),
  });
  const {access_token} = await response.json();

  // Authenticate with Discord client (using the access_token)
  auth = await discordSdk.commands.authenticate({
    access_token,
  });
}

This function simply prompts the Discord user for permission to access their identity. You could then, for instance, use this information to identify them in a multiplayer experience.

Finally, in the script.onLoad block, we'll need to call the discord setup function.

script.onload = () => {
  createUnityInstance(canvas, config, (progress) => {
    document.querySelector("#unity-progress-bar-full").style.width = 100 * progress + "%";
        }).then(async (unityInstance) => { // Add the async modifier here
          document.querySelector("#unity-loading-bar").style.display = "none";

          // Setup Discord SDK Connection
          await setupDiscordSdk();
        }).catch((message) => {
          alert(message);
        });
      };

If you head back to discord and reenter your activity, you will now see a modal asking for permission to share your information with the activity.

Discord Activity Modal Prompting for User Permissions

The user data can then be used to personalize the in-game experience, set up multiplayer, etc. Those topics are out of the scope of this tutorial.

From Development to Production

This tutorial has taken you through the steps to set up the activity for local development. To move to production, you'll need to host your webapp live on a public URL. Make sure to update the root mapping URL in the activity settings to reflect it's permanent location.

You might also take this step during the development stage of your app. For instance a deploy pipeline or some other such mechanism.

You will also likely need to get the app verified before it can be used publicly across Discord. See the App Verification tab in the Discord development dashboard.

App Verification Requirements

Next Steps

You can find all the code from this project on my Github here: https://github.com/supertorio/Unity-Discord-Example

Like most things in the tech world, there are many different ways to accomplish something like this. If you prefer to build in React or Vue, there are libraries for working with unity web builds.

You can also communicate with the Discord API directly from within unity.

There are also third party tools that are designed to make the process of setting up Discord Activities even easier.

Go forth and create friends. And if this was helpful, or you make something really cool, please let me know!!