Tutorial 3: Blueprint and C++ integration
Once the page exists, the Unreal side has four jobs: host the page, receive actions, publish state, and provide sounds and resources. You can do all four in Blueprint alone, in C++, or with a C++ base class and a Blueprint subclass.
On this page
- Before you start: download the dependencies
- Choose a route
- The contract used here
- Blueprint only: from scratch
- 1. Create the App Definition
- 2. Create the Widget that hosts the page
- 3. Show it
- 4. Receive actions: On Web Event
- 5. Publish state
- 6. Requests that need a result: On Web Request
- 7. Ready and errors
- 8. Presentation state and cleanup
- JSON in Blueprint
- Common features in Blueprint
- Control sounds
- Sounds with a business meaning
- Text and language
- Runtime images
- Live pictures: Native Surface
- WorldUI: labels and interaction that follow Actors
- Remote websites
- Host it with CommonUI, without C++
- Blueprint node reference
- C++: from scratch
- Module dependencies
- Plain widget: UUserWidget with BindWidget
- CommonUI pages
- Four channels for publishing state
- Resource interfaces
- WorldUI
- A C++ base class with a Blueprint subclass
- InstantScreen overview
- Troubleshooting
To have AI write the page, see Tutorial 1; to write it by hand, see Tutorial 4.
Game state, rules and saved data belong to Unreal; the page renders state and submits actions. Check every parameter from the page again in Unreal.
Before you start: download the dependencies
Click the WebUIStudio button on the Level Editor toolbar and click Download in the OrionBrowser Dependencies window. Without the dependencies an interface shows only a line of text and the project cannot be packaged. Skip this if you have already done it; details are in Installation.
Choose a route
Use Blueprint only when the state is a few numbers, strings and switches. Use C++ when the state has arrays and nesting, or you need high-frequency data or guaranteed delivery. Use a C++ base class with a Blueprint subclass when programmers own the validation and designers configure sounds and assets in Blueprint.
The host can be a plain widget (an Orion WebUI in a Widget Blueprint), a CommonUI page (parent class Orion WebUI Activatable Widget), or an InstantScreen, where several pages share one browser. For a first integration use Blueprint only with a plain widget.
The contract used here
The settings panel of Tutorials 1 and 4. The AppId is SettingsPanel, and the production files are in the project's Content/.
| Direction | Name | Parameters | Meaning |
|---|---|---|---|
| Unreal → page | ue:settingsPanel.state |
{ stateRevision, volume, quality, culture } |
Full state |
| Page → Unreal (emit) | settingsPanel.ready |
{} |
The page is listening |
| Page → Unreal (emit) | settingsPanel.setVolume |
{ volume: 0–100 } |
Change the volume |
| Page → Unreal (emit) | settingsPanel.setQuality |
{ quality: "low" | "medium" | "high" } |
Choose the quality |
| Page → Unreal (call) | settingsPanel.resetDefaults |
{} |
Restore defaults; needs a result |
| Page → Unreal (emit) | settingsPanel.close |
{} |
Close the panel |
A message the page sends with emit arrives in On Web Event. One sent with call arrives in On Web Request and must be answered.
- StateRevision
- 0
- Volume
- 80
- Quality
- high
- Culture
Settings
Blueprint only: from scratch
1. Create the App Definition
- Right-click in the Content Browser, choose Miscellaneous → Data Asset, pick the class
OrionWebUIAppDefinition(search forOrion), and name itDA_SettingsPanel. - Open it and set:
| Property | Value | Note |
|---|---|---|
AppId |
SettingsPanel |
Must be identical to the folder name and to orionWebUI.appId in the page's package.json |
EntryHtml |
index.html |
The default |
| Use Dev Server in Editor | cleared | When enabled, Designer prefers a local development server |
DA_SettingsPanel. Two properties were changed: App Id is SettingsPanel and Use Dev Server in Editor is cleared. A changed property shows the reset arrow on its right.Leave everything else at its default. The common properties are listed in Tutorial 4.
2. Create the Widget that hosts the page
- Create a User Widget Blueprint named
WBP_SettingsPanel. - In Designer, find Orion WebUI in the palette, drag it in, name it
WebUI, enable Is Variable, and let it fill its parent. - In Details, set App Definition to
DA_SettingsPanel. Designer shows the page.
WBP_SettingsPanel. The hierarchy holds one widget, WebUI: dragged into an empty hierarchy, Orion WebUI becomes the root and fills the whole widget by itself; only inside a container such as a Canvas Panel does it have to be told to fill. Once the Helper is ready, the canvas draws the page from dist.WebUI selected: the name is WebUI, Is Variable is ticked and App Definition points at DA_SettingsPanel from the previous step. Everything else stays at its default.3. Show it
In the Player Controller (or wherever you manage interfaces):
- Create Widget (Class
WBP_SettingsPanel, Owning PlayerSelf) and store the result in a variable. - Add to Viewport.
- Set Input Mode Game And UI: connect
Selfto Player Controller (the node does nothing while it is empty) and theWebUIof this Widget to In Widget to Focus, so that keyboard input reaches the page. - Set Show Mouse Cursor to
true.
BP_PlayerController: Create Widget (Owning Player wired to Self), the return value stored in the variable SettingsPanel, Add to Viewport, then Set Input Mode Game And UI giving focus to the WebUI inside that widget, and finally the cursor is shown. The Player Controller pin of Set Input Mode Game And UI needs Self as well; left empty, the node does nothing. The graph starts at BeginPlay; use whatever moment should show the interface, but create it only once. The picture is wider than the column: scroll it sideways, or click to enlarge.To close, do the reverse: Remove from Parent, Set Input Mode Game Only (connect its Player Controller too; inside the Widget Blueprint, use Get Owning Player), hide the cursor. Create an interface only once.
BP_PlayerController is the Blueprint of the previous figure (brought in by a Game Mode that names it as the default Player Controller).4. Receive actions: On Web Event
Select WebUI in Designer and click the plus next to On Web Event under Events at the bottom of Details. The event gives Event Name and Payload Json. Connect a Switch on Name, select it, and add one output per event name under Pin Names in Details. The node shows pin names with spaces (Settings Panel.set Volume); the comparison still uses the name you entered.
WebUI selected in Designer and Events typed into the search box. The plus button next to an event adds its node to the event graph; for an event that already has one (here On Web Event and On Web Request) the button jumps to that node instead.Settings Panel.set Volume); what is compared is still the settingsPanel.setVolume you typed. Default carries no game logic: unregistered event names are ignored. The next figures show what follows each output.settingsPanel.ready: call PublishState from the next section.
settingsPanel.setVolume:
- Get Json Number: connect
Payload JsontoJson, and entervolumeasField. - Branch on
Valid. If it isfalse, do nothing. - Branch on the value being between 0 and 100.
- If it passes: Round it to an integer, store it in the variable
Volume, apply it to the game's volume setting, and callPublishState. - If it does not pass, call
PublishStateanyway to correct the page's display.
settingsPanel.setVolume output: Get Json Number reads volume; the first Branch tests Valid, the second In Range (Float) (0 to 100); when both pass, the result of Round is stored in Volume and Publish State is called. Both False outputs reach Publish State too, through the reroute nodes below: pushing the correct state back is what corrects the page. The picture is wider than the column: scroll it sideways, or click to enlarge.settingsPanel.setQuality: read quality with Get Json String, check Valid, accept only low, medium and high with a Switch on String, then store it and call PublishState.
settingsPanel.setQuality output: Get Json String reads quality, Branch tests Valid, and Switch on String lets only low, medium and high through; the value is then stored in Quality and published. False and Default reach Publish State as well. The picture is wider than the column: scroll it sideways, or click to enlarge.settingsPanel.close: run the closing steps of section 3.
settingsPanel.close output: Remove from Parent, then Set Input Mode Game Only on the Player Controller that Get Owning Player returns, and the cursor is hidden. The picture is wider than the column: scroll it sideways, or click to enlarge.Connect no business logic to the Default output. The plugin's own notifications (for example __orion.inputReady) arrive here too; let them fall through to Default.
5. Publish state
Create a function PublishState, with the variables StateRevision (Integer64), Volume (Integer), Quality (String) and Culture (String):
WBP_SettingsPanel: the PublishState function and the four variables of this section. WebUI appears by itself once Is Variable is ticked; bResetAllowed and ControlSounds belong to the next section and to “Control sounds”.- Add one to
StateRevision. - Set Json Number: enter
{}asJson,stateRevisionasField, and connectStateRevisiontoValue. - Connect the return value to
Jsonof the next Set Json Number, withFieldvolume. - In the same way add two Set Json String nodes for
qualityandculture. - Call Post Retained Latest Event to Web on
WebUI:Event Nameisue:settingsPanel.state, andPayload Jsonis the return value of the last node.
PublishState: StateRevision is increased by one and stored; four Set Json nodes, starting from {}, write stateRevision, volume, quality and culture in turn, and the last return value goes to Post Retained Latest Event to Web. The Set Json nodes are pure: no execution pins, only the data wires chain them. Where an integer meets Value, the editor inserted a small conversion node by itself. The picture is wider than the column: scroll it sideways, or click to enlarge.Always send the full state; stateRevision only increases. A reloaded page sends settingsPanel.ready again, so always publish when you receive it.
Volume and Quality, and PublishState sent the whole state back for the page to show.6. Requests that need a result: On Web Request
Add the On Web Request event of WebUI. It gives Request and Response. Break Request (click the arrow at the bottom of the Break node to expand it) to get Name and Payload Json, then connect a Switch on Name:
Request to get Name, which feeds Switch on Name again. The Break node lists only the first members by default; Payload Json and the rest are behind the arrow at its bottom. The blue Response wire runs along the bottom of the graph to every node that answers. The picture is wider than the column: scroll it sideways, or click to enlarge.settingsPanel.resetDefaults:
- When it is allowed: set the variables back to their defaults and apply them, call
PublishState, then call Resolve Json onResponsewith{}asData Json. - When it is not: call Reject on
Response, with your own error code asError Code(for exampleE_LOCKED) and a reason for the player asError Message.
Default output: call Reject on Response with the code E_UNKNOWN_REQUEST.
settingsPanel.resetDefaults output: Branch tests your own rule (here the variable bResetAllowed). When allowed, Volume and Quality go back to their defaults, then Publish State, then Resolve Json; when not, Reject with the code E_LOCKED. The Default output has a Reject of its own (E_UNKNOWN_REQUEST): otherwise a request nobody answered is resolved automatically with {}. Answer each request once. The picture is wider than the column: scroll it sideways, or click to enlarge.Rules:
- Each request is answered once. Call Resolve Json only after Unreal has really accepted the action.
- When the result takes a while (waiting for a save to finish, for example), call Defer first, store
Responsein a variable, and answer later. If no answer arrives withinBridgeCallTimeoutSecondsof the App Definition (15 seconds by default), the page receivesE_TIMEOUT. - A request that gets neither an answer nor Defer succeeds with
{}by default (Auto Resolve Unhandled Requests onWebUI), so always Reject explicitly on the Default output.
7. Ready and errors
- On Web Ready: the page and Unreal are connected; it fires again after the page reloads. You can publish the first state here.
- On Web Error: bridge errors, blocked navigation and the like. While developing, connect it to Print String; look here first when something is wrong.
8. Presentation state and cleanup
The page becomes visible by itself once it is ready. If a Blueprint-only interface calls Set Presentation Lifecycle State, use Visible alone, with 0 as Presentation Revision. To hide the interface for a while, change the Widget's visibility or remove it.
When the Widget is destroyed, clean up what you used in Event Destruct:
- Deactivate Control Sound Policy (with
SelfasPolicy Owner) - Clear Native Surfaces and Release Texture Resources
- Unbind Overlay Widget of WorldUI
- Your own timers
JSON in Blueprint
Get Json String / Number / Boolean reads one top-level field; Valid is false when the field is missing or has another type. Set Json String / Number / Boolean writes one field and returns the new JSON. They handle top-level fields only, and they escape quotes and line breaks correctly; do not concatenate text entered by the player with Append.
For arrays or nested objects: flatten them into top-level fields (such as slot0Name, slot0Count and slotCount); or enable the engine's Json Blueprint Utilities plugin, build the object, and publish it as a string; or move that part of the state into C++.
Common features in Blueprint
Control sounds
Hover and click sounds are played by Unreal. Every clickable element of the page has a control Id (data-orion-control-id), and a policy in Blueprint decides what to play.
- In the Widget Blueprint that hosts the page, add a variable
ControlSoundsof typeOrionWebUIControlSoundPolicyand compile. - Fill in its default value:
| Field | Meaning |
|---|---|
| Default Sounds → Hover Sound / Click Sound | The default sounds of every control; each has Sound, Volume Multiplier, Enabled and more |
| Control Overrides | Overrides by control Id. Enter Control Id, enable Override Hover Sound or Override Click Sound, and fill in that sound |
ControlSounds variable (Default Value in Details, with the variable selected in My Blueprint). Default Sounds apply to every control; here they are S_StationHover and S_StationClick from the plugin's sample. Control Overrides replace them per control Id. The entry shown silences the click of settings-close: Override Click Sound is ticked and Enabled of the Click Sound below it is cleared.- Event Construct: call Activate Control Sound Policy on
WebUIwithSelfasPolicy Owner, an emptyContext Id, andControlSoundsasPolicy. - Event Destruct: call Deactivate Control Sound Policy on
WebUIwithSelfasPolicy Owner.
Context Id empty (None); wire Policy to the ControlSounds variable.Notes:
- To silence one control, add an override, enable the matching Override switch, and clear Enabled of that sound.
- To change the policy (when the volume changes, for example), call Activate Control Sound Policy again for the same
Policy Owner. - When an area of the page needs another policy, the page writes
data-orion-sound-context="Shop"on the area's root element, and Blueprint activates a second policy with a differentPolicy OwnerandShopasContext Id. - To check the Ids, select
WebUIin Designer and set Design Time Control Id Preview Mode to All Controls.
Sounds with a business meaning
Sounds such as "saved" or "not enough credit" are requested by the page at the right moment; the sound assets are configured in Unreal:
- Create an
OrionWebUISoundManifestdata asset and registerStableId(for examplesettings.saved) andSoundin Entries. - Assign it to Sound Manifest of the App Definition.
- The page calls
api.playSound("settings.saved"). Blueprint can also call Play Sound by Id onWebUI.
data-orion-control-id="settings-reset"api.playSound("settings.saved")playSound.Text and language
The simplest way is to publish culture in the state and let the page use its own bilingual dictionary, as the supplied sample does. To use Unreal's localization pipeline, create an OrionWebUITextCatalog data asset, register keys (for example settingsPanel.title) with Unreal text in Texts, and assign it to Text Catalog of the App Definition; the page looks text up by key.
Runtime images
To show a Texture2D (an avatar, an icon generated at run time) in the page:
- Call Request Texture Resource on
WebUI: a stable name asStable Id(for exampleplayer.avatar), the texture asTexture, andOptionsbuilt with a Make node, withMax Output Dimensionset for the displayed size. - Bind On Texture Resource Completed of
WebUI. WhenStatusis Ready, writeUrlfromHandleinto the state and publish it; the page uses it for an image. - When it is no longer needed, call Release Texture Resource.
Requesting the same Stable Id again yields a new address, so the page uses only the latest one. When pixels change in place, raise Source Revision in Options. Virtual textures are not supported.
Live pictures: Native Surface
Draws a Render Target, a scene capture or a UI material directly at a position in the page:
- The page puts a placeholder element at the position and binds it under a stable name (for example
station.capture). - Blueprint calls Set Native Surface Texture on
WebUI(Surface Idis the same name,Textureis the Render Target), or Set Native Surface Material. Position and size follow the placeholder. - When finished, call Clear Native Surface.
There are also Set Native Surface Tint (tint and opacity), Set Native Surface Composition Layer (Above Browser by default; Below Browser draws under the page and needs transparency enabled on the App Definition), and On Native Surface Layout Changed (pause the scene capture while Visible is false). The native picture receives no mouse input; put rounded corners and masks into the UI material.
WorldUI: labels and interaction that follow Actors
Each local player has one page that carries every world label; Actors carry a data component only.
- Create an
OrionWebUIWorldElementDefinitiondata asset and set what you need:ElementType(the page uses it to choose a renderer),MaxVisibleDistance(0disables distance culling),bCheckOcclusion,bClampToViewport,bScaleWithDistance. For clicks, enablebInteractiveand enter action names inAllowedActions(for examplecollect);MaxInteractionDistanceis 300 by default. - Add the component to the Actor (search for
World Element; the class isOrionWebUIWorldElementComponent): assign Definition and enter Element Key. - Provide text and state with Set Payload Json; the default renderer in the page reads
labelandactionLabel. Show or hide with Set Element Visible. - After the interface is created, get the local player subsystem
OrionWebUIWorldSubsystemand call Bind Overlay Widget withWebUI. - Bind On Interaction of the component (it gives
Player,Action IdandPayload Json), decide whether the game rules allow it, then change state. A networked game sends a validated server request as well. - Call Unbind Overlay Widget when the interface is destroyed.
Remote websites
Use a separate Orion Browser widget for external websites. It has no game bridge; do not mix it with Orion WebUI. Open an address with Initial URL or Load URL, navigate with Go Back / Go Forward / Reload / Stop Load, and show a failed view on On Load Error, as the sample's WBP_OrionWebsite does.
Host it with CommonUI, without C++
- Choose Orion WebUI Activatable Widget as the parent class when you create the Widget Blueprint.
- Add an Orion WebUI child. Its name must be
WebUI. - In Class Defaults set App Definition, Control Sound Policy (no manual activation needed), Input Config (
Menuby default), and optionally Input Manifest. - In the event graph, override
HandleWebUIEvent,HandleWebUIRequestandHandleWebUIReady. The logic is the same as for On Web Event and On Web Request above. - Push it onto your CommonUI stack.
When an action of the Input Manifest fires, ue:commonAction is sent to the page and HandleCommonAction is called; when the page already submits an action for it, Blueprint must not run it again. Override HandleWebUIBackAction and return true to say you handled back; when nothing handled it, Deactivate on Back Action decides whether the page closes.
Blueprint node reference
All of these are on WebUI (Orion WebUI).
| Category | Nodes |
|---|---|
| Loading | Load App, Reload App, Set Initial Route Override, Get Current Route |
| Sending | Post Retained Latest Event to Web (a page that starts listening later still receives the newest), Post Event to Web (lost if the page is not listening) |
| Answering requests | On Response: Resolve Json, Reject, Defer, Has Responded |
| Calling the page | Call Web; the result arrives in On Web Call Completed. Use it only to ask the page about presentation |
| Sound, images, native surfaces | Play Control Sound, Play Sound by Id, Request Texture Resource, Set Native Surface Texture and more; see the sections above |
| Advanced delivery | Post Flow Controlled Latest Event to Web and others; see the C++ channels |
C++: from scratch
Module dependencies
The plugin's browser modules support Win64 only and take no part in Server builds. Put the code that hosts interfaces into a module built for clients only:
// MyGameUI.Build.cs
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "UMG", "OrionWebUI", "OrionWebUIWidget" });
PrivateDependencyModuleNames.AddRange(new string[] { "Json" });
// For CommonUI pages add: "CommonUI", "OrionWebUICommonUI"
// For WorldUI add: "OrionWebUIWorld" (the component, available on Server) and "OrionWebUIWorldWidget" (the subsystem)When the project has a Server target, give this module "TargetDenyList": ["Server"] and "PlatformAllowList": ["Win64"] in its descriptor in the .uproject or .uplugin.
Plain widget: UUserWidget with BindWidget
A Widget Blueprint uses this class as its parent and has an Orion WebUI named WebUI in Designer.
// SettingsPanelWidget.h
#pragma once
#include "Blueprint/UserWidget.h"
#include "OrionWebUIDefinitions.h"
#include "SettingsPanelWidget.generated.h"
class UOrionWebUIWidget;
UCLASS(Abstract, Blueprintable)
class MYGAMEUI_API USettingsPanelWidget : public UUserWidget
{
GENERATED_BODY()
protected:
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
private:
UFUNCTION()
void HandleWebReady();
UFUNCTION()
void HandleWebEvent(FName EventName, const FString& PayloadJson);
UFUNCTION()
void HandleWebRequest(const FOrionWebUIBridgeRequest& Request, UOrionWebUIResponseHandle* Response);
void PublishState();
protected:
UPROPERTY(BlueprintReadOnly, meta=(BindWidget))
TObjectPtr<UOrionWebUIWidget> WebUI;
UPROPERTY(EditDefaultsOnly, Category="Settings Panel")
FOrionWebUIControlSoundPolicy ControlSounds;
private:
int64 StateRevision = 0;
int32 Volume = 80;
FString Quality = TEXT("high");
FString Culture = TEXT("en");
};// SettingsPanelWidget.cpp
#include "SettingsPanelWidget.h"
#include "Dom/JsonObject.h"
#include "OrionWebUIWidget.h"
#include "Policies/CondensedJsonPrintPolicy.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h"
namespace SettingsPanel
{
static const FName StateEvent(TEXT("ue:settingsPanel.state"));
static const FName ReadyEvent(TEXT("settingsPanel.ready"));
static const FName SetVolumeEvent(TEXT("settingsPanel.setVolume"));
static const FName SetQualityEvent(TEXT("settingsPanel.setQuality"));
static const FName CloseEvent(TEXT("settingsPanel.close"));
static const FName ResetDefaultsRequest(TEXT("settingsPanel.resetDefaults"));
}
void USettingsPanelWidget::NativeConstruct()
{
Super::NativeConstruct();
if (!WebUI)
{
return;
}
WebUI->OnWebReady.AddUniqueDynamic(this, &ThisClass::HandleWebReady);
WebUI->OnWebEvent.AddUniqueDynamic(this, &ThisClass::HandleWebEvent);
WebUI->OnWebRequest.AddUniqueDynamic(this, &ThisClass::HandleWebRequest);
WebUI->ActivateControlSoundPolicy(this, NAME_None, ControlSounds);
}
void USettingsPanelWidget::NativeDestruct()
{
if (WebUI)
{
WebUI->DeactivateControlSoundPolicy(this);
WebUI->OnWebReady.RemoveDynamic(this, &ThisClass::HandleWebReady);
WebUI->OnWebEvent.RemoveDynamic(this, &ThisClass::HandleWebEvent);
WebUI->OnWebRequest.RemoveDynamic(this, &ThisClass::HandleWebRequest);
}
Super::NativeDestruct();
}
void USettingsPanelWidget::HandleWebReady()
{
PublishState();
}
void USettingsPanelWidget::HandleWebEvent(FName EventName, const FString& PayloadJson)
{
if (EventName == SettingsPanel::ReadyEvent)
{
PublishState();
return;
}
if (EventName == SettingsPanel::CloseEvent)
{
RemoveFromParent();
return;
}
TSharedPtr<FJsonObject> Payload;
if (!FJsonSerializer::Deserialize(TJsonReaderFactory<>::Create(PayloadJson), Payload) || !Payload.IsValid())
{
return;
}
if (EventName == SettingsPanel::SetVolumeEvent)
{
double RequestedVolume = 0.0;
if (Payload->TryGetNumberField(TEXT("volume"), RequestedVolume) && RequestedVolume >= 0.0 && RequestedVolume <= 100.0)
{
Volume = FMath::RoundToInt32(RequestedVolume);
// Apply the volume to the game here.
}
PublishState();
return;
}
if (EventName == SettingsPanel::SetQualityEvent)
{
FString RequestedQuality;
if (Payload->TryGetStringField(TEXT("quality"), RequestedQuality)
&& (RequestedQuality == TEXT("low") || RequestedQuality == TEXT("medium") || RequestedQuality == TEXT("high")))
{
Quality = RequestedQuality;
}
PublishState();
}
}
void USettingsPanelWidget::HandleWebRequest(const FOrionWebUIBridgeRequest& Request, UOrionWebUIResponseHandle* Response)
{
if (!Response)
{
return;
}
if (Request.Name == SettingsPanel::ResetDefaultsRequest)
{
Volume = 80;
Quality = TEXT("high");
PublishState();
Response->ResolveJson(TEXT("{}"));
return;
}
Response->Reject(TEXT("E_UNKNOWN_REQUEST"), TEXT("Unknown settings panel request."));
}
void USettingsPanelWidget::PublishState()
{
if (!WebUI)
{
return;
}
const TSharedRef<FJsonObject> State = MakeShared<FJsonObject>();
State->SetNumberField(TEXT("stateRevision"), static_cast<double>(++StateRevision));
State->SetNumberField(TEXT("volume"), Volume);
State->SetStringField(TEXT("quality"), Quality);
State->SetStringField(TEXT("culture"), Culture);
FString Json;
FJsonSerializer::Serialize(State, TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Json));
WebUI->PostRetainedLatestEventToWeb(SettingsPanel::StateEvent, Json);
}The answering rules are the same as in Blueprint; to answer asynchronously, call Response->Defer() first and keep the pointer. FName comparison ignores case and the page does not, so spell an event name one way throughout the project. If you do not want to parse JSON yourself, bind the native delegate OnWebEventParsed with AddUObject; it gives a TSharedPtr<FJsonObject>.
CommonUI pages
When the page enters a CommonUI stack, derive from UOrionWebUIActivatableWidget; the base class handles loading the app, the sound policy, input actions and back. The Widget Blueprint contains an Orion WebUI child named WebUI; configure AppDefinition, InputManifest, ControlSoundPolicy and InputConfig in Class Defaults, and push the page only through your CommonUI stack, never with AddToViewport.
// SettingsPanelScreen.h
#pragma once
#include "OrionWebUIActivatableWidget.h"
#include "SettingsPanelScreen.generated.h"
UCLASS(Abstract, Blueprintable)
class MYGAMEUI_API USettingsPanelScreen : public UOrionWebUIActivatableWidget
{
GENERATED_BODY()
protected:
virtual void NativeOnActivated() override;
virtual void HandleWebUIReady_Implementation() override;
virtual void HandleWebUIEvent_Implementation(FName EventName, const FString& PayloadJson) override;
virtual void HandleWebUIRequest_Implementation(const FOrionWebUIBridgeRequest& Request, UOrionWebUIResponseHandle* Response) override;
virtual bool HandleWebUIBackAction_Implementation() override;
private:
void PublishState(bool bForceRepublish);
private:
FString PublishedStateJson;
int64 StateRevision = 0;
};// SettingsPanelScreen.cpp (excerpt)
void USettingsPanelScreen::NativeOnActivated()
{
Super::NativeOnActivated();
PublishState(true); // State may have changed while deactivated: publish from current data
}
void USettingsPanelScreen::HandleWebUIReady_Implementation()
{
Super::HandleWebUIReady_Implementation();
PublishState(true);
}
void USettingsPanelScreen::HandleWebUIEvent_Implementation(FName EventName, const FString& PayloadJson)
{
if (EventName == StandardPresentationReadyEventName || EventName == StandardPresentationAppliedEventName)
{
return;
}
// The rest is the same as HandleWebEvent of the plain widget; get the widget with GetWebUIWidget()
}
bool USettingsPanelScreen::HandleWebUIBackAction_Implementation()
{
return false; // Return true when you handled back yourself; the default close does not run
}Four channels for publishing state
| Function | When |
|---|---|
PostRetainedLatestEventToWeb |
Ordinary state; the default choice |
PostFlowControlledLatestEventToWeb(Event, Json, AckEvent, RevisionField, Revision) |
Data that may change every frame |
PostReliableRetainedLatestEventToWeb(..., RetryIntervalSeconds, MaxAttempts) |
Snapshots that must arrive |
PostReliablePresentationEventToWeb(...) |
Entering the screen without showing a half-built page; the CommonUI base class already wraps it |
Allocate revisions with UOrionWebUIWidget::AllocatePresentationRevision(); they are a separate number space from the stateRevision of business state. Do not allocate a new revision when the content did not change. An acknowledgement from the page only says that a revision arrived or is shown; it does not mean business success.
Resource interfaces
In C++, use RequestTextureResource(StableId, Texture, Options) (OnTextureResourceCompleted fires on completion), SetNativeSurfaceTexture and SetNativeSurfaceMaterial. For CPU pixels you already have, use RequestPixelResource(StableId, FOrionWebUIPixelSource, Options), which is C++ only; for a material that samples transient resources, use SetNativeSurfaceMaterialWithDependencies(SurfaceId, Material, Dependencies). When you change the Render Target, create a new dynamic material instance and call SetNativeSurfaceMaterial; do not modify an instance that is still bound.
WorldUI
#include "OrionWebUIWorldSubsystem.h"
if (ULocalPlayer* LocalPlayer = GetOwningLocalPlayer())
{
if (UOrionWebUIWorldSubsystem* WorldUI = LocalPlayer->GetSubsystem<UOrionWebUIWorldSubsystem>())
{
WorldUI->BindOverlayWidget(WebUI);
}
}OnInteraction of the component is a dynamic multicast delegate (APlayerController* Player, FName ActionId, const FString& PayloadJson). For a label that belongs to one local player, use SetOwningLocalPlayer of the component.
A C++ base class with a Blueprint subclass
Write the validation that must not be bypassed in C++, and leave what designers tune to Blueprint:
UCLASS(Abstract, Blueprintable)
class MYGAMEUI_API USettingsPanelWidget : public UUserWidget
{
GENERATED_BODY()
protected:
UFUNCTION(BlueprintNativeEvent, Category="Settings Panel")
void OnVolumeAccepted(int32 NewVolume);
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Settings Panel")
bool bResetEnabled = true;
};- Call the
BlueprintNativeEventonly after validation passes; a Blueprint override cannot skip it. - Give each fixed button one event and one enable switch, and publish the switches to the page with the full state.
- The sound policy, App Definition and Input Manifest are
EditDefaultsOnlyproperties configured in Class Defaults of the Blueprint subclass. - Do not rename, change the signature of, or delete events that Blueprints already override.
InstantScreen overview
InstantScreen lets several interfaces on one layer share one resident browser, and shows an interface only once its state, resources and first frame are ready. For a single panel, a plain widget is enough. It needs instant-screen.config.ts in the page, assets such as OrionInstantScreenDefinition, an Orion InstantScreen Runtime Host in the root layout, and interfaces whose parent derives from UOrionInstantScreenActivatableWidget; see the InstantScreen reference shipped with the plugin.
Troubleshooting
| Symptom | Check |
|---|---|
| The page is blank | Are AppId, the folder name and orionWebUI.appId identical? Does dist/index.html exist? Look at On Web Error |
Designer shows Orion WebUI dist entry is missing |
The app was not built, or AppId is wrong |
| Clicks do nothing | Is On Web Event bound on WebUI? Do the event names under Pin Names match character for character? Is Valid false? |
| The page keeps showing an old value | Do you publish after every change? Does stateRevision increase? |
| A request waits until it times out | Defer was called but no answer followed |
| A request always "succeeds" | The Default branch has no Reject |
| Keyboard and gamepad do nothing | Is the viewport a CommonGameViewportClient (see installation)? Is the Player Controller pin of Set Input Mode Game And UI connected, and does focus go to WebUI? |
| The page disappears after Set Presentation Lifecycle State | A Blueprint-only page uses Visible alone; remove calls with other states |
| No sound | Is a policy active? Do the controls have Ids? Does Context Id match? |
| A runtime image does not show | Did you wait for Ready before giving the address to the page? Is it a virtual texture? |
| The page is blank after packaging | Build dist first, then package; see Package and troubleshoot |