适用: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] jslib→postMessage→ iframe 内页面 - 网页 → Unity :iframe
postMessage→ 宿主window.message→unityInstance.SendMessage→C#公共方法
跨域时必须用
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 Template 选
Default或 自定义 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 操作:
- 场景中建空物体
WebGLBridge,挂上此脚本 - 填写
Default Url(iframe 要加载的页面) - 不要 改 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)
- 复制模板:
Editor/Data/PlaybackEngines/WebGLSupport/BuildTools/WebGLTemplates/Default
→ 项目Assets/WebGLTemplates/CustomBridge - Player Settings → Resolution and Presentation → WebGL Template 选
CustomBridge - 在
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\""))
{
// 处理按钮点击
}
生产环境建议用
JsonUtility或Newtonsoft.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 对自动播放、弹窗的限制。
六、安全清单(上线必做)
postMessage校验event.origin,禁用裸'*'接收端- 发送端
postMessage(data, targetOrigin)用具体域名,不用* - 勿把
eval、用户输入直接插入 iframe URL - 敏感操作(支付、登录)放在 HTTPS 且由服务端鉴权
- 若嵌入第三方站,确认对方允许被 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.jslib在Assets/Plugins/WebGL/ - 自定义 Template 已在 Player Settings 选中
-
index.html里已init(instance)并监听message - iframe 页面已向
parent.postMessage发消息 - 生产环境已限制
origin - 真机构建 WebGL 测试(编辑器无法测 jslib)
十、原理小结
- WebGL 不能在内部分辨网页像素,内嵌 = DOM 层 iframe 叠在 Canvas 上。
- Unity ↔ 宿主页 走 jslib +
SendMessage经典桥。 - 宿主页 ↔ iframe 在跨域下必须走
postMessage,不能eval跨帧脚本。 - 用 JSON 协议 + 单一 Bridge 物体 可维护性最好,也便于以后加登录、支付、排行榜等消息类型。