Web UIs (ReUi)¶
A bundle can ship a ReUi, which is web content that clients will host in sandboxed
iframe's. ReUi is a full web environment that behaves like a regular web host.
HTML, CSS and JavaScript, built with whatever tooling you like (the reference bundles use
a Svelte single-page app). It renders above the game and communicates with
your client-side component over messages.
The UI is one of the optional parts of a bundle. The rest of this page assumes you have set up a bundle project as described in Project Layout and have a client-side component.
Serving the UI¶
The UI must be a set of static files. The game does not run a Node server
or execute a framework at runtime. You build your app, put the output in a
folder of the bundle (conventionally ui/), and list every file in the
manifest so the files are delivered to clients.
[ReUi]
initial_page = "index.html"
content_root = "ui/"
files = [
"ui/index.html",
"ui/_app/**",
"ui/fonts/**",
"ui/sw.js",
]
filesdecides what clients download. List every asset the page uses at runtime, including framework build output. Globs (*,**) are supportedinitial_pageis the page loaded in the frame. With a framework that emits absolute URLs (for example/_app/...), setcontent_rootto the folder the build output lives in (ui/above). The page is then served as if that folder were the origin root, soindex.htmland/-based asset paths resolve. Pointinitial_pageat an absolutehttps://URL instead to host the page yourself
See Bundle Config for the full option reference.
Talking to your client-side code¶
The UI and your client component live in the same client realm and communicate over messages. Messages are identified by name and carry a JSON payload.
Client code to the page¶
Send from your component with re.Ui.SendMessage. The payload reaches the
page as a window message event whose data is the JSON object you sent.
The convention the reference UI uses is an object with a messageName and
a payload field, built from a generic record:
public partial record UiMessage<T>(string messageName, T payload);
// Sending from client-side code
re.Ui.SendMessage(
new UiMessage<ChatHudPayload>(
"chat-hud-update",
new ChatHudPayload(author, contents)
)
);
On the page, subscribe and dispatch on the name:
window.addEventListener("message", (event) => {
const { messageName, payload } = JSON.parse(event.data);
const handler = handlers.get(messageName);
if (handler) {
handler(payload);
}
});
Warning
For security reasons, messages can only be sent to your own iframe
If you are designing a complex server framework with a centralized UI, write a dispatcher in the same bundle that hosts said UI
Page to client code¶
The page sends a message by POSTing JSON to the message endpoint, with the message name in the URL:
await fetch(`https://ugc-message/${messageName}`, {
method: "POST",
headers: { "Content-Type": "application/json; charset=UTF-8" },
body: JSON.stringify(payload),
});
Your client code subscribes with re.Ui.OnMessage, deserializing the JSON
payload into a typed record:
re.Ui.OnMessage(
"chat-message-sent",
(ChatMessagePayload data) =>
{
// data carries the JSON object the page posted
}
);
One handler per message name per client realm. Pick names that cannot collide with other bundles (a bundle-specific prefix helps).
Input¶
You can control how input is handled for your bundle. By default the game consumes keyboard and mouse input. For a chat window or a menu, capture input, prevent it from passing to the game and show the cursor:
and release it again when the UI closes (accept_input: false,
exclusive_input: false, cursor_active: false). A good way to handle this is
by connecting the input blocking to a hotkey (see Script API).
Example: Chat round trip¶
A chat message crosses every hop of the messaging stack. Opening the window is a keybind on the client:
re.Con.RegisterCommandWithKeyBind(
"open-chat-window",
(string[] args) =>
{
re.Ui.SendMessage(new UiMessage<ChatHudPayload>("chat-hud-update", new ChatHudPayload(open: true)));
re.Ui.SetInputPolicy(accept_input: true, exclusive_input: true, cursor_active: true);
},
EInputKey.IK_Enter,
"Open Chat Window"
);
When the player submits, the page posts to https://ugc-message/chat-message-sent.
Your client code forwards it to the server realm:
re.Ui.OnMessage(
"chat-message-sent",
(ChatMessagePayload data) =>
{
re.Events.EmitNet("chat-message-sent", data);
}
);
The server realm validates and broadcasts it back to every client (see
The Networking Model on why the server checks before
forwarding), and each client's OnNet handler pushes it into the page:
re.Events.OnNet(
"chat-broadcast",
(ChatMessagePayload data) =>
{
re.Ui.SendMessage(new UiMessage<ChatHudPayload>("chat-hud-update", new ChatHudPayload(data.author, data.contents)));
}
);
The page receives chat-hud-update and appends the line.
Developing the UI¶
The UI is ordinary web content. Build it with your framework's dev server, then move the production build into the bundle and list the files. Message traffic only exists inside the game client, so test the full loop in-game. Page-only work (layout, state) can be iterated in a normal browser.
Note
Because communication uses window messages and message endpoints, you can stub these out with development placeholders to develop your UI outside of the game
Design a development mode that lets you iterate in a standard browser for most testing