Skip to content

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.

Use Node.js 24 or newer. Create a MyMenu folder, open a terminal there, and add the event package and Vite:

npm init -y
npm add @studiouwu/engawa
npm add --save-dev vite

Create this source structure:

MyMenu/
├── package.json
├── vite.config.mjs
├── index.html
└── src/
    ├── menu.js
    └── menu.css

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:

import EngawaUIEvents from "@studiouwu/engawa";

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:

npx vite 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.

MyMenu/
├── index.html
├── vendor/
│   └── engawa.global.js
├── menu.js
└── menu.css

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:

const EngawaUIEvents = globalThis.EngawaUIEvents;

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.

  1. Click Add in the Components panel.
  2. Add Engawa UI Component if the actor does not already have one.
  3. Assign the imported index EngawaUI Web File to HTML Asset.
  4. Click Add again and add EngawaUI Event.
  5. Select EngawaUI Event.
  6. 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:

  1. Select the EngawaUI Event component in the Blueprint.
  2. In Details > Events, click + beside On Event Received.
  3. Also add On Event Error now; it is the fastest way to diagnose setup and payload problems.
  4. Drag from Event Name and add Switch on String.
  5. Add a switch pin named exactly menu.start.clicked.
  6. 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:

  1. Select the EngawaUI Event component.
  2. Disable Listen For All Events.
  3. Add menu.start.clicked to Event Names To Listen For.
  4. 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.

  1. Select the Engawa UI Component and add On Page Loaded from its Events section.
  2. Drag the EngawaUI Event component into the Event Graph.
  3. Drag from it and add Emit Event.
  4. Set Event Name to menu.start.result.
  5. Set Json Payload to:
{"accepted":true,"map":"TrainingGround"}
  1. Connect On Page Loaded to Emit Event for this first test.
  2. 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:

EngawaUIEvents.emit("inventory.changed", {
  slots: ["sword", "potion"],
  selected: 1
});

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.length for 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:

  1. Source Engawa UI Component points to the view displaying this page.
  2. Listen For All Events is enabled, or the exact case-sensitive name is in Event Names To Listen For.
  3. The page uses EngawaUIEvents.emit, not the Gameplay Tag facade.
  4. 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:

Plugins/EngawaUI/Content/Web/EventsTest/index.html

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.

See also