Orion OrionBrowser使用手册
中文EN

教程 3:蓝图与 C++ 接入

网页写好之后,Unreal 这一侧要做四件事:承载页面、接收操作、发布状态、提供声音与资源。可以全部用蓝图,也可以用 C++,或者由 C++ 写基类、蓝图做子类。

本页目录

网页由 AI 来写见教程 1,人工编写见教程 4。

游戏状态、规则和存档属于 Unreal,网页只显示状态并提交操作。 网页发来的每个参数都要在 Unreal 里重新检查。

开始之前:下载依赖

点击关卡编辑器工具栏上的 WebUIStudio 按钮,在“OrionBrowser 依赖”窗口里点“下载”。没有依赖时界面只显示一行提示,工程也不能打包。已经下载过就跳过,详见安装。

先选路线

状态只是若干个数字、文字和开关时用纯蓝图;状态里有数组和嵌套结构、需要高频数据或可靠送达时用 C++;程序定校验、设计师在蓝图里配声音和资产时用 C++ 基类加蓝图子类。

承载页面可以用普通控件(在 Widget 蓝图里放一个 Orion WebUI)、CommonUI 页面(父类选 Orion WebUI Activatable Widget),或多个页面共用一个浏览器的 InstantScreen。第一次接入请用纯蓝图加普通控件。

本教程用到的合同

沿用教程 1 和教程 4 的设置面板。AppId 为 SettingsPanel,生产文件位于工程 Content/UI/WebUI/SettingsPanel/dist。

方向 名称 参数 说明
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,必须应答。

Unreal蓝图持有状态,校验每个操作
StateRevision
0
Volume
80
Quality
high
Culture
网页显示状态,提交操作

Settings

Volume0
Quality

游戏画面(示意)
    图 1可以动手试:拖动滑块、切换画质或语言。网页只提交操作,Unreal 校验后把结果发回网页;勾上左侧的规则再试,被拒绝的数值会自动恢复。

    纯蓝图:从零接入

    1. 建 App Definition

    1. 内容浏览器里右键,选择 Miscellaneous → Data Asset,类选 OrionWebUIAppDefinition(搜索 Orion),命名为 DA_SettingsPanel。
    2. 打开它,设置:
    属性 值 说明
    AppId SettingsPanel 必须与目录名、网页 package.json 里的 orionWebUI.appId 完全一致
    EntryHtml index.html 默认值
    Use Dev Server in Editor 取消勾选 勾选时 Designer 会优先连接本机开发服务器
    图 2DA_SettingsPanel 的资产编辑器。只改了两处:App Id 填 SettingsPanel,取消勾选 Use Dev Server in Editor。改过的属性右侧出现“还原默认值”的箭头。

    其余属性保持默认,常用属性见教程 4。

    2. 建承载页面的 Widget

    1. 新建 User Widget 蓝图 WBP_SettingsPanel。
    2. 在 Designer 的控件面板搜索 Orion WebUI 并拖入,命名为 WebUI,勾选 Is Variable,让它铺满父级。
    3. 在 Details 里把 App Definition 设为 DA_SettingsPanel。Designer 里会直接显示页面。
    图 3WBP_SettingsPanel 的 Designer。层级里只有一个 WebUI:把 Orion WebUI 拖进空的层级时它就是根控件,自然占满整个 Widget;放进 Canvas Panel 之类的容器时才需要再设置铺满。Helper 就绪后,画布上直接画出 dist 里的页面。
    图 4选中 WebUI 后的 Details:名字是 WebUI,Is Variable 已勾选,App Definition 指向上一步的 DA_SettingsPanel。其余保持默认。

    3. 显示出来

    在 Player Controller(或你管理界面的蓝图)里:

    1. Create Widget(Class 选 WBP_SettingsPanel,Owning Player 接 Self),把返回值保存为变量。
    2. Add to Viewport。
    3. Set Input Mode Game And UI:Player Controller 接 Self(空着时节点不起作用),In Widget to Focus 接这个 Widget 里的 WebUI,键盘输入才会进入网页。
    4. Set Show Mouse Cursor 设为 true。
    图 5BP_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),隐藏鼠标。同一个界面只创建一次。

    图 6在编辑器里运行(PIE)的结果:设置面板叠在关卡画面上,页面没有内容的地方是透明的。右侧大纲里是这次运行生成的对象,其中的 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),比较时仍用你填的名字。

    图 7Designer 里选中 WebUI,在 Details 的搜索框输入 Events 后看到的 Events 分区。点事件右边的加号,事件图里就多出对应的节点;已经添加过的事件(这里是 On Web Event 和 On Web Request)按钮换成了“跳到该节点”。
    图 8On Web Event (WebUI) 接 Switch on Name,每个事件名一个输出。编辑器把引脚名显示成带空格的样子(Settings Panel.set Volume),实际比较的仍是你填的 settingsPanel.setVolume。Default 不接业务逻辑:没有登记的事件名一律不处理。下面几张图是各条输出后面的节点。

    settingsPanel.ready:调用下一节的 PublishState。

    settingsPanel.setVolume:

    1. Get Json Number:Json 接 Payload Json,Field 填 volume。
    2. Branch 判断 Valid,为 false 时不处理。
    3. Branch 判断数值在 0 到 100 之间。
    4. 通过后:Round 成整数,存入变量 Volume,应用到游戏的音量设置,调用 PublishState。
    5. 没通过也调用 PublishState,纠正网页上的显示。
    图 9settingsPanel.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。

    图 10settingsPanel.setQuality 这条输出:Get Json String 读 quality,Branch 判断 Valid,Switch on String 只放行 low、medium、high,通过后存入 Quality 并发布。False 和 Default 同样接到 Publish State。图比版面宽,可以左右滚动;点击放大。

    settingsPanel.close:执行第 3 步里的关闭流程。

    图 11settingsPanel.close 这条输出:Remove from Parent,再对 Get Owning Player 得到的 Player Controller 调用 Set Input Mode Game Only,并隐藏鼠标。图比版面宽,可以左右滚动;点击放大。

    Default 输出不要接业务逻辑。插件自己的通知(例如 __orion.inputReady)也会到这里,让它们落到 Default 即可。

    图 12整张 On Web Event 图(缩小到 50%):一个事件、一个 Switch on Name,后面是上面几张图里的四条输出。

    5. 发布状态

    新建函数 PublishState,变量 StateRevision(Integer64)、Volume(Integer)、Quality(String)、Culture(String):

    图 13WBP_SettingsPanel 的 My Blueprint 面板:函数 PublishState 和这一节的四个变量。WebUI 是勾选 Is Variable 后自动出现的;bResetAllowed 和 ControlSounds 分别在下一节和“控件音效”里用到。
    1. StateRevision 加一。
    2. Set Json Number:Json 填 {},Field 填 stateRevision,Value 接 StateRevision。
    3. 把返回值接到下一个 Set Json Number 的 Json,Field 填 volume。
    4. 同样接两个 Set Json String,写入 quality 和 culture。
    5. 对 WebUI 调用 Post Retained Latest Event to Web:Event Name 填 ue:settingsPanel.state,Payload Json 接最后一个节点的返回值。
    图 14PublishState:StateRevision 加一后存回;四个 Set Json 节点从 {} 开始依次写入 stateRevision、volume、quality、culture,最后一个返回值交给 Post Retained Latest Event to Web。Set Json 是纯函数节点,没有执行引脚,只靠数据线串起来。整数接到 Value 时,编辑器自动插入了转成浮点数的小节点。图比版面宽,可以左右滚动;点击放大。

    每次都发完整状态,stateRevision 只增不减。页面重新加载后会再发 settingsPanel.ready,收到时一定要重新发布。

    图 15运行中的面板,在页面上把音量调到 35、画质选了 Low 之后。这两个值不是页面自己记下的:蓝图校验后改了 Volume 和 Quality,再由 PublishState 把整份状态发回来,页面照着显示。

    6. 需要结果的请求:On Web Request

    为 WebUI 添加 On Web Request 事件,它给出 Request 和 Response。用 Break 拆开 Request(点击节点底部的箭头展开),取 Name 和 Payload Json,再接 Switch on Name:

    图 16On Web Request (WebUI):用 Break 拆开 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。

    图 17settingsPanel.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),蓝图里的策略决定播什么。

    1. 在承载页面的 Widget 蓝图里新建变量 ControlSounds,类型 OrionWebUIControlSoundPolicy,编译。
    2. 在变量默认值里填写:
    字段 含义
    Default Sounds → Hover Sound / Click Sound 所有控件的默认声音,每一项有 Sound、Volume Multiplier、Enabled 等
    Control Overrides 按控件 Id 覆盖。填 Control Id,勾选 Override Hover Sound 或 Override Click Sound 后填对应的声音
    图 18变量 ControlSounds 的默认值(在 My Blueprint 里选中变量后,Details 的 Default Value)。Default Sounds 是所有控件的默认声音,这里用的是插件示例自带的 S_StationHover 和 S_StationClick;Control Overrides 按控件 Id 覆盖。图中这一条让 settings-close 的点击不出声:勾选 Override Click Sound,再取消它下面 Click Sound 的 Enabled。
    1. Event Construct:对 WebUI 调用 Activate Control Sound Policy,Policy Owner 接 Self,Context Id 留空,Policy 接 ControlSounds。
    2. Event Destruct:对 WebUI 调用 Deactivate Control Sound Policy,Policy Owner 接 Self。
    图 19Event Construct 里激活策略,Event Destruct 里对称地注销。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 里配置:

    1. 新建 OrionWebUISoundManifest 数据资产,在 Entries 里登记 StableId(例如 settings.saved)和 Sound。
    2. 把它指定给 App Definition 的 Sound Manifest。
    3. 网页调用 api.playSound("settings.saved")。蓝图也可以对 WebUI 调用 Play Sound by Id。
    控件音效:悬停与点击,网页不写代码
    页面上的控件data-orion-control-id="settings-reset"
    插件自动上报控件 Id
    控件音效策略默认声音,加按 Id 的覆盖
    Unreal 播放声音资产在蓝图里选
    业务声音:有含义的时刻,由网页请求
    页面调用api.playSound("settings.saved")
    声音 Id
    Sound ManifestApp Definition 上登记的 StableId
    Unreal 播放清单里没有就拒绝
    图 20两条声音通道互不叠加:已经有自动点击声的按钮,不要再用 playSound 播一遍。

    文字与语言

    最简单的做法是蓝图在状态里发布 culture,网页用自己的双语字典显示,随附示例就是这样。要走 Unreal 的本地化流程,就新建 OrionWebUITextCatalog 数据资产,在 Texts 里登记 Key(例如 settingsPanel.title)对应的 Unreal 文本,指定给 App Definition 的 Text Catalog,网页用 Key 取字。

    运行时图片

    把一张 Texture2D(头像、运行时生成的图标)交给网页显示:

    1. 对 WebUI 调用 Request Texture Resource:Stable Id 填稳定的名字(例如 player.avatar),Texture 接贴图,Options 用 Make 节点构造,Max Output Dimension 按显示尺寸填写。
    2. 绑定 WebUI 的 On Texture Resource Completed。Status 为 Ready 时,把 Handle 里的 Url 写进状态并发布,网页把它用在图片上。
    3. 不再需要时调用 Release Texture Resource。

    同一个 Stable Id 再次请求会得到新地址,网页只用最新的地址。像素原地变化时增大 Options 里的 Source Revision。不支持虚拟纹理。

    实时画面:Native Surface

    把 Render Target、场景捕获或 UI 材质直接画在网页的某个位置上:

    1. 网页在目标位置放一个占位元素,并用一个稳定的名字绑定它(例如 station.capture)。
    2. 蓝图对 WebUI 调用 Set Native Surface Texture(Surface Id 填同一个名字,Texture 接 Render Target),或 Set Native Surface Material。位置和大小跟随占位元素。
    3. 结束时调用 Clear Native Surface。
    屏幕上的 Orion WebUI 控件网页边框、标题、按钮由网页绘制Unreal 的画面Render Target 或材质占位元素<OrionNativeSurface surface-id="station.capture" />位置和大小跟随这个元素蓝图Set Native Surface TextureSurface Id =station.capture
    图 21Native Surface:Unreal 把画面直接画在占位元素的位置上,像素不经过网页。默认画在网页上方,不接收鼠标;圆角、遮罩和淡出放进 UI 材质里做。

    另有 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 上只挂数据组件。

    关卡里的 Actor能量单元OrionWebUIWorldElementComponentDefinition · Element Key · Payload Json终端OrionWebUIWorldElementComponentDefinition · Element Key · Payload Json每个本地玩家一个网页<OrionWorldOverlayDOM />能量单元 [收集]终端 [提交]标签跟着 Actor 在屏幕上移动位置、可见性、数据点击动作检查后触发 On Interaction
    图 22WorldUI:Actor 上只挂数据组件,所有世界标签画在同一个网页里。点击标签上的按钮,触发组件的 On Interaction。
    1. 新建 OrionWebUIWorldElementDefinition 数据资产,按需设置:ElementType(网页据此选择渲染方式)、MaxVisibleDistance(0 表示不按距离隐藏)、bCheckOcclusion、bClampToViewport、bScaleWithDistance;要能点击,开启 bInteractive,在 AllowedActions 里填动作名(例如 collect),MaxInteractionDistance 默认 300。
    2. 给 Actor 添加组件(搜索 World Element,类为 OrionWebUIWorldElementComponent):指定 Definition,填 Element Key。
    3. 用 Set Payload Json 提供文字和状态,默认的网页渲染器读取 label 和 actionLabel。用 Set Element Visible 显示或隐藏。
    4. 界面创建后,取得本地玩家子系统 OrionWebUIWorldSubsystem,调用 Bind Overlay Widget,传入 WebUI。
    5. 绑定组件的 On Interaction(给出 Player、Action Id、Payload Json),判断游戏规则是否允许,再修改状态。联网游戏要再发一次经过校验的服务器请求。
    6. 界面销毁时调用 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++

    1. 新建 Widget 蓝图时父类选 Orion WebUI Activatable Widget。
    2. 放入 Orion WebUI 子控件,名字必须是 WebUI。
    3. 在 Class Defaults 里设置 App Definition、Control Sound Policy(不需要再手动激活)、Input Config(默认 Menu),以及可选的 Input Manifest。
    4. 在事件图里重写(Override)HandleWebUIEvent、HandleWebUIRequest、HandleWebUIReady,逻辑与前面的 On Web Event、On Web Request 相同。
    5. 把它推入你的 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.csC#
    // 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.hC++
    // 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.cppC++
    // 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.hC++
    // 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.cppC++
    // 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

    C++
    #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++ 里,把设计师要调的留给蓝图:

    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 再打包,见打包与故障排查

    输入两个以上字符开始搜索

    联系我们

    使用中遇到问题,或者有购买、合作方面的事,欢迎发邮件给我们。