【Unity WebGL】内嵌网页与双向通信

适用:Unity 2021.3 LTS+,浏览器端 WebGL 构建

目标:在 Unity WebGL 中内嵌 H5 网页,并实现 Unity ↔ 网页双向通信


目录


一、先理解「能做什么、不能做什么」

1.1 WebGL 的本质

Unity WebGL 构建产物运行在浏览器里:一个 <canvas> 画 3D,外面包一层 index.html

游戏画面在 Canvas 里,普通网页在 DOM 里------这是两套渲染体系。

1.2 常见误区

想法 现实
在 3D 场景里用 RawImage 显示任意网址 WebGL 不能像桌面端那样把网页渲染成纹理
用 WebView 插件 浏览器环境没有原生 WebView,只有 iframe / 新窗口
直接 iframe.contentWindow.eval() 跨域控制 被浏览器禁止(同源策略)

1.3 正确方案:iframe 叠加 + 消息桥

复制代码
┌─────────────────────────────────────────┐
│  index.html(宿主页面)                    │
│  ┌─────────────┐  ┌──────────────────┐  │
│  │ Unity Canvas│  │ <iframe> 内嵌网页 │  │
│  └──────┬──────┘  └────────┬─────────┘  │
│         │    jslib 桥接      │ postMessage│
│         └────────┬───────────┘           │
│              WebGLBridge (C#)            │
└─────────────────────────────────────────┘

数据流:

  • Unity → 网页C#[DllImport] jslibpostMessage → iframe 内页面
  • 网页 → Unity :iframe postMessage → 宿主 window.messageunityInstance.SendMessageC# 公共方法

跨域时必须用 postMessage,这是浏览器官方提供的安全通道。


二、整体架构设计(推荐)

2.1 三层职责

层级 职责
Unity(C#) 业务逻辑、UI 开关、协议解析
jslib(Plugins/WebGL) C# 与 JavaScript 的唯一出口
index.html + iframe 页面 布局、显示网页、收发 postMessage

2.2 通信协议建议

用 JSON 字符串,避免多参数混乱:

json 复制代码
{
  "type": "OPEN_URL",
  "payload": { "url": "https://example.com/page" }
}

约定 type 枚举,Unity 与网页两端各写一个 switch 分发。


三、从零实施步骤

步骤 0:环境准备

  • Unity 2021.3 LTS 或更新(WebGL 支持稳定)
  • Player Settings → WebGL
    • Publishing Settings → Decompression Fallback:若目标服务器不支持 Brotli,按需开启
    • Enable Exceptions :开发期建议 Explicitly Thrown Exceptions Only
  • 需要改 HTML 时:Publishing Settings → WebGL TemplateDefault自定义 Template

步骤 1:创建通信桥接脚本(C#)

在场景中新建空物体,命名 WebGLBridge (名字与 SendMessage 目标一致)。

文件路径建议: Assets/Scripts/WebGLBridge.cs

csharp 复制代码
using System;
using System.Runtime.InteropServices;
using UnityEngine;

public class WebGLBridge : MonoBehaviour
{
#if UNITY_WEBGL && !UNITY_EDITOR
    [DllImport("__Internal")]
    private static extern void WebGL_ShowIframe(string url, int x, int y, int w, int h);

    [DllImport("__Internal")]
    private static extern void WebGL_HideIframe();

    [DllImport("__Internal")]
    private static extern void WebGL_PostToIframe(string json);
#else
    private static void WebGL_ShowIframe(string url, int x, int y, int w, int h) { }
    private static void WebGL_HideIframe() { }
    private static void WebGL_PostToIframe(string json) { }
#endif

    [SerializeField] private string defaultUrl = "https://your-domain.com/embed.html";

    private void Start()
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        WebGL_ShowIframe(defaultUrl, 100, 100, 600, 400);
#endif
    }

    // 供 JS 调用:SendMessage("WebGLBridge", "OnMessageFromWeb", json)
    public void OnMessageFromWeb(string json)
    {
        Debug.Log("[Web] -> [Unity] " + json);
        // 解析 type / payload,驱动游戏逻辑
    }

    public void SendToWeb(string type, string payloadJson)
    {
        var msg = $"{{\"type\":\"{type}\",\"payload\":{payloadJson}}}";
#if UNITY_WEBGL && !UNITY_EDITOR
        WebGL_PostToIframe(msg);
#else
        Debug.Log("[Unity] -> [Web] " + msg);
#endif
    }

    public void CloseWebPanel()
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        WebGL_HideIframe();
#endif
    }
}

原理说明:

  • [DllImport("__Internal")] 只在 WebGL 构建时链接到同名的 JavaScript 函数(写在 .jslib 里)。
  • SendMessage 只能调用 public 且参数为 string / int / float 的方法,复杂对象请传 JSON 字符串。
  • #if UNITY_WEBGL && !UNITY_EDITOR:编辑器里 jslib 不存在,用空实现避免报错。

Inspector 操作:

  1. 场景中建空物体 WebGLBridge,挂上此脚本
  2. 填写 Default Url(iframe 要加载的页面)
  3. 不要 改 GameObject 名字,或改了以后 JS 里 SendMessage 的第一个参数也要改

步骤 2:创建 jslib 插件

文件路径: Assets/Plugins/WebGL/WebGLBridge.jslib

javascript 复制代码
mergeInto(LibraryManager.library, {
  WebGL_ShowIframe: function (urlPtr, x, y, w, h) {
    var url = UTF8ToString(urlPtr);
    window.__unityWebBridge.showIframe(url, x, y, w, h);
  },
  WebGL_HideIframe: function () {
    window.__unityWebBridge.hideIframe();
  },
  WebGL_PostToIframe: function (jsonPtr) {
    var json = UTF8ToString(jsonPtr);
    window.__unityWebBridge.postToIframe(json);
  }
});

原理: Unity 会把 mergeInto(LibraryManager.library, {...}) 里的函数编进最终 wasm 的导入表,C# 的 DllImport 名必须完全一致


步骤 3:自定义 WebGL 模板(宿主 HTML)

  1. 复制模板:
    Editor/Data/PlaybackEngines/WebGLSupport/BuildTools/WebGLTemplates/Default
    → 项目 Assets/WebGLTemplates/CustomBridge
  2. Player Settings → Resolution and Presentation → WebGL TemplateCustomBridge
  3. index.html</body> 前加入桥接脚本(在 createUnityInstance 成功之后 再绑定):
javascript 复制代码
window.__unityWebBridge = {
  unityInstance: null,
  iframe: null,

  showIframe: function (url, x, y, w, h) {
    this.hideIframe();
    var iframe = document.createElement('iframe');
    iframe.id = 'unity-embed-iframe';
    iframe.src = url;
    iframe.style.cssText =
      'position:fixed;left:' + x + 'px;top:' + y + 'px;' +
      'width:' + w + 'px;height:' + h + 'px;border:0;z-index:9999;';
    document.body.appendChild(iframe);
    this.iframe = iframe;
  },

  hideIframe: function () {
    var old = document.getElementById('unity-embed-iframe');
    if (old) old.remove();
    this.iframe = null;
  },

  postToIframe: function (json) {
    if (!this.iframe || !this.iframe.contentWindow) return;
    // 生产环境请把 '*' 换成具体 origin
    this.iframe.contentWindow.postMessage(json, '*');
  },

  init: function (instance) {
    this.unityInstance = instance;
    var self = this;
    window.addEventListener('message', function (e) {
      // 安全:校验来源
      // if (e.origin !== 'https://your-trusted.com') return;
      if (typeof e.data !== 'string') return;
      if (!self.unityInstance) return;
      self.unityInstance.SendMessage('WebGLBridge', 'OnMessageFromWeb', e.data);
    });
  }
};

// 在 createUnityInstance(...).then((instance) => { ... }) 里追加:
// window.__unityWebBridge.init(instance);

关键点(常见踩坑):

问题 说明
unityInstance is not defined 必须在 createUnityInstance().then() 里赋值后再调用
SendMessage 无反应 第一个参数是 GameObject 名字,不是脚本类名
iframe 错位 position: fixed + 高 z-index;可按 Canvas getBoundingClientRect() 动态定位

步骤 4:内嵌网页(iframe 内)收发消息

文件示例: embed.html(与 Unity 可不同域,但必须配合 postMessage

html 复制代码
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Embed</title>
</head>
<body>
  <button id="btn">通知 Unity</button>
  <script>
    // 接收 Unity -> 网页
    window.addEventListener('message', function (e) {
      // if (e.origin !== 'https://unity-host.com') return;
      var msg = JSON.parse(e.data);
      console.log('from Unity', msg);
      if (msg.type === 'PING') {
        parent.postMessage(JSON.stringify({ type: 'PONG', payload: {} }), '*');
      }
    });

    document.getElementById('btn').onclick = function () {
      parent.postMessage(JSON.stringify({
        type: 'BUTTON_CLICK',
        payload: { id: 'btn' }
      }), '*');
    };
  </script>
</body>
</html>

跨域原理:

父页面无法读 iframe 的 DOM,但 postMessage 只传字符串 ,浏览器允许。双方监听 message 事件并校验 event.origin


步骤 5:Unity 侧调用示例

csharp 复制代码
// 发消息给内嵌页
bridge.SendToWeb("PING", "{}");

// 关闭面板
bridge.CloseWebPanel();

OnMessageFromWeb 里:

csharp 复制代码
// 简单解析示例
if (json.Contains("\"type\":\"BUTTON_CLICK\""))
{
    // 处理按钮点击
}

生产环境建议用 JsonUtilityNewtonsoft.Json 做正式反序列化。


四、双向通信方式对照

方向 机制 说明
JS → Unity unityInstance.SendMessage(go, method, arg) string / int / float;GameObject 名必须存在
Unity → JS jslib 调 window.xxx() 推荐集中放在 __unityWebBridge
iframe ↔ 宿主 postMessage 跨域唯一稳妥方案
旧 API Application.ExternalCall 已过时,新项目勿用

五、布局与交互进阶

5.1 iframe 对齐 Canvas 区域

showIframe 里读取 Canvas 位置:

javascript 复制代码
var canvas = document.querySelector('#unity-canvas');
var r = canvas.getBoundingClientRect();
iframe.style.left = (r.left + offsetX) + 'px';
iframe.style.top = (r.top + offsetY) + 'px';

窗口缩放时监听 resize,重新计算,避免 iframe 与 3D UI「错位」。

5.2 输入穿透

iframe 会挡住下方 Canvas 的鼠标事件------这是预期行为。

需要「只显示网页、点击仍给游戏」时,可对 iframe 设 pointer-events: none(但网页内按钮也会点不到),一般要 分区域:游戏区纯 Canvas,网页区纯 iframe。

5.3 全屏

Unity 全屏 (SetFullscreen) 后 DOM 结构变化,iframe 可能需重新定位或暂时 hideIframe()

5.4 移动端

小屏建议 iframe 全屏覆盖或独立路由页;注意 iOS Safari 对自动播放、弹窗的限制。


六、安全清单(上线必做)

  1. postMessage 校验 event.origin ,禁用裸 '*' 接收端
  2. 发送端 postMessage(data, targetOrigin) 用具体域名,不用 *
  3. 勿把 eval、用户输入直接插入 iframe URL
  4. 敏感操作(支付、登录)放在 HTTPS 且由服务端鉴权
  5. 若嵌入第三方站,确认对方允许被 iframe(X-Frame-Options / Content-Security-Policy: frame-ancestors

七、调试流程

现象 排查
unityInstance is not defined 是否在 createUnityInstance().then 之后才调用
SendMessage 无反应 GameObject 名、方法是否 public、是否拼写一致
jslib 函数找不到 .jslib 是否在 Plugins/WebGL,函数名是否与 DllImport 一致
iframe 空白 控制台看是否被 X-Frame-Options 拒绝
收不到 postMessage 是否 JSON.stringify、origin 是否被过滤
编辑器正常、构建失败 是否忘了 #if UNITY_WEBGL 包裹 DllImport

调试技巧: 浏览器 F12 → Console 看 JS 日志;Unity 用 Debug.Log;iframe 与父页可用不同 DevTools 上下文(Chrome 可选 frame)。


八、可选方案对比

方案 适用 缺点
iframe + postMessage(本篇) 内嵌 H5、活动页、商城 不能进 3D 纹理
仅改 index.html,不用 iframe 上传文件、登录按钮 不是「内嵌网页」,是外壳控件
新窗口 window.open 完整第三方站 非内嵌
gree/unity-webview 等插件 多端 WebView WebGL 仍多为 iframe 思路
UI Toolkit + 外链图片 只展示静态 H5 截图 非实时网页

九、最小检查清单(发布前)

  • 场景中存在名为 WebGLBridge 的物体且挂脚本
  • WebGLBridge.jslibAssets/Plugins/WebGL/
  • 自定义 Template 已在 Player Settings 选中
  • index.html 里已 init(instance) 并监听 message
  • iframe 页面已向 parent.postMessage 发消息
  • 生产环境已限制 origin
  • 真机构建 WebGL 测试(编辑器无法测 jslib)

十、原理小结

  1. WebGL 不能在内部分辨网页像素,内嵌 = DOM 层 iframe 叠在 Canvas 上。
  2. Unity ↔ 宿主页 走 jslib + SendMessage 经典桥。
  3. 宿主页 ↔ iframe 在跨域下必须走 postMessage ,不能 eval 跨帧脚本。
  4. JSON 协议 + 单一 Bridge 物体 可维护性最好,也便于以后加登录、支付、排行榜等消息类型。

相关推荐
图扑软件9 小时前
WebGIS 零碳智能港口态势感知与 VR 远控系统
物联网·自动化·vr·webgl·数字孪生·3d 渲染
zyh______10 小时前
C#语法糖(按照实用性排序)
unity·c#
avi911112 小时前
[AI教做人]CinemachineCamera 各项调整配置参数入门 + Profiler
unity·游戏开发·camera·cinemachine·visual camera·镜头配置
winfredzhang12 小时前
WebGL 2 实战:单文件移植与扩展 Heartfelt 玻璃雨滴折射效果
webgl·雨滴·hml·水雾
郝学胜-神的一滴2 天前
[简化版 GAMES 101] 计算机图形学 16:纹理走样、Mipmap、三线性插值、各向异性过滤原理全解
unity·游戏引擎·godot·图形渲染·three.js·opengl·unreal
2301_767113982 天前
Superpowers 游戏引擎从零开发实战指南
游戏引擎
吴梓穆2 天前
untiy TextMeshPro (TMP) text 操作 中文字符集 字体材质操作(添加纹理 添加描边 添加阴影)
unity
想你依然心痛2 天前
嵌入式单元测试:Unity/CMock框架与硬件在环测试——测试驱动、桩函数
unity·单元测试·游戏引擎
yangmu32032 天前
《星露谷物语》MOD配置与实战安装综合指南
游戏·游戏引擎·游戏程序