Skip to content

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",
]
  • files decides what clients download. List every asset the page uses at runtime, including framework build output. Globs (*, **) are supported
  • initial_page is the page loaded in the frame. With a framework that emits absolute URLs (for example /_app/...), set content_root to the folder the build output lives in (ui/ above). The page is then served as if that folder were the origin root, so index.html and /-based asset paths resolve. Point initial_page at an absolute https:// 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:

re.Ui.SetInputPolicy(accept_input: true, exclusive_input: true, cursor_active: true);

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