教程 3:蓝图与 C++ 接入
网页写好之后,Unreal 这一侧要做四件事:承载页面、接收操作、发布状态、提供声音与资源。可以全部用蓝图,也可以用 C++,或者由 C++ 写基类、蓝图做子类。
本页目录
- 开始之前:下载依赖
- 先选路线
- 本教程用到的合同
- 纯蓝图:从零接入
- 1. 建 App Definition
- 2. 建承载页面的 Widget
- 3. 显示出来
- 4. 接收操作:On Web Event
- 5. 发布状态
- 6. 需要结果的请求:On Web Request
- 7. 就绪与错误
- 8. 显示状态与清理
- 蓝图里的 JSON
- 蓝图里的常用功能
- 控件音效
- 有业务含义的声音
- 文字与语言
- 运行时图片
- 实时画面:Native Surface
- WorldUI:跟随 Actor 的标签与交互
- 远程网站
- 用 CommonUI 承载,不写 C++
- 蓝图节点速查
- C++:从零接入
- 模块依赖
- 普通控件:UUserWidget 加 BindWidget
- CommonUI 页面
- 发布状态的四种通道
- 资源接口
- WorldUI
- C++ 基类加蓝图子类
- InstantScreen 概览
- 排查
游戏状态、规则和存档属于 Unreal,网页只显示状态并提交操作。 网页发来的每个参数都要在 Unreal 里重新检查。
开始之前:下载依赖
点击关卡编辑器工具栏上的 WebUIStudio 按钮,在“OrionBrowser 依赖”窗口里点“下载”。没有依赖时界面只显示一行提示,工程也不能打包。已经下载过就跳过,详见安装。
先选路线
状态只是若干个数字、文字和开关时用纯蓝图;状态里有数组和嵌套结构、需要高频数据或可靠送达时用 C++;程序定校验、设计师在蓝图里配声音和资产时用 C++ 基类加蓝图子类。
承载页面可以用普通控件(在 Widget 蓝图里放一个 Orion WebUI)、CommonUI 页面(父类选 Orion WebUI Activatable Widget),或多个页面共用一个浏览器的 InstantScreen。第一次接入请用纯蓝图加普通控件。
本教程用到的合同
沿用教程 1 和教程 4 的设置面板。AppId 为 SettingsPanel,生产文件位于工程 Content/。
| 方向 | 名称 | 参数 | 说明 |
|---|---|---|---|
| Unreal → 网页 | ue:settingsPanel.state |
{ stateRevision, volume, quality, culture } |
完整状态 |
| 网页 → Unreal(emit) | settingsPanel.ready |
{} |
网页开始监听 |
| 网页 → Unreal(emit) | settingsPanel.setVolume |
{ volume: 0–100 } |
调音量 |
| 网页 → Unreal(emit) | settingsPanel.setQuality |
{ quality: "low" | "medium" | "high" } |
选画质 |
| 网页 → Unreal(call) | settingsPanel.resetDefaults |
{} |
还原默认,需要结果 |
| 网页 → Unreal(emit) | settingsPanel.close |
{} |
关闭面板 |
网页用 emit 发来的消息到达 On Web Event;用 call 发来的到达 On Web Request,必须应答。
- StateRevision
- 0
- Volume
- 80
- Quality
- high
- Culture
Settings
纯蓝图:从零接入
1. 建 App Definition
- 内容浏览器里右键,选择 Miscellaneous → Data Asset,类选
OrionWebUIAppDefinition(搜索Orion),命名为DA_SettingsPanel。 - 打开它,设置:
| 属性 | 值 | 说明 |
|---|---|---|
AppId |
SettingsPanel |
必须与目录名、网页 package.json 里的 orionWebUI.appId 完全一致 |
EntryHtml |
index.html |
默认值 |
| Use Dev Server in Editor | 取消勾选 | 勾选时 Designer 会优先连接本机开发服务器 |
DA_SettingsPanel 的资产编辑器。只改了两处:App Id 填 SettingsPanel,取消勾选 Use Dev Server in Editor。改过的属性右侧出现“还原默认值”的箭头。其余属性保持默认,常用属性见教程 4。
2. 建承载页面的 Widget
- 新建 User Widget 蓝图
WBP_SettingsPanel。 - 在 Designer 的控件面板搜索 Orion WebUI 并拖入,命名为
WebUI,勾选 Is Variable,让它铺满父级。 - 在 Details 里把 App Definition 设为
DA_SettingsPanel。Designer 里会直接显示页面。
WBP_SettingsPanel 的 Designer。层级里只有一个 WebUI:把 Orion WebUI 拖进空的层级时它就是根控件,自然占满整个 Widget;放进 Canvas Panel 之类的容器时才需要再设置铺满。Helper 就绪后,画布上直接画出 dist 里的页面。WebUI 后的 Details:名字是 WebUI,Is Variable 已勾选,App Definition 指向上一步的 DA_SettingsPanel。其余保持默认。3. 显示出来
在 Player Controller(或你管理界面的蓝图)里:
- Create Widget(Class 选
WBP_SettingsPanel,Owning Player 接Self),把返回值保存为变量。 - Add to Viewport。
- Set Input Mode Game And UI:Player Controller 接
Self(空着时节点不起作用),In Widget to Focus 接这个 Widget 里的WebUI,键盘输入才会进入网页。 - Set Show Mouse Cursor 设为
true。
BP_PlayerController 的事件图:Create Widget(Owning Player 接 Self),把返回值存进变量 SettingsPanel,Add to Viewport,再用 Set Input Mode Game And UI 把焦点交给这个 Widget 里的 WebUI,最后显示鼠标。Set Input Mode Game And UI 的 Player Controller 引脚也要接 Self,空着不会生效。这里从 BeginPlay 开始;换成你要显示界面的时机即可,但同一个界面只创建一次。图比版面宽,可以左右滚动;点击放大。关闭时反过来:Remove from Parent,Set Input Mode Game Only(Player Controller 同样要接;在 Widget 蓝图里接 Get Owning Player),隐藏鼠标。同一个界面只创建一次。
BP_PlayerController 就是上图的蓝图(由一个把它设为默认 Player Controller 的 Game Mode 带出来)。4. 接收操作:On Web Event
在 Designer 里选中 WebUI,在 Details 底部的 Events 里点击 On Web Event 右边的加号。事件给出 Event Name 和 Payload Json。接一个 Switch on Name,选中它,在 Details 的 Pin Names 里为每个事件名添加一个输出。节点上的引脚名会显示成带空格的样子(Settings Panel.set Volume),比较时仍用你填的名字。
WebUI,在 Details 的搜索框输入 Events 后看到的 Events 分区。点事件右边的加号,事件图里就多出对应的节点;已经添加过的事件(这里是 On Web Event 和 On Web Request)按钮换成了“跳到该节点”。Settings Panel.set Volume),实际比较的仍是你填的 settingsPanel.setVolume。Default 不接业务逻辑:没有登记的事件名一律不处理。下面几张图是各条输出后面的节点。settingsPanel.ready:调用下一节的 PublishState。
settingsPanel.setVolume:
- Get Json Number:
Json接Payload Json,Field填volume。 - Branch 判断
Valid,为false时不处理。 - Branch 判断数值在 0 到 100 之间。
- 通过后:Round 成整数,存入变量
Volume,应用到游戏的音量设置,调用PublishState。 - 没通过也调用
PublishState,纠正网页上的显示。
settingsPanel.setVolume 这条输出:Get Json Number 读 volume,第一个 Branch 判断 Valid,第二个判断 In Range (Float)(0 到 100);都通过后把 Round 的结果存入 Volume,再调用 Publish State。两个 False 经下方的转接点(Reroute)也接到 Publish State:把正确的状态推回去,网页上的显示就被纠正。图比版面宽,可以左右滚动;点击放大。settingsPanel.setQuality:用 Get Json String 读 quality,检查 Valid,再用 Switch on String 只接受 low、medium、high,通过后存入变量并调用 PublishState。
settingsPanel.setQuality 这条输出:Get Json String 读 quality,Branch 判断 Valid,Switch on String 只放行 low、medium、high,通过后存入 Quality 并发布。False 和 Default 同样接到 Publish State。图比版面宽,可以左右滚动;点击放大。settingsPanel.close:执行第 3 步里的关闭流程。
settingsPanel.close 这条输出:Remove from Parent,再对 Get Owning Player 得到的 Player Controller 调用 Set Input Mode Game Only,并隐藏鼠标。图比版面宽,可以左右滚动;点击放大。Default 输出不要接业务逻辑。插件自己的通知(例如 __orion.inputReady)也会到这里,让它们落到 Default 即可。
5. 发布状态
新建函数 PublishState,变量 StateRevision(Integer64)、Volume(Integer)、Quality(String)、Culture(String):
WBP_SettingsPanel 的 My Blueprint 面板:函数 PublishState 和这一节的四个变量。WebUI 是勾选 Is Variable 后自动出现的;bResetAllowed 和 ControlSounds 分别在下一节和“控件音效”里用到。StateRevision加一。- Set Json Number:
Json填{},Field填stateRevision,Value接StateRevision。 - 把返回值接到下一个 Set Json Number 的
Json,Field填volume。 - 同样接两个 Set Json String,写入
quality和culture。 - 对
WebUI调用 Post Retained Latest Event to Web:Event Name填ue:settingsPanel.state,Payload Json接最后一个节点的返回值。
PublishState:StateRevision 加一后存回;四个 Set Json 节点从 {} 开始依次写入 stateRevision、volume、quality、culture,最后一个返回值交给 Post Retained Latest Event to Web。Set Json 是纯函数节点,没有执行引脚,只靠数据线串起来。整数接到 Value 时,编辑器自动插入了转成浮点数的小节点。图比版面宽,可以左右滚动;点击放大。每次都发完整状态,stateRevision 只增不减。页面重新加载后会再发 settingsPanel.ready,收到时一定要重新发布。
Volume 和 Quality,再由 PublishState 把整份状态发回来,页面照着显示。6. 需要结果的请求:On Web Request
为 WebUI 添加 On Web Request 事件,它给出 Request 和 Response。用 Break 拆开 Request(点击节点底部的箭头展开),取 Name 和 Payload Json,再接 Switch on Name:
Request 取 Name,同样接 Switch on Name。Break 节点默认只列出前几个成员,Payload Json 等在节点底部的展开箭头里。Response 的蓝色连线沿着图的底部通到每个应答节点。图比版面宽,可以左右滚动;点击放大。settingsPanel.resetDefaults:
- 允许时:把变量改回默认值并应用,调用
PublishState,然后对Response调用 Resolve Json,Data Json填{}。 - 不允许时:对
Response调用 Reject,Error Code填你自己的错误码(例如E_LOCKED),Error Message填给玩家看的原因。
Default 输出:对 Response 调用 Reject,错误码写 E_UNKNOWN_REQUEST。
settingsPanel.resetDefaults 这条输出:Branch 判断你自己的条件(这里是变量 bResetAllowed)。允许时把 Volume、Quality 改回默认值,Publish State,然后 Resolve Json;不允许时 Reject,错误码 E_LOCKED。Default 输出接另一个 Reject(E_UNKNOWN_REQUEST):否则没人应答的请求会被自动以 {} 成功应答。每个请求只应答一次。图比版面宽,可以左右滚动;点击放大。规则:
- 每个请求只应答一次。Unreal 确实接受了操作之后才 Resolve Json。
- 结果要等一会儿才有(例如等存档写完)时,先调用 Defer 并把
Response存成变量,之后再应答。超过 App Definition 的BridgeCallTimeoutSeconds(默认 15 秒)没有应答,网页会收到E_TIMEOUT。 - 既没应答也没 Defer 的请求,默认会自动以
{}成功应答(WebUI的 Auto Resolve Unhandled Requests),所以 Default 输出一定要显式 Reject。
7. 就绪与错误
- On Web Ready:页面与 Unreal 的通信建立,页面重新加载后会再次触发。可以在这里发布首个状态。
- On Web Error:通信出错、导航被拦截等。调试阶段接到 Print String,排查时最先看它。
8. 显示状态与清理
页面就绪后会自己进入可见状态。纯蓝图的界面如果调用 Set Presentation Lifecycle State,只用 Visible,Presentation Revision 填 0。要暂时隐藏界面,改 Widget 的可见性或把它移除。
Widget 销毁时在 Event Destruct 里清理你用过的东西:
- Deactivate Control Sound Policy(
Policy Owner接Self) - Clear Native Surfaces、Release Texture Resources
- WorldUI 的 Unbind Overlay Widget
- 你自己设置的定时器
蓝图里的 JSON
Get Json String / Number / Boolean 读取顶层的一个字段,字段缺失或类型不符时 Valid 为 false;Set Json String / Number / Boolean 写入一个字段并返回新的 JSON。它们只处理顶层字段,并会正确转义引号和换行,不要用 Append 手工拼接玩家输入的文字。
需要数组或嵌套对象时:摊平成顶层字段(例如 slot0Name、slot0Count、slotCount);或启用引擎自带的 Json Blueprint Utilities 插件构造对象,转成字符串再发布;或把这部分状态放进 C++。
蓝图里的常用功能
控件音效
悬停和点击的声音由 Unreal 播放。网页上每个可点击元素有一个控件 Id(data-orion-control-id),蓝图里的策略决定播什么。
- 在承载页面的 Widget 蓝图里新建变量
ControlSounds,类型OrionWebUIControlSoundPolicy,编译。 - 在变量默认值里填写:
| 字段 | 含义 |
|---|---|
| Default Sounds → Hover Sound / Click Sound | 所有控件的默认声音,每一项有 Sound、Volume Multiplier、Enabled 等 |
| Control Overrides | 按控件 Id 覆盖。填 Control Id,勾选 Override Hover Sound 或 Override Click Sound 后填对应的声音 |
ControlSounds 的默认值(在 My Blueprint 里选中变量后,Details 的 Default Value)。Default Sounds 是所有控件的默认声音,这里用的是插件示例自带的 S_StationHover 和 S_StationClick;Control Overrides 按控件 Id 覆盖。图中这一条让 settings-close 的点击不出声:勾选 Override Click Sound,再取消它下面 Click Sound 的 Enabled。- Event Construct:对
WebUI调用 Activate Control Sound Policy,Policy Owner接Self,Context Id留空,Policy接ControlSounds。 - Event Destruct:对
WebUI调用 Deactivate Control Sound Policy,Policy Owner接Self。
Context Id 留空(None);Policy 接变量 ControlSounds。要点:
- 让某个控件不出声:加一条覆盖,勾选对应的 Override,再取消该项的 Enabled。
- 换策略(例如音量变了):对同一个
Policy Owner再调用一次 Activate Control Sound Policy。 - 页面里的区域要用另一份策略时,网页在区域根元素上写
data-orion-sound-context="Shop",蓝图换一个Policy Owner,以Shop为Context Id再激活一份。 - 核对 Id:在 Designer 里选中
WebUI,Design Time Control Id Preview Mode 选 All Controls。
有业务含义的声音
“保存成功”“余额不足”这类声音由网页在合适的时机请求,声音资产在 Unreal 里配置:
- 新建
OrionWebUISoundManifest数据资产,在 Entries 里登记StableId(例如settings.saved)和Sound。 - 把它指定给 App Definition 的 Sound Manifest。
- 网页调用
api.playSound("settings.saved")。蓝图也可以对WebUI调用 Play Sound by Id。
data-orion-control-id="settings-reset"api.playSound("settings.saved")playSound 播一遍。文字与语言
最简单的做法是蓝图在状态里发布 culture,网页用自己的双语字典显示,随附示例就是这样。要走 Unreal 的本地化流程,就新建 OrionWebUITextCatalog 数据资产,在 Texts 里登记 Key(例如 settingsPanel.title)对应的 Unreal 文本,指定给 App Definition 的 Text Catalog,网页用 Key 取字。
运行时图片
把一张 Texture2D(头像、运行时生成的图标)交给网页显示:
- 对
WebUI调用 Request Texture Resource:Stable Id填稳定的名字(例如player.avatar),Texture接贴图,Options用 Make 节点构造,Max Output Dimension按显示尺寸填写。 - 绑定
WebUI的 On Texture Resource Completed。Status为 Ready 时,把Handle里的Url写进状态并发布,网页把它用在图片上。 - 不再需要时调用 Release Texture Resource。
同一个 Stable Id 再次请求会得到新地址,网页只用最新的地址。像素原地变化时增大 Options 里的 Source Revision。不支持虚拟纹理。
实时画面:Native Surface
把 Render Target、场景捕获或 UI 材质直接画在网页的某个位置上:
- 网页在目标位置放一个占位元素,并用一个稳定的名字绑定它(例如
station.capture)。 - 蓝图对
WebUI调用 Set Native Surface Texture(Surface Id填同一个名字,Texture接 Render Target),或 Set Native Surface Material。位置和大小跟随占位元素。 - 结束时调用 Clear Native Surface。
另有 Set Native Surface Tint(着色与透明度)、Set Native Surface Composition Layer(默认 Above Browser;Below Browser 画在网页下方,需要 App Definition 开启透明),以及 On Native Surface Layout Changed(Visible 为 false 时暂停场景捕获)。原生画面不接收鼠标,圆角和遮罩放进 UI 材质里做。
WorldUI:跟随 Actor 的标签与交互
每个本地玩家用一个网页承载所有世界标签,Actor 上只挂数据组件。
- 新建
OrionWebUIWorldElementDefinition数据资产,按需设置:ElementType(网页据此选择渲染方式)、MaxVisibleDistance(0表示不按距离隐藏)、bCheckOcclusion、bClampToViewport、bScaleWithDistance;要能点击,开启bInteractive,在AllowedActions里填动作名(例如collect),MaxInteractionDistance默认 300。 - 给 Actor 添加组件(搜索
World Element,类为OrionWebUIWorldElementComponent):指定 Definition,填 Element Key。 - 用 Set Payload Json 提供文字和状态,默认的网页渲染器读取
label和actionLabel。用 Set Element Visible 显示或隐藏。 - 界面创建后,取得本地玩家子系统
OrionWebUIWorldSubsystem,调用 Bind Overlay Widget,传入WebUI。 - 绑定组件的 On Interaction(给出
Player、Action Id、Payload Json),判断游戏规则是否允许,再修改状态。联网游戏要再发一次经过校验的服务器请求。 - 界面销毁时调用 Unbind Overlay Widget。
远程网站
外部网站用单独的 Orion Browser 控件,它没有游戏通信接口,不要和 Orion WebUI 混用。用 Initial URL 或 Load URL 打开地址,Go Back / Go Forward / Reload / Stop Load 导航,On Load Error 显示失败画面,参考示例的 WBP_OrionWebsite。
用 CommonUI 承载,不写 C++
- 新建 Widget 蓝图时父类选 Orion WebUI Activatable Widget。
- 放入 Orion WebUI 子控件,名字必须是
WebUI。 - 在 Class Defaults 里设置 App Definition、Control Sound Policy(不需要再手动激活)、Input Config(默认
Menu),以及可选的 Input Manifest。 - 在事件图里重写(Override)
HandleWebUIEvent、HandleWebUIRequest、HandleWebUIReady,逻辑与前面的 On Web Event、On Web Request 相同。 - 把它推入你的 CommonUI 页面栈。
Input Manifest 里的动作触发后,向网页发 ue:commonAction 并调用 HandleCommonAction;网页已经为这个动作提交操作时,蓝图不要再执行一遍。重写 HandleWebUIBackAction 并返回 true 表示已处理返回键;没被处理时,Deactivate on Back Action 决定是否关闭页面。
蓝图节点速查
以下节点都在 WebUI(Orion WebUI)上。
| 分类 | 节点 |
|---|---|
| 加载 | Load App、Reload App、Set Initial Route Override、Get Current Route |
| 发消息 | Post Retained Latest Event to Web(之后开始监听的页面也能收到最新一条)、Post Event to Web(页面没在监听就丢失) |
| 请求应答 | Response 上的 Resolve Json、Reject、Defer、Has Responded |
| 调用网页 | Call Web,结果从 On Web Call Completed 回来;只用来查询显示层面的信息 |
| 声音、图片、原生画面 | Play Control Sound、Play Sound by Id、Request Texture Resource、Set Native Surface Texture 等,见上文各节 |
| 进阶送达 | Post Flow Controlled Latest Event to Web 等,见 C++ 的发布通道 |
C++:从零接入
模块依赖
插件的浏览器模块只支持 Win64,不参与 Server 构建。把承载界面的代码放在只为客户端构建的模块里:
// MyGameUI.Build.cs
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "UMG", "OrionWebUI", "OrionWebUIWidget" });
PrivateDependencyModuleNames.AddRange(new string[] { "Json" });
// 使用 CommonUI 页面时再加:"CommonUI", "OrionWebUICommonUI"
// 使用 WorldUI 时再加:"OrionWebUIWorld"(组件,Server 可用)、"OrionWebUIWorldWidget"(子系统)工程有 Server Target 时,在 .uproject 或 .uplugin 的模块描述里给这个模块加上 "TargetDenyList": ["Server"] 和 "PlatformAllowList": ["Win64"]。
普通控件:UUserWidget 加 BindWidget
Widget 蓝图以这个类为父类,Designer 里放一个名为 WebUI 的 Orion WebUI。
// 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);
// 在这里把音量应用到游戏。
}
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);
}应答规则与蓝图相同;要异步应答,先 Response->Defer() 并保存指针。FName 比较不区分大小写而网页区分,事件名在整个工程里只用一种大小写写法。不想自己解析 JSON 时,可以绑定原生委托 OnWebEventParsed(AddUObject),它直接给出 TSharedPtr<FJsonObject>。
CommonUI 页面
页面进入 CommonUI 页面栈时,派生 UOrionWebUIActivatableWidget,基类负责加载 App、声音策略、输入动作和返回键。Widget 蓝图里放一个名为 WebUI 的 Orion WebUI 子控件,在 Class Defaults 里配置 AppDefinition、InputManifest、ControlSoundPolicy、InputConfig;页面只通过你的 CommonUI 页面栈推入,不要直接 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(节选)
void USettingsPanelScreen::NativeOnActivated()
{
Super::NativeOnActivated();
PublishState(true); // 停用期间状态可能变过,按当前数据重新发布
}
void USettingsPanelScreen::HandleWebUIReady_Implementation()
{
Super::HandleWebUIReady_Implementation();
PublishState(true);
}
void USettingsPanelScreen::HandleWebUIEvent_Implementation(FName EventName, const FString& PayloadJson)
{
if (EventName == StandardPresentationReadyEventName || EventName == StandardPresentationAppliedEventName)
{
return;
}
// 之后与普通控件的 HandleWebEvent 相同;用 GetWebUIWidget() 取得控件
}
bool USettingsPanelScreen::HandleWebUIBackAction_Implementation()
{
return false; // 返回 true 表示自己处理了返回键,不执行默认关闭
}发布状态的四种通道
| 函数 | 何时用 |
|---|---|
PostRetainedLatestEventToWeb |
一般状态,默认选择 |
PostFlowControlledLatestEventToWeb(Event, Json, AckEvent, RevisionField, Revision) |
每帧都可能变化的数据 |
PostReliableRetainedLatestEventToWeb(..., RetryIntervalSeconds, MaxAttempts) |
必须送达的快照 |
PostReliablePresentationEventToWeb(...) |
进入画面时不想看到半成品;CommonUI 基类已经封装 |
版本号用 UOrionWebUIWidget::AllocatePresentationRevision() 分配,它和业务状态的 stateRevision 是两套编号;内容没变就不要分配新版本。网页的回执只表示送达或显示,不代表业务成功。
资源接口
C++ 里用 RequestTextureResource(StableId, Texture, Options)(完成时触发 OnTextureResourceCompleted)、SetNativeSurfaceTexture、SetNativeSurfaceMaterial。已有 CPU 像素时用只有 C++ 可用的 RequestPixelResource(StableId, FOrionWebUIPixelSource, Options);材质用到了临时资源时用 SetNativeSurfaceMaterialWithDependencies(SurfaceId, Material, Dependencies)。更换 Render Target 时新建动态材质实例再调用 SetNativeSurfaceMaterial,不要修改仍在绑定中的实例。
WorldUI
#include "OrionWebUIWorldSubsystem.h"
if (ULocalPlayer* LocalPlayer = GetOwningLocalPlayer())
{
if (UOrionWebUIWorldSubsystem* WorldUI = LocalPlayer->GetSubsystem<UOrionWebUIWorldSubsystem>())
{
WorldUI->BindOverlayWidget(WebUI);
}
}组件的 OnInteraction 是动态多播委托 (APlayerController* Player, FName ActionId, const FString& PayloadJson)。只属于某个本地玩家的标签用组件的 SetOwningLocalPlayer。
C++ 基类加蓝图子类
把不能被绕过的校验写在 C++ 里,把设计师要调的留给蓝图:
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;
};- 校验通过后才调用
BlueprintNativeEvent,蓝图重写无法跳过校验。 - 每个固定按钮一个事件和一个启用开关,开关随完整状态发布给网页。
- 声音策略、App Definition、Input Manifest 作为
EditDefaultsOnly属性,在蓝图子类的 Class Defaults 里配置。 - 不要改名、改签名或删除蓝图已经重写的事件。
InstantScreen 概览
InstantScreen 让同一层的多个界面共用一个常驻浏览器,界面在状态、资源和首帧都就绪后才显示。单个面板用普通控件就够了。它需要网页里的 instant-screen.config.ts、OrionInstantScreenDefinition 等资产、根布局里的 Orion InstantScreen Runtime Host,以及父类派生自 UOrionInstantScreenActivatableWidget 的界面,做法见随插件分发的 InstantScreen 参考。
排查
| 现象 | 检查 |
|---|---|
| 页面空白 | AppId、目录名、orionWebUI.appId 是否一致;dist/index.html 是否存在;看 On Web Error |
Designer 显示 Orion WebUI dist entry is missing |
没有构建,或 AppId 写错 |
| 点击没反应 | On Web Event 是否绑定在 WebUI 上;Pin Names 里的事件名是否逐字一致;Valid 是否为 false |
| 网页一直停在旧数值 | 是否每次变化后都发布;stateRevision 是否递增 |
| 请求一直等到超时 | 调用了 Defer 却没有应答 |
| 请求总是“成功” | Default 分支没有 Reject |
| 键盘、手柄没反应 | 视口是否为 CommonGameViewportClient(见安装);Set Input Mode Game And UI 的 Player Controller 是否接了,焦点是否给了 WebUI |
| 调用 Set Presentation Lifecycle State 后页面消失 | 纯蓝图的页面只用 Visible,去掉其他状态的调用 |
| 没有声音 | 是否激活了策略;控件有没有 Id;Context Id 是否对得上 |
| 运行时图片不显示 | 是否等到 Ready 才把地址给网页;是否是虚拟纹理 |
| 打包后页面空白 | 先构建 dist 再打包,见打包与故障排查 |