Unreal Engine 5: Full-Stack Dev with JavaScript and WebUI Plugin

Learn how to get Unreal Engine 5 and your full-stack application to communicate

Tricia Hughes
Better Programming

--

Image generated using MidJourney

As I entered into the last phase of my coding boot camp and was preparing to embark on the trials of creating a full-stack capstone project on my own, I was beginning to feel like this was the time that I could take my project to the next level; do something that is technically the same but also quite different from what I’ve made during my time in the boot camp. I have a deep love for video games, and I’ve wanted to work in the industry for many years now, so it felt like a natural transition to interweave full-stack web development with a game engine.

This project is still a work in progress, of course. However, feel free to check it out on GitHub.

In this article, I will show you how I got Unreal Engine 5 and my full-stack application to communicate utilizing a plugin called WebUI by Tracer Interactive.

Note: You need an Epic account connected to your GitHub to view the repository.

In Unreal Engine…

You’ll want to start by creating a widget. Specifically, a widget blueprint can be found in the “User Interface” section when right-clicking in the “Content Browser.”

User Interface / Widget Blueprint

Widget blueprints are where you can design and create logic for interfaces rendered in the game’s viewport. Next, you’ll want to search for a canvas panel in the widget palette. The widget palette is where you can find common UI elements such as buttons, images, text, inputs, etc.

A canvas panel is like a blank slate that will always scale to whatever parent it’s inside of and give children widgets arbitrary coordinates. This is important in this case because it allows any objects you add as a child to the panel to have arbitrary coordinates.

Palette & Canvas Panel Screenshot

Since we’re using the WebUI plugin, we’ll add a WebInterface widget as a child to the canvas panel. This is the Chromium web browser that we’ll be using inside Unreal to run the web application.

Search Web Interface in Palette

By default, the size of it is quite small in the canvas panel, so we’ll have to adjust it by setting the anchors to ensure that it always fills the viewport. To do so, head over to the anchor of the widget, click the full-screen icon, and adjust the offsets to zero. This forces the widget to have zero padding on each display corner.

Screenshot of Anchors and Offsets

Next, let’s set the initial URL. This is the destination that the web browser first loads. In this case, it’s set to a local server that I’m using to develop my web application.

Head over to the content browser in the engine, where we’ll create a level for your widget to live in. Once you open your level, you’ll want to access its blueprint so that you can start scripting your classes.

Levels in UE5
Accessing level blueprints in UE5

Now, we’ll want to display the widget we made when we click the “Play” button above the viewport, and the world begins simulating. On “Event BeginPlay,” we’ll want to create a new node called “CreateWidget” and point its class to the widget class we just created. That way, we can turn it into an object to access its variables and call functions from it.

Create widget on Event BeginPlay

The return value is the constructed widget object itself. So now we can call built-in functions on our widget.

Return value of Create widget

The next function we want to call is the AddToViewport function, which does exactly as the name suggests. Once the object is added to the viewport, the importance of the setup of our widget layout matters the most (the anchors and corresponding offsets).

Event BeginPlay -> Create Widget -> Add to Viewport Function

Now, let's compile and head back to the viewport and click Play!

Compile and Play buttons
Web application running in the viewport

Note: When you’re running the web application in Unreal, you can access the Developer tools by first clicking into the viewport and then pressing Cmd + Shift + I.

Developer Tools for the web app in the viewport

WebUI plugin

The WebUI plugin has built-in interface functions allowing the browser to broadcast events inside Unreal. When these events are broadcasted, it passes the Name of the event and the JSON Data associated with it.

On Interface Event Button and Node

With the Name passed of the event from the web app, you can execute events/functions in Unreal based on that name. We achieve this with the SwitchOnName node.

Remember that the data being passed from the web app will accompany the name, allowing you to pass said data into any functions in Unreal that you may want to call alongside that name.

With all of this being said, it is required that we implement specific JavaScript code into our web app to ensure that the plugin can communicate with our JavaScript and the Unreal Engine application.

In the web application

According to the documentation provided by the WebUI plugin developer, you need to implement a JavaScript Unreal helper function in your index.html file. For readability and accessibility, I broke this part into three JavaScript files and then imported those files into my index.html file via script source tags.

File Structure

Note: The code that was provided was bare-bones. You may also notice some differences in the code blocks I provide versus what the plugin provides in their sample code; these are on purpose. After many hours of troubleshooting, the following code allowed the plugin to work for me successfully. The simplicity of the sample code didn’t quite work with my React components or code reusability standards.

Also, initializing the interface may be helpful if it’s separated from defining the helper function (found below).

In the unrealHelper.js file, the function’s purpose is to generate a UUID for an event broadcast interface which the plugin uses to send and receive events to and from Unreal.

// The unrealHelper.js code exports three items: a global variable 'ue', a function 'uuidv4' that generates a UUID v4 string, and a function 'ue5' that broadcasts messages using either ue.interface.broadcast or the browser's history state depending on availability, while also accepting an optional callback function. The callback function is stored in the ue.interface object using a UUID as the key and is removed after a specified timeout.:

// ue: A global variable that is initialized as an empty object if it's not already an object.
// uuidv4: A function that generates a UUID v4 string.
// ue5: A function that broadcasts messages using either the ue.interface.broadcast function or the browser's history state, depending on the availability of ue.interface.broadcast.
// The ue5 function also accepts an optional callback function as an argument, which is stored in the ue.interface object using a UUID as the key. The stored function is removed after a specified timeout.

// Export the global variable `ue`. If it's not an object, initialize it as an empty object.
export let ue;
if (typeof ue !== "object") {
ue = {};
}

// Export the `uuidv4` function that generates a unique identifier (UUID v4) as a string.
export const uuidv4 = function () {
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, function (t) {
return (
t ^
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (t / 4)))
).toString(16);
});
};

// Export the `ue5` function that allows broadcasting messages using either the `ue.interface.broadcast`
// function or the browser's history state depending on the availability of the `ue.interface.broadcast` function.
export const ue5 = (function (r) {
// Check if `ue.interface.broadcast` is not available, and initialize `ue.interface` as an empty object.
if (
"object" != typeof ue.interface ||
"function" != typeof ue.interface.broadcast
) {
ue.interface = {};
return function (t, e, n, o) {
var u, i;
if ("string" == typeof t) {
// If the second argument is a function, shift the arguments.
if ("function" == typeof e) {
o = n;
n = e;
e = null;
}
// Create an array with the first argument, an empty string, and the return value of the function `r` called with `n` and `o`.
u = [t, "", r(n, o)];
if (void 0 !== e) {
u[1] = e;
}
// Encode the array as a JSON string and store it in the browser's history state.
i = encodeURIComponent(JSON.stringify(u));
if (
"object" == typeof window.history &&
"function" == typeof window.history.pushState
) {
window.history.pushState({}, "", "#" + i);
window.history.pushState({}, "", "#" + encodeURIComponent("[]"));
} else {
document.location.hash = i;
document.location.hash = encodeURIComponent("[]");
}
}
};
} else {
// If `ue.interface.broadcast` is available, store the existing `ue.interface` and initialize a new empty object.
const i = ue.interface;
ue.interface = {};
return function (t, e, n, o) {
var u;
if ("string" == typeof t) {
// If the second argument is a function, shift the arguments.
if ("function" == typeof e) {
o = n;
n = e;
e = null;
}
u = r(n, o);
// Broadcast the message using the `ue.interface.broadcast` function.
if (void 0 !== e) {
i.broadcast(t, JSON.stringify(e), u);
} else {
i.broadcast(t, "", u);
}
}
};
}
})(function (t, e) {
// If the first argument is not a function, return an empty string.
if ("function" != typeof t) return "";
// Generate a UUID and store the function `t` in `ue.interface` using the UUID as a key.
var n = uuidv4();
ue.interface[n] = t;
// After a certain timeout, delete the stored function from `ue.interface`.
setTimeout(function () {
delete ue.interface[n];
}, 1e3 * Math.max(1, parseInt(e) || 0));
return n;
});

The unrealInterface.js file initializes the ue.interface as an empty object and defines a broadcast method used to communicate with Unreal.

// This code initializes ue.interface as an empty object and defines a broadcast method for it if certain conditions are met. It then assigns the broadcast method to the ue5 variable if ue.interface.broadcast is defined.

// Check if the global variable `ue` is not an object, or if `ue.interface` is not an object.
if (typeof ue != "object" || typeof ue.interface != "object") {
// Define an anonymous function (IIFE) that takes an `obj` parameter and executes it immediately with `ue.interface` as the argument.
(function (obj) {
// If `obj` exists and has a `broadcast` method, initialize `ue.interface` as an empty object.
if (obj && obj.broadcast) {
ue.interface = {};

// Define the `broadcast` method for `ue.interface`.
ue.interface.broadcast = function (name, data) {
// If the `name` argument is not a string, return immediately.
if (typeof name != "string") return;
// If the `data` argument is not undefined, call `obj.broadcast` with the `name` and the JSON stringified `data` arguments.
if (typeof data != "undefined") {
obj.broadcast(name, JSON.stringify(data));
} else {
// If the `data` argument is undefined, log an error and call `obj.broadcast` with the `name` argument and an empty string.
console.log("Incoming Error");
obj.broadcast(name, "");
}
};
}
})(ue.interface);
}

// If `ue` and `ue.interface` exist, and `ue.interface.broadcast` is defined,
// assign `ue.interface.broadcast` to `ue5`.
if (ue && ue.interface && ue.interface.broadcast) {
ue5 = ue.interface.broadcast;
}

The unrealFunctionLibrary.js file is where the functions live that I want to use to communicate with Unreal. We import the ue5 function from the unrealHelper.js file, wrapping the ue5 function call with a separate function definition that can easily be called from any other component that imports the function library file. This should act as a foundation for future reusability should I add more functions that need to be called from various components.

//Imports the ue5 function from the unrealHelper.js file
import { ue5, ue } from "./unrealHelper";

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//Trigger Events *in* Unreal Engine with optional JSON data
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

export const AccountInitialized = (data) => {
ue5("AccountInitialized", data);
};

In the index.html file, I’m defining custom methods that will be called from Unreal and assigning custom events to them to be dispatched, allowing other parts of the application to listen for and react to the custom event.

<!DOCTYPE html>
<html lang="en">
<head>
<script src="../src/unreal/unrealHelper.js"></script>
<script src="../src/unreal/unrealInterface.js"></script>
<script src="../src/unreal/unrealFunctionLibrary.js"></script>
<!--




-->
<script>
// Define a method named `accountFinalized` on `ue.interface` object.
ue.interface.accountFinalized = function (data) {
// Create a new CustomEvent called "accountFinalized" with the provided `data` as the event detail.
const accountFinalizedEvent = new CustomEvent("accountFinalized", {
detail: data,
});

// Dispatch the custom "accountFinalized" event on the `document` object.
document.dispatchEvent(accountFinalizedEvent);
};
</script>
<!--

A Simple Example

If we have a button in Unreal, we can add an onClicked event on the button. When clicked, it calls a JavaScript function of our choosing via function name, referencing the WebUI plugin's web interface widget.

A more applicable use case

For my project, I wanted to create a self-expression social platform combining customizable 3D avatars with anonymous discussions. The user story is that a user will signup for an account, customize their avatar for their profile and finalize their account, which will then route them to the home page.

The web app and developer tools open in Unreal
Avatar creation page and Developer Tools

In my web application, I have a few console.logs() to display the user data being passed from the web app to the engine in the developer tools output log. Once the user is finished customizing, they will click the “Finalize Avatar” button, which will route them to the web app's home page by resetting the browser widget's visibility in Unreal.

Below is the React component that I have routed for the avatar creation scene in Unreal. I’m invoking the AccountInitalized function, which is passing an object with a key of Name and an interpolated string of the username captured during signup and is now being broadcast to a widget in Unreal via the WebUI WebInterface widget. This will then dynamically show the new user’s username when creating their avatar.

import React, { useEffect } from "react";
import { useHistory } from "react-router-dom";
import { AccountInitialized } from "../unreal/unrealFunctionLibrary";

function CreateAvatar({ fetchUser, user, userId }) {
const history = useHistory();
console.log(user);
console.log(userId);

AccountInitialized({ name: `${user}` });

useEffect(() => {
// Define the event listener
const handleAccountFinalized = (event) => {
console.log("Received data from index.html:", event.detail);
fetchUser();
history.push("/home");
};

document.addEventListener("accountFinalized", handleAccountFinalized);

return () => {
document.removeEventListener("accountFinalized", handleAccountFinalized);
};
}, []);

return <></>;
}

export default CreateAvatar;
Home page and custom avatar JSON data

That’s it. That’s the basics of the process! This project is still in development, but I’d like to continue writing articles about some of the other processes and code I wrote to manipulate some of the data communicated between Unreal and my web application.

Until next time!

— Tricia

--

--

Full-stack Software Engineer in Progress | Thirst for Knowledge & Efficiency | Love for Design & Video Games