)
簡介UE5引擎中集成網(wǎng)頁瀏覽能力常受限于內(nèi)置瀏覽器方案而Edge WebView2 Runtime提供了現(xiàn)代且穩(wěn)定的內(nèi)核支持。這份源碼基于該組件實現(xiàn)了一套完整的在線瀏覽器功能并可直接嵌入引擎界面適用于游戲內(nèi)嵌頁面、可視化數(shù)據(jù)展示、混合式交互等場景。資源面向掌握C與UE5基礎(chǔ)、希望擴(kuò)展引擎Web能力的中高級開發(fā)者。壓縮包共158個文件、約17.85MB以h/cpp源碼、uasset/umap關(guān)卡資源、dll/lib運行庫、sln工程配置為主體另有tlog構(gòu)建日志、ini設(shè)置和說明文檔。核心模塊包括EdgeMgr、SWebView、WebViewWidget及消息處理主機(jī)工程結(jié)構(gòu)清晰適合直接打開運行也方便按模塊學(xué)習(xí)。通過學(xué)習(xí)可掌握WebView2在UE5中的初始化、頁面加載與宿主通信機(jī)制進(jìn)而自定義導(dǎo)航、腳本注入與事件回調(diào)。當(dāng)前已有394人學(xué)習(xí)下載開箱即用的設(shè)計使其成為UE5內(nèi)置瀏覽器功能的重要參考實現(xiàn)。1. UE5 在線瀏覽器方案選型為什么我選 Edge WebView2 Runtime初看這個資源一般會想UE5 里裝個瀏覽器不是照著網(wǎng)上抄個 WebBrowser 插件就行我拆完這套基于 Edge WebView2 Runtime 的方案之后想法有變化——UE5 自帶的 WebBrowser 在新版本里依賴 CEF包體大、權(quán)限邊界難控而這套方案直接調(diào)用系統(tǒng)級 Edge 內(nèi)核Runtime 由 Windows 統(tǒng)一管理UE 端只做渲染和交互封裝。它主要解決三件事一是把 Web 頁面變成 3D 場景里的可交互面板二是保持瀏覽器內(nèi)核的自動更新能力三是省掉自己編譯 Chromium 的維護(hù)成本。適合需要在 UE5 里做頁游外殼、動態(tài)活動頁、后臺管理界面的開發(fā)者也適合想給項目補(bǔ)充 Web 技術(shù)棧入口的團(tuán)隊。2. 工程落地第一步源碼包結(jié)構(gòu)、Runtime 檢測與首個 BrowserActor2.1 源碼包三塊結(jié)構(gòu)插件、檢測腳本與示例工程拿到資源后不要急著把整個目錄拖進(jìn) UE 工程。我一般按下面順序過一遍目錄先理解它的邊界再動手插件目錄包含 UE5 調(diào)用 WebView2 的封裝層底層是 C 對 WebView2 COM 接口的包裝上層又提供一組藍(lán)圖可調(diào)用的節(jié)點。檢測與安裝腳本負(fù)責(zé)探測目標(biāo)機(jī)器上 WebView2 Runtime 的安裝情況缺失時觸發(fā)靜默安裝。示例工程里面有現(xiàn)成地圖打開就能看到帶地址欄的瀏覽器 UI。把它們按模塊拆開看放置方式如下內(nèi)容作用建議放置位置插件目錄UE5 工程內(nèi)調(diào)用 WebView2 的封裝層[項目根]/Plugins/檢測與安裝腳本檢查 Runtime缺失時觸發(fā)安裝項目 Build 目錄或部署腳本示例地圖與藍(lán)圖組件快速驗證導(dǎo)航、輸入、事件[項目根]/Content/這個結(jié)構(gòu)決定了后續(xù)維護(hù)的邊界引擎相關(guān)代碼盡量不動只改插件層和腳本業(yè)務(wù)頁面的更新走 Web 端發(fā)布和 UE 工程解耦。需要留意 UE5 小版本的差異。源碼包如果基于 5.1/5.2 編寫直接放進(jìn) 5.4 工程通常會遇到 C 頭文件變動比如某個模塊從公開變成非公開或者模塊依賴名改了。解決方案一般是在插件模塊的 Build.cs 里補(bǔ)齊依賴或者在項目里右鍵插件選擇重新編譯。從下載到運行我踩的坎基本都是這一小步。2.2 Runtime 檢測與部署不要在目標(biāo)機(jī)器上裸奔WebView2 Runtime 分 Evergreen常青和 Fixed Version固定版本兩種模式。UE5 里用 Evergreen 的居多因為 Windows 系統(tǒng)會自己更新開發(fā)包不用每發(fā)一版都重帶運行時。Fixed Version 適合給封閉環(huán)境離線分發(fā)但要隨程序帶一套運行時文件包體積會明顯增加。這套資源默認(rèn)的檢測腳本常見做法是查注冊表確認(rèn) Edge WebView2 Runtime 是否安裝。注冊表路徑一般是HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}其中pv這個值就是已安裝版本號。腳本拿到版本號后和最低要求做對比如果缺失或版本過低就調(diào)起 WebView2 運行時安裝器。靜默安裝命令通常是./MicrosoftEdgeWebview2Setup.exe /silent /install注意區(qū)分多數(shù)在線下載包只有 bootstrapper它需要聯(lián)網(wǎng)下載完整運行時后再安裝。目標(biāo)機(jī)器在內(nèi)網(wǎng)隔離的環(huán)境里跑就得提前準(zhǔn)備好完整離線安裝包并把自動更新策略關(guān)掉。參數(shù)說明/silent不彈任何交互窗口。/install和/silent組合時執(zhí)行安裝動作。/update已裝有舊版本時可強(qiáng)制升級到最新版。腳本執(zhí)行時盡量以管理員權(quán)限調(diào)用否則 UAC 會攔住注冊表寫入。部署現(xiàn)場如果不想彈 UAC可以用機(jī)器級策略推送但這套源碼自帶的腳本不包含企業(yè)級分發(fā)能力適合小批量測試機(jī)或項目組內(nèi)自用。2.3 首個 BrowserActor創(chuàng)建組件并加載首屏檢查完 Runtime接下來創(chuàng)建一個最簡單的瀏覽器 Actor。常見做法是新建一個 C Actor 或藍(lán)圖 Actor在其中掛一個 Browser 組件。為了讓頁面渲染到 UE 場景這個組件通常繼承自 UWidgetComponent或者內(nèi)部使用 Slate 控件承載 WebView2 的視口。C 初始化示例描述通用接口實際類名以源碼為準(zhǔn)// MyBrowserActor.cpp AMyBrowserActor::AMyBrowserActor() { // 創(chuàng)建瀏覽器組件并設(shè)置屏幕尺寸 BrowserWidget CreateDefaultSubobjectUBrowserWidgetComponent(TEXT(BrowserWidget)); BrowserWidget-SetupAttachment(RootComponent); BrowserWidget-SetWidgetSpace(EWidgetSpace::Screen); } void AMyBrowserActor::BeginPlay() { Super::BeginPlay(); // 初始化參數(shù)緩存目錄、是否允許透明、語言 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分開便于后續(xù)復(fù)用同一個組件切換頁面。參數(shù)里最需要關(guān)注UserDataFolder它用來存 Cookie 和 Local Storage。如果項目里要開多個 WebView2 實例UserDataFolder不能共用同一個目錄否則會出現(xiàn)互踩文件鎖、白屏甚至崩潰。這樣建好后運行地圖能看到瀏覽器首頁。如果場景里一片黑先檢查是不是組件寬度和高度寫成了 0或者場景里沒有放一面能接收渲染結(jié)果的面片。這個階段的驗收標(biāo)準(zhǔn)就是“能亮起來”還沒到交互層面。3. 把瀏覽器用起來導(dǎo)航封裝、事件回調(diào)與 JS 雙向通信3.1 導(dǎo)航封裝從 OpenUrl 到前進(jìn)后退把瀏覽器接進(jìn)來之后第一步是封裝導(dǎo)航。UE 側(cè)不建議直接到處調(diào)用 WebView2 原生接口而是包一層 BrowserSession方便做協(xié)議白名單和錯誤處理。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 狀態(tài)再校驗協(xié)議白名單。不做協(xié)議限制的話file://指向本地任意路徑就能變成一個文件讀取入口這在業(yè)務(wù)里是很大的隱患。參數(shù)說明RuntimeReady在瀏覽器組件 Init 成功后置 true。Navigate是異步方法返回 true 只表示請求已提交不代表加載成功。到這一步頁面加載失敗與否要看后續(xù)導(dǎo)航完成事件不能只看返回值。實際項目不只有 OpenUrl前端頁面跳轉(zhuǎn)目標(biāo)通常是業(yè)務(wù)后臺地址同時還需要支持前進(jìn)、后退、刷新、停止。這四個接口直接映射 WebView2 原生方法void UBrowserSession::GoBack() { WebView-GoBack(); } void UBrowserSession::GoForward() { WebView-GoForward(); } void UBrowserSession::Reload() { WebView-Reload(); } void UBrowserSession::Stop() { WebView-Stop(); }嚴(yán)格來說GoBack和GoForward應(yīng)該在CanGoBack/CanGoForward為 true 時才調(diào)用到原生層。否則頁面沒有歷史記錄時點過去會出現(xiàn)無響應(yīng)或者奇怪的邊界異常。3.2 生命周期事件加載開始、結(jié)束與頁面標(biāo)題WebView2 頁面加載是典型的異步過程不監(jiān)聽事件后面做加載狀態(tài) UI 會無從下手。需要掛的事件至少有這三個NavigationStarting攔截請求、改請求頭、注入登錄態(tài)。NavigationCompleted判斷加載是否成功成功后刷新頁面紋理。DocumentTitleChanged同步地址欄標(biāo)題。給 Session 掛監(jiān)聽的代碼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 會觸發(fā)多次每次 Uri 都不一樣。顯示地址欄時要以 NavigationCompleted 的 Uri 為準(zhǔn)不要用第一次跳轉(zhuǎn)的地址。如果想在加載期間顯示 Loading還有一個不可忽略的 NewWindowRequested 事件。部分頁面會通過window.open打開新窗口如果策略是“在當(dāng)前瀏覽器內(nèi)打開”需要在這里接管窗口請求而不是讓系統(tǒng)彈出外部窗口。3.3 JS 雙向通信ExecuteScript 與 WebMessageReceivedUE 側(cè)主動調(diào)用網(wǎng)頁函數(shù)最直接的是ExecuteScriptFString JsCode TEXT(window.App window.App.refresh()); WebView-ExecuteScript(JsCode, [](HRESULT Result, FString JsonResult) { if (SUCCEEDED(Result)) { // JsonResult 是 JS 表達(dá)式返回值可能是字符串、對象序列化后的文本 UE_LOG(LogTemp, Log, TEXT(JS Result: %s), *JsonResult); } });重點提醒ExecuteScript里的 JS 代碼應(yīng)該在頁面加載完成后執(zhí)行。如果頁面還沒 load執(zhí)行結(jié)果通常是nullptr或undefined回調(diào)收到的內(nèi)容全部是空。我一般在NavigationCompleted成功后再觸發(fā)首屏 JS。網(wǎng)頁回傳消息到 UE 側(cè)走的是WebMessageReceived事件。前端 JS 調(diào)用方式window.chrome.webview.postMessage({type: clickButton, value: 123});前提是瀏覽器初始化時沒有禁用 Web Message 通道。UE 端監(jiān)聽和處理的示例WebView-OnWebMessageReceived().AddLambda([](const auto Args) { FString RawMessage Args.GetMessage(); // 消息是 JSON 字符串解析時注意回到游戲線程再操作 UObject AsyncTask(ENamedThreads::GameThread, [RawMessage]() { // 在這里安全更新游戲邏輯 }); });這一節(jié)的核心是讓網(wǎng)頁和 UE 端分工明確網(wǎng)頁負(fù)責(zé)繪制和交互采集UE 負(fù)責(zé)邏輯決策和場景表現(xiàn)。通道封裝得好后面做頁游殼、活動頁都只是換 URL 的事。4. 渲染與交互細(xì)節(jié)從黑屏到流暢內(nèi)嵌界面的關(guān)鍵設(shè)置第 2 章完成了“能亮起來”這一步解決的是“亮得對不對、點得準(zhǔn)不準(zhǔn)、像不像場景的一部分”。4.1 把 WebView2 輸出接到 UE 的紋理管線上常見實現(xiàn)是 WebView2 生成共享紋理UE 側(cè)拿到共享句柄再創(chuàng)建動態(tài)材質(zhì)。代碼大致長這樣// 初始化時把瀏覽器畫面寫到目標(biāo)紋理 BrowserWidget-CreateSharedTexture(); UMaterialInstanceDynamic* DynMat Mesh-CreateAndSetMaterialInstanceDynamic(0); DynMat-SetTextureParameterValue(TEXT(WebViewTexture), BrowserWidget-GetSharedTexture());邏輯說明核心是紋理句柄的傳遞而不是 CPU 復(fù)制像素數(shù)據(jù)。共享紋理由插件內(nèi)部完成UE 側(cè)只需要拿到紋理資源賦給材質(zhì)。每次頁面更新后要調(diào)用UpdateResource讓渲染線程認(rèn)領(lǐng)否則頁面滾動時會花屏或殘留上一幀。參數(shù)說明TextureWidth / TextureHeight 對應(yīng)網(wǎng)頁視口大小一般設(shè)為 1920×1080 或 1280×720。如果 UE 工程用的是 Vulkan RHI某些舊版本插件只支持 DX11/DX12需要確認(rèn) RHI Backend。動態(tài)材質(zhì)參數(shù)名必須和材質(zhì)藍(lán)圖里的命名一致大寫差異會靜默失敗運行時根本看不到報錯。4.2 輸入事件鼠標(biāo)點擊、焦點與滾輪方向“頁面能看見但點不動”是接入第三階段最明顯的反饋。根因往往是輸入模式?jīng)]有正確設(shè)置。UE 端處理輸入前控制器先獲得鼠標(biāo)輸入再把屏幕坐標(biāo)轉(zhuǎn)成 WebView2 的視口坐標(biāo)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 的歸一化坐標(biāo)重新映射到瀏覽器的像素坐標(biāo)避免不同分辨率下點擊偏移。參數(shù)說明SendMouseMove和SendMouseClick要分開調(diào)用移動頻率可以高點擊必須一次一次觸發(fā)。滾輪方向在 WebView2 里和 UE 默認(rèn)滾動方向相反很多項目會在回調(diào)前乘一個 -1。焦點切換時比如從游戲操作桿切到 UI需要顯式調(diào)用SetFocus和SetOwnerPC否則鍵盤事件會漏掉。4.3 透明背景與 Web 端樣式配合想讓網(wǎng)頁像一個懸浮 HUD 融入場景而不是一塊白色矩形板可以打開背景透明。初始化參數(shù)BackgroundTransparent設(shè)為 true并把材質(zhì) Blend Mode 改為半透明。網(wǎng)頁端還需要配合設(shè)置html, body { background: transparent; }如果網(wǎng)頁 body 寫了白色背景即使 UE 端透明也無濟(jì)于事。透明通道的調(diào)試不太直觀建議用純色測試頁先確認(rèn)網(wǎng)頁端全透明再回來調(diào) UE 材質(zhì)。不要在一堆復(fù)雜 UI 上排查透明問題越調(diào)越接近玄學(xué)。5. 避坑排查WebView2 接入 UE5 的五個高頻問題5.1 彈窗提示“運行時未安裝”但目標(biāo)機(jī)器明明裝了 Edge現(xiàn)象啟動項目后彈窗報錯說找不到 WebView2 Runtime而機(jī)器上的 Edge 瀏覽器可以正常打開。原因檢測腳本查的是 64 位注冊表視圖UE 工程編譯成了 32 位開發(fā)版進(jìn)程訪問注冊表時被重定向到另一份視圖所以查不到安裝記錄。解決把檢測腳本改成同時查詢 32 位和 64 位兩個注冊表視圖或者干脆統(tǒng)一要求項目使用 Win64 目標(biāo)平臺。實際項目里32 位 Editor 跑瀏覽器組件最常出現(xiàn)空指針和找不到 Runtime 的報錯。5.2 頁面加載成功但畫面全黑日志沒有任何錯誤現(xiàn)象導(dǎo)航事件顯示加載成功網(wǎng)頁本身沒問題但 UE 場景里一片黑。原因共享紋理更新失敗通常是 RHI 模式不匹配或者動態(tài)材質(zhì)采樣器沒有被賦值。也可能是材質(zhì)沒有設(shè)置成 Unlit被場景光照影響導(dǎo)致畫面不可見。解決先在材質(zhì)里臨時用純色輸出確認(rèn)材質(zhì)本身有效再把紋理更新放回渲染線程檢查返回值。若仍然黑屏在瀏覽器初始化參數(shù)里加--disable-gpu走軟件渲染驗證注意軟渲會吃掉更多 CPU只能用來定位問題。5.3 頁面白屏瀏覽器控制臺報跨域錯誤現(xiàn)象頁面內(nèi)容沒有顯示打開 WebView2 的調(diào)試日志發(fā)現(xiàn)前端報Not allowed to load local resource或跨域。原因網(wǎng)頁工程里引用了本地文件或其他子域名的資源WebView2 對自定義協(xié)議和file://的限制比普通瀏覽器更嚴(yán)格。解決把頁面放到本地 HTTP 服務(wù)或正式域名下不要用file://方式打開。如果業(yè)務(wù)必須用到內(nèi)網(wǎng) IP 作資源地址要么做域名映射要么在初始化參數(shù)里給特定源放開跨域策略。不要全局放開遠(yuǎn)程調(diào)試端口那樣會暴露給局域網(wǎng)內(nèi)的其他進(jìn)程。5.4 JS postMessage 到 UE 時數(shù)據(jù)順序錯亂現(xiàn)象網(wǎng)頁快速發(fā)送多條消息UE 端收到的順序和發(fā)送順序不一致部分消息丟失。原因WebMessageReceived回調(diào)觸發(fā)在 WebView2 的后臺線程多個消息頻繁進(jìn)入時如果 UE 側(cè)用普通成員變量臨時存儲會出現(xiàn)覆蓋和錯序。解決在回調(diào)里立刻拷貝數(shù)據(jù)并用AsyncTask投遞到游戲線程隊列。UE 側(cè)用一個 FIFO 隊列按到達(dá)順序出隊不要直接在回調(diào)里訪問 UObject。順便提一句ExecuteScript的回調(diào)也有同樣問題不要在回調(diào)里直接操作 UI。5.5 打包體積大了不少冷啟動首屏要等幾秒現(xiàn)象按默認(rèn)配置打包后安裝目錄多出幾十上百 MB第一次打開瀏覽器組件要卡好幾秒。原因插件把 Fixed Version 的 WebView2 Runtime 一起打進(jìn)了包或者UserDataFolder首次創(chuàng)建時要做大量初始化和緩存寫入再加上編輯器里測試不明顯的資源加載全在運行時發(fā)生。解決優(yōu)先走 Evergreen 運行時打包時不帶運行時文件讓目標(biāo)機(jī)器復(fù)用系統(tǒng) Edge 內(nèi)核。同時把UserDataFolder指到用戶可寫目錄但要注意多實例不能共用。代碼里對初始化位置做超時顯示和失敗重試不要一直停在黑屏。6. 進(jìn)階優(yōu)化離線分發(fā)、內(nèi)存控制與白名單策略這套資源到這一步已經(jīng)能跑通主流程。在正式上線前我建議再做三個進(jìn)階動作收口頁面入口、管住緩存、把分發(fā)從“能裝”提升到“裝得穩(wěn)”。頁面白名單是必做的。如果你做的是純內(nèi)部工具業(yè)務(wù)域名通常只有一兩個可以在初始化時把允許列表傳進(jìn)去并在NavigationStarting里再次攔截if (!PermittedDomains.Contains(Args.Uri())) { Args.Cancel(); }這段邏輯不能省。直接放行會讓任意鏈接跳轉(zhuǎn)到外部網(wǎng)站UE 端就變成一個隱形瀏覽器后續(xù)內(nèi)容安全審查會很難交代。內(nèi)存控制也有技巧。WebView2 本身會開 GPU 進(jìn)程、渲染進(jìn)程UE 游戲進(jìn)程再占一份內(nèi)存??梢栽贏dditionalBrowserArguments里加啟動參數(shù)限制--disable-gpu --max-decoded-image-size-mb128disable-gpu會犧牲 GPU 加速如果頁面上有很多攝像頭視頻流建議先壓測再決定。max-decoded-image-size-mb限制單張圖片解碼內(nèi)存適合頁面里經(jīng)常加載超大截圖的場景。這個參數(shù)在低配機(jī)器上效果明顯但截圖畫質(zhì)會下降。離線分發(fā)場景下優(yōu)先建議 Evergreen 模式。只要目標(biāo)機(jī)器能連上微軟更新通道就不需要為每個項目單獨維護(hù) Runtime 版本。如果項目處于完全內(nèi)網(wǎng)環(huán)境才考慮 Fixed Version但此時要手動跟進(jìn)安全更新。我自己的一個習(xí)慣是每次 Demo 更新后跑一遍“冷啟動 — 加載頁面 — 五步交互 — 退出重進(jìn)”的自查流程對照下面這張表判斷檢查項預(yù)期結(jié)果失敗時的優(yōu)先檢查點Runtime 檢測未裝時自動觸發(fā)靜默安裝注冊表鍵、執(zhí)行權(quán)限首屏加載3 秒內(nèi)出現(xiàn)頁面內(nèi)容UserDataFolder 鎖、紋理更新JS 消息點擊頁面按鈕UE 日志收到 JSONWebMessage 通道、后臺線程焦點切換鍵盤輸入能進(jìn)入網(wǎng)頁輸入模式、SetFocus打包部署無網(wǎng)機(jī)器可運行Evergreen / Fixed 模式選擇從那以后我每次把插件分發(fā)給同事之前都會強(qiáng)制走一遍這張表的流程尤其是焦點切換和打包部署兩項總是最后才暴露問題。檢查完再交付能省掉不少“為什么我這臺機(jī)器不行”的來回溝通。希望幫到你。本文還有配套的精品資源點擊獲取