
简介UE5引擎中集成网页浏览能力常受限于内置浏览器方案而Edge WebView2 Runtime提供了现代且稳定的内核支持。这份源码基于该组件实现了一套完整的在线浏览器功能并可直接嵌入引擎界面适用于游戏内嵌页面、可视化数据展示、混合式交互等场景。资源面向掌握C与UE5基础、希望扩展引擎Web能力的中高级开发者。压缩包共158个文件、约17.85MB以h/cpp源码、uasset/umap关卡资源、dll/lib运行库、sln工程配置为主体另有tlog构建日志、ini设置和说明文档。核心模块包括EdgeMgr、SWebView、WebViewWidget及消息处理主机工程结构清晰适合直接打开运行也方便按模块学习。通过学习可掌握WebView2在UE5中的初始化、页面加载与宿主通信机制进而自定义导航、脚本注入与事件回调。当前已有394人学习下载开箱即用的设计使其成为UE5内置浏览器功能的重要参考实现。1. UE5 在线浏览器方案选型为什么我选 Edge WebView2 Runtime初看这个资源一般会想UE5 里装个浏览器不是照着网上抄个 WebBrowser 插件就行我拆完这套基于 Edge WebView2 Runtime 的方案之后想法有变化——UE5 自带的 WebBrowser 在新版本里依赖 CEF包体大、权限边界难控而这套方案直接调用系统级 Edge 内核Runtime 由 Windows 统一管理UE 端只做渲染和交互封装。它主要解决三件事一是把 Web 页面变成 3D 场景里的可交互面板二是保持浏览器内核的自动更新能力三是省掉自己编译 Chromium 的维护成本。适合需要在 UE5 里做页游外壳、动态活动页、后台管理界面的开发者也适合想给项目补充 Web 技术栈入口的团队。2. 工程落地第一步源码包结构、Runtime 检测与首个 BrowserActor2.1 源码包三块结构插件、检测脚本与示例工程拿到资源后不要急着把整个目录拖进 UE 工程。我一般按下面顺序过一遍目录先理解它的边界再动手插件目录包含 UE5 调用 WebView2 的封装层底层是 C 对 WebView2 COM 接口的包装上层又提供一组蓝图可调用的节点。检测与安装脚本负责探测目标机器上 WebView2 Runtime 的安装情况缺失时触发静默安装。示例工程里面有现成地图打开就能看到带地址栏的浏览器 UI。把它们按模块拆开看放置方式如下内容作用建议放置位置插件目录UE5 工程内调用 WebView2 的封装层[项目根]/Plugins/检测与安装脚本检查 Runtime缺失时触发安装项目 Build 目录或部署脚本示例地图与蓝图组件快速验证导航、输入、事件[项目根]/Content/这个结构决定了后续维护的边界引擎相关代码尽量不动只改插件层和脚本业务页面的更新走 Web 端发布和 UE 工程解耦。需要留意 UE5 小版本的差异。源码包如果基于 5.1/5.2 编写直接放进 5.4 工程通常会遇到 C 头文件变动比如某个模块从公开变成非公开或者模块依赖名改了。解决方案一般是在插件模块的 Build.cs 里补齐依赖或者在项目里右键插件选择重新编译。从下载到运行我踩的坎基本都是这一小步。2.2 Runtime 检测与部署不要在目标机器上裸奔WebView2 Runtime 分 Evergreen常青和 Fixed Version固定版本两种模式。UE5 里用 Evergreen 的居多因为 Windows 系统会自己更新开发包不用每发一版都重带运行时。Fixed Version 适合给封闭环境离线分发但要随程序带一套运行时文件包体积会明显增加。这套资源默认的检测脚本常见做法是查注册表确认 Edge WebView2 Runtime 是否安装。注册表路径一般是HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}其中pv这个值就是已安装版本号。脚本拿到版本号后和最低要求做对比如果缺失或版本过低就调起 WebView2 运行时安装器。静默安装命令通常是./MicrosoftEdgeWebview2Setup.exe /silent /install注意区分多数在线下载包只有 bootstrapper它需要联网下载完整运行时后再安装。目标机器在内网隔离的环境里跑就得提前准备好完整离线安装包并把自动更新策略关掉。参数说明/silent不弹任何交互窗口。/install和/silent组合时执行安装动作。/update已装有旧版本时可强制升级到最新版。脚本执行时尽量以管理员权限调用否则 UAC 会拦住注册表写入。部署现场如果不想弹 UAC可以用机器级策略推送但这套源码自带的脚本不包含企业级分发能力适合小批量测试机或项目组内自用。2.3 首个 BrowserActor创建组件并加载首屏检查完 Runtime接下来创建一个最简单的浏览器 Actor。常见做法是新建一个 C Actor 或蓝图 Actor在其中挂一个 Browser 组件。为了让页面渲染到 UE 场景这个组件通常继承自 UWidgetComponent或者内部使用 Slate 控件承载 WebView2 的视口。C 初始化示例描述通用接口实际类名以源码为准// MyBrowserActor.cpp AMyBrowserActor::AMyBrowserActor() { // 创建浏览器组件并设置屏幕尺寸 BrowserWidget CreateDefaultSubobjectUBrowserWidgetComponent(TEXT(BrowserWidget)); BrowserWidget-SetupAttachment(RootComponent); BrowserWidget-SetWidgetSpace(EWidgetSpace::Screen); } void AMyBrowserActor::BeginPlay() { Super::BeginPlay(); // 初始化参数缓存目录、是否允许透明、语言 FWebViewInitParams InitParams; InitParams.UserDataFolder FPaths::ProjectSavedDir() / TEXT(WebViewData); InitParams.BackgroundTransparent false; InitParams.Locale TEXT(zh-CN); BrowserWidget-Init(InitParams); BrowserWidget-LoadURL(TEXT(https://example.com)); }我一般会把Init和LoadURL分开便于后续复用同一个组件切换页面。参数里最需要关注UserDataFolder它用来存 Cookie 和 Local Storage。如果项目里要开多个 WebView2 实例UserDataFolder不能共用同一个目录否则会出现互踩文件锁、白屏甚至崩溃。这样建好后运行地图能看到浏览器首页。如果场景里一片黑先检查是不是组件宽度和高度写成了 0或者场景里没有放一面能接收渲染结果的面片。这个阶段的验收标准就是“能亮起来”还没到交互层面。3. 把浏览器用起来导航封装、事件回调与 JS 双向通信3.1 导航封装从 OpenUrl 到前进后退把浏览器接进来之后第一步是封装导航。UE 侧不建议直接到处调用 WebView2 原生接口而是包一层 BrowserSession方便做协议白名单和错误处理。bool UBrowserSession::OpenUrl(const FString Url) { if (!RuntimeReady) { UE_LOG(LogTemp, Error, TEXT(Runtime is not ready)); return false; } FString LowerUrl Url.ToLower(); if (!(LowerUrl.StartsWith(TEXT(https://)) || LowerUrl.StartsWith(TEXT(http://)))) { UE_LOG(LogTemp, Warning, TEXT(Blocked URL: %s), *Url); return false; } return WebView-Navigate(Url); }逻辑说明先校验 Runtime 状态再校验协议白名单。不做协议限制的话file://指向本地任意路径就能变成一个文件读取入口这在业务里是很大的隐患。参数说明RuntimeReady在浏览器组件 Init 成功后置 true。Navigate是异步方法返回 true 只表示请求已提交不代表加载成功。到这一步页面加载失败与否要看后续导航完成事件不能只看返回值。实际项目不只有 OpenUrl前端页面跳转目标通常是业务后台地址同时还需要支持前进、后退、刷新、停止。这四个接口直接映射 WebView2 原生方法void UBrowserSession::GoBack() { WebView-GoBack(); } void UBrowserSession::GoForward() { WebView-GoForward(); } void UBrowserSession::Reload() { WebView-Reload(); } void UBrowserSession::Stop() { WebView-Stop(); }严格来说GoBack和GoForward应该在CanGoBack/CanGoForward为 true 时才调用到原生层。否则页面没有历史记录时点过去会出现无响应或者奇怪的边界异常。3.2 生命周期事件加载开始、结束与页面标题WebView2 页面加载是典型的异步过程不监听事件后面做加载状态 UI 会无从下手。需要挂的事件至少有这三个NavigationStarting拦截请求、改请求头、注入登录态。NavigationCompleted判断加载是否成功成功后刷新页面纹理。DocumentTitleChanged同步地址栏标题。给 Session 挂监听的代码WebView-OnNavigationStarting().AddLambda([](const auto Args) { // 注意这里拿到的 Uri 可能因为重定向而变化 UE_LOG(LogTemp, Log, TEXT(Navigation starting: %s), *Args.Uri()); }); WebView-OnNavigationCompleted().AddLambda([](const auto Args) { if (Args.IsSuccess()) { UE_LOG(LogTemp, Log, TEXT(Navigation success: %s), *Args.Uri()); } });这里有一个容易误用的点地址栏输入 A 地址页面可能自动跳到 B 地址NavigationStarting 会触发多次每次 Uri 都不一样。显示地址栏时要以 NavigationCompleted 的 Uri 为准不要用第一次跳转的地址。如果想在加载期间显示 Loading还有一个不可忽略的 NewWindowRequested 事件。部分页面会通过window.open打开新窗口如果策略是“在当前浏览器内打开”需要在这里接管窗口请求而不是让系统弹出外部窗口。3.3 JS 双向通信ExecuteScript 与 WebMessageReceivedUE 侧主动调用网页函数最直接的是ExecuteScriptFString JsCode TEXT(window.App window.App.refresh()); WebView-ExecuteScript(JsCode, [](HRESULT Result, FString JsonResult) { if (SUCCEEDED(Result)) { // JsonResult 是 JS 表达式返回值可能是字符串、对象序列化后的文本 UE_LOG(LogTemp, Log, TEXT(JS Result: %s), *JsonResult); } });重点提醒ExecuteScript里的 JS 代码应该在页面加载完成后执行。如果页面还没 load执行结果通常是nullptr或undefined回调收到的内容全部是空。我一般在NavigationCompleted成功后再触发首屏 JS。网页回传消息到 UE 侧走的是WebMessageReceived事件。前端 JS 调用方式window.chrome.webview.postMessage({type: clickButton, value: 123});前提是浏览器初始化时没有禁用 Web Message 通道。UE 端监听和处理的示例WebView-OnWebMessageReceived().AddLambda([](const auto Args) { FString RawMessage Args.GetMessage(); // 消息是 JSON 字符串解析时注意回到游戏线程再操作 UObject AsyncTask(ENamedThreads::GameThread, [RawMessage]() { // 在这里安全更新游戏逻辑 }); });这一节的核心是让网页和 UE 端分工明确网页负责绘制和交互采集UE 负责逻辑决策和场景表现。通道封装得好后面做页游壳、活动页都只是换 URL 的事。4. 渲染与交互细节从黑屏到流畅内嵌界面的关键设置第 2 章完成了“能亮起来”这一步解决的是“亮得对不对、点得准不准、像不像场景的一部分”。4.1 把 WebView2 输出接到 UE 的纹理管线上常见实现是 WebView2 生成共享纹理UE 侧拿到共享句柄再创建动态材质。代码大致长这样// 初始化时把浏览器画面写到目标纹理 BrowserWidget-CreateSharedTexture(); UMaterialInstanceDynamic* DynMat Mesh-CreateAndSetMaterialInstanceDynamic(0); DynMat-SetTextureParameterValue(TEXT(WebViewTexture), BrowserWidget-GetSharedTexture());逻辑说明核心是纹理句柄的传递而不是 CPU 复制像素数据。共享纹理由插件内部完成UE 侧只需要拿到纹理资源赋给材质。每次页面更新后要调用UpdateResource让渲染线程认领否则页面滚动时会花屏或残留上一帧。参数说明TextureWidth / TextureHeight 对应网页视口大小一般设为 1920×1080 或 1280×720。如果 UE 工程用的是 Vulkan RHI某些旧版本插件只支持 DX11/DX12需要确认 RHI Backend。动态材质参数名必须和材质蓝图里的命名一致大写差异会静默失败运行时根本看不到报错。4.2 输入事件鼠标点击、焦点与滚轮方向“页面能看见但点不动”是接入第三阶段最明显的反馈。根因往往是输入模式没有正确设置。UE 端处理输入前控制器先获得鼠标输入再把屏幕坐标转成 WebView2 的视口坐标void ABrowserActor::HandleMouseInput(APlayerController* PC, FVector2D ScreenPosition) { FVector2D ViewportSize; PC-GetViewportSize(ViewportSize); float U ScreenPosition.X / ViewportSize.X; float V ScreenPosition.Y / ViewportSize.Y; float PixelX U * BrowserWidget-GetWidth(); float PixelY V * BrowserWidget-GetHeight(); BrowserWidget-SendMouseMove(PixelX, PixelY); BrowserWidget-SendMouseClick(EMouseButton::Left); }逻辑说明这里把 0 到 1 的归一化坐标重新映射到浏览器的像素坐标避免不同分辨率下点击偏移。参数说明SendMouseMove和SendMouseClick要分开调用移动频率可以高点击必须一次一次触发。滚轮方向在 WebView2 里和 UE 默认滚动方向相反很多项目会在回调前乘一个 -1。焦点切换时比如从游戏操作杆切到 UI需要显式调用SetFocus和SetOwnerPC否则键盘事件会漏掉。4.3 透明背景与 Web 端样式配合想让网页像一个悬浮 HUD 融入场景而不是一块白色矩形板可以打开背景透明。初始化参数BackgroundTransparent设为 true并把材质 Blend Mode 改为半透明。网页端还需要配合设置html, body { background: transparent; }如果网页 body 写了白色背景即使 UE 端透明也无济于事。透明通道的调试不太直观建议用纯色测试页先确认网页端全透明再回来调 UE 材质。不要在一堆复杂 UI 上排查透明问题越调越接近玄学。5. 避坑排查WebView2 接入 UE5 的五个高频问题5.1 弹窗提示“运行时未安装”但目标机器明明装了 Edge现象启动项目后弹窗报错说找不到 WebView2 Runtime而机器上的 Edge 浏览器可以正常打开。原因检测脚本查的是 64 位注册表视图UE 工程编译成了 32 位开发版进程访问注册表时被重定向到另一份视图所以查不到安装记录。解决把检测脚本改成同时查询 32 位和 64 位两个注册表视图或者干脆统一要求项目使用 Win64 目标平台。实际项目里32 位 Editor 跑浏览器组件最常出现空指针和找不到 Runtime 的报错。5.2 页面加载成功但画面全黑日志没有任何错误现象导航事件显示加载成功网页本身没问题但 UE 场景里一片黑。原因共享纹理更新失败通常是 RHI 模式不匹配或者动态材质采样器没有被赋值。也可能是材质没有设置成 Unlit被场景光照影响导致画面不可见。解决先在材质里临时用纯色输出确认材质本身有效再把纹理更新放回渲染线程检查返回值。若仍然黑屏在浏览器初始化参数里加--disable-gpu走软件渲染验证注意软渲会吃掉更多 CPU只能用来定位问题。5.3 页面白屏浏览器控制台报跨域错误现象页面内容没有显示打开 WebView2 的调试日志发现前端报Not allowed to load local resource或跨域。原因网页工程里引用了本地文件或其他子域名的资源WebView2 对自定义协议和file://的限制比普通浏览器更严格。解决把页面放到本地 HTTP 服务或正式域名下不要用file://方式打开。如果业务必须用到内网 IP 作资源地址要么做域名映射要么在初始化参数里给特定源放开跨域策略。不要全局放开远程调试端口那样会暴露给局域网内的其他进程。5.4 JS postMessage 到 UE 时数据顺序错乱现象网页快速发送多条消息UE 端收到的顺序和发送顺序不一致部分消息丢失。原因WebMessageReceived回调触发在 WebView2 的后台线程多个消息频繁进入时如果 UE 侧用普通成员变量临时存储会出现覆盖和错序。解决在回调里立刻拷贝数据并用AsyncTask投递到游戏线程队列。UE 侧用一个 FIFO 队列按到达顺序出队不要直接在回调里访问 UObject。顺便提一句ExecuteScript的回调也有同样问题不要在回调里直接操作 UI。5.5 打包体积大了不少冷启动首屏要等几秒现象按默认配置打包后安装目录多出几十上百 MB第一次打开浏览器组件要卡好几秒。原因插件把 Fixed Version 的 WebView2 Runtime 一起打进了包或者UserDataFolder首次创建时要做大量初始化和缓存写入再加上编辑器里测试不明显的资源加载全在运行时发生。解决优先走 Evergreen 运行时打包时不带运行时文件让目标机器复用系统 Edge 内核。同时把UserDataFolder指到用户可写目录但要注意多实例不能共用。代码里对初始化位置做超时显示和失败重试不要一直停在黑屏。6. 进阶优化离线分发、内存控制与白名单策略这套资源到这一步已经能跑通主流程。在正式上线前我建议再做三个进阶动作收口页面入口、管住缓存、把分发从“能装”提升到“装得稳”。页面白名单是必做的。如果你做的是纯内部工具业务域名通常只有一两个可以在初始化时把允许列表传进去并在NavigationStarting里再次拦截if (!PermittedDomains.Contains(Args.Uri())) { Args.Cancel(); }这段逻辑不能省。直接放行会让任意链接跳转到外部网站UE 端就变成一个隐形浏览器后续内容安全审查会很难交代。内存控制也有技巧。WebView2 本身会开 GPU 进程、渲染进程UE 游戏进程再占一份内存。可以在AdditionalBrowserArguments里加启动参数限制--disable-gpu --max-decoded-image-size-mb128disable-gpu会牺牲 GPU 加速如果页面上有很多摄像头视频流建议先压测再决定。max-decoded-image-size-mb限制单张图片解码内存适合页面里经常加载超大截图的场景。这个参数在低配机器上效果明显但截图画质会下降。离线分发场景下优先建议 Evergreen 模式。只要目标机器能连上微软更新通道就不需要为每个项目单独维护 Runtime 版本。如果项目处于完全内网环境才考虑 Fixed Version但此时要手动跟进安全更新。我自己的一个习惯是每次 Demo 更新后跑一遍“冷启动 — 加载页面 — 五步交互 — 退出重进”的自查流程对照下面这张表判断检查项预期结果失败时的优先检查点Runtime 检测未装时自动触发静默安装注册表键、执行权限首屏加载3 秒内出现页面内容UserDataFolder 锁、纹理更新JS 消息点击页面按钮UE 日志收到 JSONWebMessage 通道、后台线程焦点切换键盘输入能进入网页输入模式、SetFocus打包部署无网机器可运行Evergreen / Fixed 模式选择从那以后我每次把插件分发给同事之前都会强制走一遍这张表的流程尤其是焦点切换和打包部署两项总是最后才暴露问题。检查完再交付能省掉不少“为什么我这台机器不行”的来回沟通。希望帮到你。本文还有配套的精品资源点击获取