Use the EngawaUI Event component¶
The EngawaUI Event component adds named, two-way events on top of EngawaUI's raw JSON bridge. It is the most convenient choice for Blueprint-driven menus, HUD actions, settings, and other messages that have a clear name.
Instead of receiving one raw message and inspecting its type field, Blueprint
receives an Event Name and its Json Payload separately.
sequenceDiagram
participant Page as JavaScript page
participant Facade as EngawaUIEvents facade
participant Runtime as EngawaUI asynchronous transport
participant BP as EngawaUI Event / Blueprint
Page->>Facade: emit("menu.start.clicked", payload)
Facade->>Runtime: versioned JSON event envelope
Runtime-->>BP: On Event Received(name, JSON payload)
BP->>Runtime: Emit Event("game.state", JSON payload)
Runtime-->>Facade: engawa-message DOM event
Facade-->>Page: listener(payload, name)
Named events use JSON-compatible payloads and run asynchronously. They are messages, not blocking function calls or RPCs.
When to use this component¶
Use EngawaUI Event when:
- Blueprint should route by a readable name such as
menu.opened; - both JavaScript and Unreal need to initiate messages;
- you want an allowlist of page events;
- you want validation and queue failures through On Event Error; or
- simple strings are sufficient and registered Gameplay Tags are unnecessary.
Use EngawaUI Gameplay Tag Event when the rest of your game already routes actions with registered Gameplay Tags. Use the raw JavaScript bridge when you are implementing your own message protocol.
1. Add the JavaScript event facade¶
The event component and the page must agree on a small versioned envelope.
@studiouwu/engawa creates and reads that envelope and includes its TypeScript
declarations. Installing the npm package is the recommended setup.
Recommended: install the npm package¶
Use Node.js 24 or newer. Create a MyMenu folder, open a terminal there, and
add the event package and Vite:
Create this source structure:
Create vite.config.mjs so production URLs remain relative inside EngawaUI's
local bundle:
import { defineConfig } from "vite";
export default defineConfig({
base: "./",
build: {
outDir: "dist",
emptyOutDir: true
}
});
Create the HTML entry:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>EngawaUI event example</title>
<link rel="stylesheet" href="/src/menu.css">
<script type="module" src="/src/menu.js"></script>
</head>
<body>
<button id="startButton" type="button">Start game</button>
<p id="status">Waiting for Unreal…</p>
</body>
</html>
At the top of src/menu.js, import the package's default facade:
Importing the package also installs the same facade at
globalThis.EngawaUIEvents. Application modules should normally use the
imported value because the package then supplies both the implementation and
its TypeScript types. TypeScript projects can also
declare payloads for individual event names.
Run the production build:
Import dist/index.html as the EngawaUI Web File. Vite resolves the bare
package import and writes browser-ready files beneath dist; importing the
unbuilt source HTML or exposing node_modules to the page will not do that.
For a larger framework example, see the Vue project guide.
Without installing through npm: use the standalone global build¶
For a plain HTML project, download the pinned standalone browser build and save it in the page's source folder. This is the same package implementation vendored as one local script; it does not require npm or a bundler. Keep it local—EngawaUI pages cannot load the CDN URL at runtime.
Load the standalone build before the application script:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>EngawaUI event example</title>
<link rel="stylesheet" href="menu.css">
<script src="vendor/engawa.global.js"></script>
<script src="menu.js" defer></script>
</head>
<body>
<button id="startButton" type="button">Start game</button>
<p id="status">Waiting for Unreal…</p>
</body>
</html>
The standalone script installs the facade globally. Bind it in menu.js so
the rest of this guide is unchanged:
JavaScript needs no declaration file. For a TypeScript project that deliberately
does not install the package through npm, create src/engawa-events.d.ts:
export {};
type EngawaJsonValue =
| null
| boolean
| number
| string
| readonly EngawaJsonValue[]
| { readonly [key: string]: EngawaJsonValue };
interface EngawaEventMap {
readonly [eventName: string]: EngawaJsonValue;
}
type EngawaEventListener<
TPayload extends EngawaJsonValue = EngawaJsonValue
> = (
payload: TPayload,
eventName: string
) => void;
type EngawaUnsubscribe = () => boolean;
interface EngawaWaitOptions {
readonly signal?: AbortSignal;
readonly timeoutMs?: number;
}
interface EngawaReceivedEvent<
TPayload extends EngawaJsonValue = EngawaJsonValue
> {
readonly eventName: string;
readonly payload: TPayload;
}
interface EngawaEventEmitter<
TEvents extends EngawaEventMap = EngawaEventMap
> {
<TName extends Extract<keyof TEvents, string>>(eventName: TName): true;
<TName extends Extract<keyof TEvents, string>>(
eventName: TName,
payload: TEvents[TName]
): true;
}
interface EngawaEventChannel<
TEvents extends EngawaEventMap = EngawaEventMap
> {
readonly emit: EngawaEventEmitter<TEvents>;
readonly addListener: <TName extends Extract<keyof TEvents, string>>(
eventName: TName,
listener: EngawaEventListener<TEvents[TName]>
) => EngawaUnsubscribe;
readonly once: <TName extends Extract<keyof TEvents, string>>(
eventName: TName,
listener: EngawaEventListener<TEvents[TName]>
) => EngawaUnsubscribe;
readonly removeListener: <TName extends Extract<keyof TEvents, string>>(
eventName: TName,
listener: EngawaEventListener<TEvents[TName]>
) => boolean;
readonly clearListeners: (
eventName?: Extract<keyof TEvents, string>
) => void;
readonly waitFor: <TName extends Extract<keyof TEvents, string>>(
eventName: TName,
options?: EngawaWaitOptions
) => Promise<EngawaReceivedEvent<TEvents[TName]>>;
}
interface EngawaEventsFacade<
TEvents extends EngawaEventMap = EngawaEventMap,
TGameplayTagEvents extends EngawaEventMap = EngawaEventMap
> extends EngawaEventChannel<TEvents> {
readonly protocolVersion: 1;
readonly gameplayTags: EngawaEventChannel<TGameplayTagEvents>;
}
declare global {
namespace EngawaUI {
interface Events extends EngawaEventMap {
"menu.start.clicked": {
difficulty: string;
localPlayer: number;
};
"menu.start.result":
| { accepted: true; map: string }
| { accepted: false; reason: string };
}
interface GameplayTagEvents extends EngawaEventMap {
"UI.Inventory.Item.Selected": {
itemId: string;
slot?: number;
quantity?: number;
requestId?: string;
};
"UI.Inventory.Item.Confirmed": {
itemId: string;
accepted: boolean;
requestId?: string;
};
}
type Facade = EngawaEventsFacade<Events, GameplayTagEvents>;
}
interface Window {
readonly EngawaUIEvents: EngawaUI.Facade;
}
var EngawaUIEvents: EngawaUI.Facade;
}
This .d.ts file only teaches TypeScript about the global. The local
engawa.global.js script still has to run first; declarations do not install
JavaScript. When upgrading the standalone build, update the custom declaration
if its API changes. Remove the custom declaration if you later install the npm
package, which provides the authoritative types.
Import the local index.html into the Unreal Content Browser as an EngawaUI
Web File. Its HTML import includes vendor/engawa.global.js automatically;
the JavaScript file does not need a separate Content Browser asset.
2. Add and connect the Unreal components¶
Open the Actor Blueprint that owns your EngawaUI view.
- Click Add in the Components panel.
- Add Engawa UI Component if the actor does not already have one.
- Assign the imported
indexEngawaUI Web File to HTML Asset. - Click Add again and add EngawaUI Event.
- Select EngawaUI Event.
- Under EngawaUI > Events, set Source Engawa UI Component to the Engawa UI Component from step 2.
The component tree should look similar to this:
BP_MenuHost
├── DefaultSceneRoot
├── EngawaUIComponent
└── EngawaUIEvent
Source Engawa UI Component → EngawaUIComponent
Auto-find is convenient for a one-view actor
If the actor has exactly one Engawa UI Component, leaving Source Engawa UI
Component empty lets Auto Find Source Component resolve it during
BeginPlay. Select the source explicitly in a beginner project—it makes
the relationship visible and avoids ambiguity if a second view is added
later.
The Details picker intentionally selects a component on the same actor. To use a view owned by another actor, call Set Source Engawa UI Component in Blueprint with that component reference.
3. Send an event from JavaScript to Blueprint¶
Create src/menu.js with this complete npm-package example. If you chose the
standalone build, put the example in menu.js and replace its first line with
const EngawaUIEvents = globalThis.EngawaUIEvents;:
import EngawaUIEvents from "@studiouwu/engawa";
(() => {
"use strict";
const startButton = document.querySelector("#startButton");
const status = document.querySelector("#status");
startButton.addEventListener("click", () => {
const payload = {
difficulty: "normal",
localPlayer: 0
};
try {
EngawaUIEvents.emit("menu.start.clicked", payload);
status.textContent = "Start request posted to EngawaUI";
} catch (error) {
status.textContent = `Could not send: ${String(error)}`;
}
});
EngawaUIEvents.addListener("menu.start.result", (payload, eventName) => {
console.log("Received", eventName, payload);
status.textContent = payload.accepted
? `Loading ${payload.map}`
: `Rejected: ${payload.reason}`;
});
})();
Now add the Blueprint event:
- Select the EngawaUI Event component in the Blueprint.
- In Details > Events, click + beside On Event Received.
- Also add On Event Error now; it is the fastest way to diagnose setup and payload problems.
- Drag from Event Name and add Switch on String.
- Add a switch pin named exactly
menu.start.clicked. - For the first test, connect that pin to Print String and print Json Payload.
On Event Received
├── Event Name ──→ Switch on String
│ └── menu.start.clicked → begin game logic
└── Json Payload → {"difficulty":"normal","localPlayer":0}
On Event Error
└── Format/Print: Event Name + Error
The payload pin is compact JSON text, not an automatically expanded
Blueprint struct. Route by Event Name, then parse Json Payload with the
JSON solution used by your project or pass it to C++ for typed deserialization.
If no data is needed, send null or omit the JavaScript payload.
4. Limit which JavaScript events Blueprint receives¶
The component listens for every simple named event by default. For a small, auditable surface:
- Select the EngawaUI Event component.
- Disable Listen For All Events.
- Add
menu.start.clickedto Event Names To Listen For. - Add any other page-to-Unreal event names the actor owns.
Simple names are exact and case-sensitive. menu.start.clicked and
Menu.Start.Clicked are different events.
The runtime listener nodes follow these rules:
- Add Event Listener adds an exact name and disables Listen For All Events.
- Remove Event Listener removes one allowlisted name.
- Clear Event Listeners disables Listen For All Events and clears the array, so the component listens to none.
- Set Listen For All Events restores or disables catch-all behavior.
- Is Listening For Event reports the current effective filter.
Removing a name while Listen For All Events is still enabled does not block that name; disable catch-all mode first.
5. Send an event from Blueprint to JavaScript¶
The page's menu.js already listens for menu.start.result. Send that event
after the page is ready.
- Select the Engawa UI Component and add On Page Loaded from its Events section.
- Drag the EngawaUI Event component into the Event Graph.
- Drag from it and add Emit Event.
- Set Event Name to
menu.start.result. - Set Json Payload to:
- Connect On Page Loaded to Emit Event for this first test.
- Connect the node's False execution output to a diagnostic print. Keep On Event Error connected as well, because some validation occurs after the initial call returns.
Engawa UI Component: On Page Loaded
└── EngawaUI Event: Emit Event
Event Name = menu.start.result
Json Payload = {"accepted":true,"map":"TrainingGround"}
├── True → accepted into event preparation
└── False → rejected immediately
Do not emit this from BeginPlay
The Engawa UI Component begins loading during BeginPlay, but the page is
not ready yet. Emit Event requires both Is Initialized and Is Page
Ready. Use On Page Loaded for the first host event.
Payload rules¶
Emit Event accepts any complete JSON value in its Json Payload string.
| Intended JavaScript payload | Blueprint Json Payload |
|---|---|
| Object | {"health":100,"shield":25} |
| Array | ["potion","key"] |
| String | "Game paused" |
| Number | 42 |
| Boolean | true |
| Null | null or an empty pin |
From JavaScript, pass the value directly:
Do not stringify the payload. The facade and bridge serialize the complete event envelope. On the receiving side, the JavaScript listener gets the parsed value, while Blueprint gets compact JSON payload text.
Event names must:
- be non-empty strings;
- contain no control characters; and
- contain no more than 256 UTF-16 code units (the value reported by JavaScript
eventName.lengthfor this check).
Short dotted names such as system.subject.action are easy to organize and
debug.
Acknowledgements and request IDs¶
Events are at-most-once messages. Emit Event = true does not mean a JavaScript listener ran. If gameplay needs confirmation, create a second event and carry your own request ID.
let nextRequestId = 1;
const requestId = `settings-${nextRequestId++}`;
const unsubscribe = EngawaUIEvents.addListener(
"settings.volume.applied",
(payload) => {
if (payload.requestId !== requestId) return;
console.log("Unreal applied volume", payload.volume);
unsubscribe();
}
);
try {
EngawaUIEvents.emit("settings.volume.requested", {
requestId,
volume: 0.75
});
} catch (error) {
unsubscribe();
console.error("Volume request was not queued", error);
}
Blueprint receives settings.volume.requested, applies the setting, and emits
settings.volume.applied with the same requestId. This acknowledgement is
application logic; EngawaUI does not generate it automatically. Register the
response listener before emitting the request so even a very fast response has
a listener.
Async behavior and limits¶
The event component is designed to keep browser work away from the game thread:
- Unreal validates readiness and admits the message without waiting.
- Envelope JSON parsing and serialization runs on a background task.
- Blueprint delegates return to the game thread.
- JavaScript facade listeners run in a queued microtask.
- Each event component prepares one incoming and one outgoing event at a time.
- Each direction has room for at most 64 active/queued preparation items.
- Max Event Bytes defaults to 65,536 and is effectively clamped between 1,024 and 65,536 bytes.
- The byte limit covers the complete UTF-8 envelope, not only your payload.
- Runtime transport queues have separate bounds and may still reject an event.
On the page, EngawaUIEvents.emit(...) returns true after it validates the
name and calls the raw bridge without a synchronous exception. The facade does
not receive an application response, so this true is not proof that the
event component or Blueprint processed the event.
Order is preserved for events emitted through one event component. Do not depend on ordering across different event components or between events, raw messages, navigation, and JavaScript evaluation.
Understand the return value and error event¶
An immediate False output can mean:
- the event name is invalid;
- no source component is selected;
- the selected page is not ready;
- the payload already exceeds the configured character preflight; or
- the component's preparation queue is full.
A True output means the event was accepted for background preparation. Later On Event Error failures can still report:
- invalid JSON;
- a UTF-8 payload or complete envelope over Max Event Bytes;
- a source component that changed while work was pending;
- a page that stopped being ready; or
- rejection by the bounded EngawaUI command queue.
For incoming traffic, On Event Error also reports malformed event envelopes, oversized messages, and incoming queue pressure. In those cases the Event Name pin may be empty because no safe name could be recovered.
Raw messages and named events can coexist¶
The event component listens to the selected Engawa UI Component's On Page Message delegate. It does not remove or consume the raw message. If you also bind On Page Message, the versioned event envelope is visible there too.
Choose one handler as the owner of a message to avoid processing the same page action twice.
For reference, the simple-event envelope is:
{
"protocol": "engawaui.event",
"version": 1,
"channel": "event",
"event": "menu.start.clicked",
"payload": {
"difficulty": "normal"
}
}
Application code should use EngawaUIEvents.emit instead of constructing this
object manually.
Multiple Engawa UI views¶
One event component talks to one selected Engawa UI Component. If an actor owns two views, explicitly set the source on every event component. Auto-find reports an error rather than guessing.
Changing the source with Set Source Engawa UI Component safely unbinds the old view and discards that component's pending event-preparation work. Events that arrive late from a destroyed or replaced view are not delivered to the new source.
Troubleshooting¶
The package cannot be resolved or the global is missing¶
With the recommended setup, make sure @studiouwu/engawa is installed and the
application entry is processed by your bundler. A bare package import does not
run directly from an imported HTML source folder.
Without installing through npm, make sure the downloaded
vendor/engawa.global.js runs before the application script and read the
facade from globalThis.EngawaUIEvents. A custom .d.ts supplies no runtime
code. Rebuild or reimport the page, then stop and restart PIE because each
PIE/view session uses an immutable bundle snapshot.
The selected Engawa UI page is not ready¶
Emit the first Unreal-to-page event from the source component's On Page
Loaded, not from Actor BeginPlay.
JavaScript emits, but On Event Received does not run¶
Check all four items:
- Source Engawa UI Component points to the view displaying this page.
- Listen For All Events is enabled, or the exact case-sensitive name is in Event Names To Listen For.
- The page uses
EngawaUIEvents.emit, not the Gameplay Tag facade. - On Event Error is bound and shows no envelope/queue error.
JsonPayload is not a valid JSON value¶
Quote JSON keys and strings with double quotes. A bare Blueprint string such as
Hello is invalid; use "Hello". Do not use JavaScript object-literal syntax
with single quotes in the Blueprint JSON pin.
An occasional event is dropped under heavy traffic¶
Do not send an event every browser animation frame or Unreal Tick. Coalesce high-frequency state into meaningful updates. For guaranteed application confirmation, add a response event and request ID.
Included test page¶
The plugin contains a ready-made manual fixture:
The page sends demo.simple.from-javascript and listens for
demo.simple.from-unreal. Import the fixture's index.html as an EngawaUI Web
File and assign that asset to the component before using the test page.