十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Unity WebGL 双向通信详解:jslib 与 SendMessage 实战指南

Unity WebGL 双向通信详解:jslib 与 SendMessage 实战指南 简介本资源面向需要在Unity WebGL项目与Web页面之间实现双向通信的Unity开发者提供了一套开箱即用的通信工具脚本和配套测试Demo。压缩包共21个文件大小仅3.89MB包含核心js/css/html前端交互文件、unitypackage插件包、8张流程示意截图以及RTF说明文档结构紧凑方便直接导入项目参考。资源已有5168人学习/下载实用性获社区认可。借助包内Demo可通过实际运行快速理解互发消息、回调处理的完整链路同时作者还提供了图文教程链接遇到接入问题可按步骤排查有效降低WebGL与前端通信的集成门槛。整体小巧但覆盖关键场景适合初中级Unity开发者快速上手。1. 为什么 Unity WebGL 的通信总在“第一次调用”上翻车Unity 打包成 WebGL 之后产物是一堆 .wasm、.data 加一层 JS 胶水跑在浏览器主线程里。前端工程师想让它跟页面上已有的 Vue/React 面板对话Unity 工程师想把游戏里的状态抛给外层 DOM听起来就是互相调个函数。真上手才发现报错往往只有一句SendMessage: object xxx not found!或者干脆静默失败。根子在于双方隔着一条窄接口C# 经 IL2CPP 编译进了 WebAssembly前端留在 JS 引擎里中间只有 .jslib 和 SendMessage 这两根管子。管子本身不复杂复杂的是类型、调用时机和实例引用这三件事。下面按“机制—单向—双向—进阶”的顺序把这条链路走一遍代码可以直接抄。2. Unity WebGL 通信的三条通道与最小可跑通示例2.1 从 C# 到 WebAssembly 的编译链路Unity 把 C# 编成 ILIL2CPP 转成 C最后 Emscripten 编成 wasm。这条链路上浏览器不认识 C#也不认识 GameObject。Unity 在 WebGL 平台留了两个口子一个从 C# 往 JS 走靠[DllImport(__Internal)]加自定义 .jslib 插件另一个从 JS 往 C# 走靠unityInstance.SendMessage。前者编译期绑定写错方法名直接链接失败后者运行期按名字查找写错只会打日志。失败表现完全不同选型前先分清这两件事。2.2 三条通道的对照通道方向绑定时机参数能力常见用途[DllImport(__Internal)] .jslibUnity → JS编译期字符串/数值指针传递调前端接口、上报事件unityInstance.SendMessageJS → Unity运行期查名字单个 string/int/float前端下发指令Application.ExternalCallUnity → JS运行期单个字符串老项目遗留新项目别用结论先给新项目统一走 .jslib 加 SendMessage 这一对。Application.ExternalCall在较新的 loader 里已经拿不到对应实现网上不少教程还在用它照抄会卡在“明明代码没报错但前端收不到”上。2.3 一个 .jslib 的完整写法目录结构固定成这样Assets/ Plugins/ WebGL/ web-bridge.jslib Scripts/ WebBridge.csweb-bridge.jslib的内容mergeInto(LibraryManager.library, { // 键名必须和 C# DllImport 里的方法名一字不差 PostToWeb: function (typePtr, payloadPtr) { var type UTF8ToString(typePtr); var payload UTF8ToString(payloadPtr); // 前端可能还没注册好先兜一层队列 if (window.UnityBridge typeof window.UnityBridge.onMessage function) { window.UnityBridge.onMessage(type, payload); } else { window.__unityInbox window.__unityInbox || []; window.__unityInbox.push([type, payload]); } } });mergeInto的第一个参数固定是LibraryManager.library第二个对象里的键名就是 C# 侧要DllImport的名字改一个字母就链接失败。UTF8ToString负责把 wasm 堆里的字符串指针读成 JS 字符串早期版本叫Pointer_stringify看到旧教程别照抄。写进__unityInbox的那段是给首次调用兜底的Unity 实例创建完成的时间点和前端createUnityInstance().then()回调不是同一时刻先到的消息丢进数组等前端注册完再补发。2.4 .jslib 的放置规则与构建约束.jslib必须放在Assets/Plugins/WebGL/下才会被识别放别处 Unity 直接忽略也不会给任何警告。扩展名必须是.jslib文件名随意。改完必须重新打包Play 模式不会重新加载这个文件。还有一点.jslib 里的代码会被 Emscripten 打进框架层不要在里面直接引用打包时还不存在的第三方库需要外部库就通过运行时注入的全局变量访问。3. Unity 主动通知前端DllImport 声明、条件编译与事件分发3.1 C# 侧写法与编辑器降级编辑器环境里没有__Internal这个模块直接跑会抛DllNotFoundException。所以声明必须用条件编译包住并且给编辑器留一条降级路径using System.Runtime.InteropServices; using UnityEngine; public class WebBridge : MonoBehaviour { // 只在 WebGL 且非编辑器下声明否则链接期找不到 __Internal #if UNITY_WEBGL !UNITY_EDITOR [DllImport(__Internal)] private static extern void PostToWeb(string type, string payload); #endif // 给 JS 调用时必须 public方法名大小写敏感 public void OnMessageFromWeb(string json) { Debug.Log(收到前端消息: json); // 解析和分发逻辑见 4.3 的信封协议 } public void Send(string type, string payloadJson) { #if UNITY_WEBGL !UNITY_EDITOR PostToWeb(type, payloadJson); #else Debug.Log($[WebBridge-Editor] {type} {payloadJson}); #endif } }参数说明type是路由用的短字符串命名用点号分段比如score.changed、scene.loadedpayloadJson是业务数据的 JSON 文本。两个都走 string原因是 DllImport 跨越到 JS 边界时字符串会被 marshal 成指针传对象或数组不会自动序列化。序列化用JsonUtility它只认可序列化字段字典和接口不支持需要传字典就先拼成数组再包一层。3.2 前端注册统一入口转成 CustomEvent不要在 .jslib 里直接调业务函数而是在前端注册一个统一入口把消息转成 DOM 事件抛出去。前端的组件通信里常见的 props 传递、事件总线在这里统一被window.dispatchEvent替代好处是 Vue、React 或原生页面都能挂同一套监听createUnityInstance(canvas, config).then((unityInstance) { window.unityInstance unityInstance; window.UnityBridge { onMessage(type, payload) { let data null; try { data JSON.parse(payload); } catch (e) { console.warn([unity] payload 不是合法 JSON, type, payload); } // 统一转 CustomEvent各组件自行监听 window.dispatchEvent(new CustomEvent(unity: type, { detail: data })); } }; // 补发 .jslib 里缓存的首批消息 (window.__unityInbox || []).forEach(([t, p]) window.UnityBridge.onMessage(t, p)); window.__unityInbox []; });前端某个业务组件里这样接// 监听 Unity 抛出的分数变化 window.addEventListener(unity:score.changed, (e) { store.commit(setScore, e.detail.score); });unity:前缀用来避开页面上已有的事件命名。用 CustomEvent 而不是维护一个回调数组好处是监听方能在任意组件生命周期里挂载和卸载不用和 Unity 实例的加载时序对齐。分发给 Vue 的 store 还是 React 的 dispatch桥接层不关心。3.3 编辑器与真机的双轨验证常见做法是抽一个IWebChannel接口WebGL 实现走 DllImport编辑器实现走Debug.Log加本地事件。业务代码不写#if只在装配阶段选实现。先在编辑器里把消息流跑一遍再看一次打包后在浏览器控制台的日志两边格式对齐能省掉大量“改一行打一次包”的来回。小游戏平台的模板和 Web 模板不是同一份桥接层要各自验一遍别拿 Web 上的结果直接推。提示每次改 .jslib 都要重新 BuildUnity 不会在 Play 模式下重新加载该文件。4. 前端给 Unity 发消息createUnityInstance、SendMessage 与协议设计4.1 拿到 unityInstance 并挂到全局Unity 2020.1 之后官方模板从UnityLoader.instantiate换成了createUnityInstance返回 Promiseconst canvas document.querySelector(#unity-canvas); const config { dataUrl: Build/xxx.data, frameworkUrl: Build/xxx.framework.js, codeUrl: Build/xxx.wasm, streamingAssetsUrl: StreamingAssets, companyName: Demo, productName: Demo, productVersion: 1.0 }; createUnityInstance(canvas, config, (progress) { // progress 是 0~1 的加载进度可以喂给进度条 console.log(加载中, (progress * 100).toFixed(1) %); }).then((unityInstance) { // 存到全局页面其它模块直接用不要重复 instantiate window.unityInstance unityInstance; });参数表里frameworkUrl容易写错它的扩展名在新版本里从.js变成了.framework.js从旧模板复制过来的路径会 404而浏览器控制台的报错只会说加载失败不会提示是文件名变了。如果页面用的是自己改过的模板createUnityInstance可能内联在 index.html 里把返回值挂到 window 上即可。4.2 SendMessage 的参数与边界// 三个参数GameObject 名字、方法名、参数值 window.unityInstance.SendMessage( Bridge, OnMessageFromWeb, JSON.stringify({ type: ui.open, payload: { panel: rank } }) );几条硬约束第一个参数是场景里激活的 GameObject 名字不是脚本类名也不是 Tag。重名时行为不可预期建议挂一个专用空物体名字全局唯一比如Bridge。第二个参数是方法名大小写敏感方法必须public且定义在挂在那个 GameObject 上的 MonoBehaviour 里。第三个参数只支持 string、int、float。传 object 会被隐式转成[object Object]所以统一用 JSON 字符串。调用瞬间该 GameObject 必须处于激活状态否则打印SendMessage: object Bridge not found!然后丢弃消息。对应的 C# 接收端[System.Serializable] public class Command { public string type; // 消息类型如 ui.open public string payload; // 业务数据的 JSON 文本 } public class Bridge : MonoBehaviour { public void OnMessageFromWeb(string json) { var cmd JsonUtility.FromJsonCommand(json); switch (cmd.type) { case ui.open: /* 打开面板 */ break; case ui.close: /* 关闭面板 */ break; default: Debug.LogWarning(未知指令 cmd.type); break; } } }4.3 用信封统一双向协议两边都照上面的写法铺开之后格式会散得到处都是。我一般会定义一个统一信封双方各维护一份类型常量表字段类型说明typestring消息类型点号分隔如ui.openpayloadstring业务内容的 JSON 文本idstring可选请求-响应模式下的关联 idtsnumber毫秒时间戳排查时序问题用信封最大的好处是 .jslib 那层完全不用知道业务只做透传新增消息类型不需要动桥接代码。代价是每次消息要序列化两次对象→字符串→指针→字符串这个开销在第 5 章里压。5. 双向通信的进阶请求响应、大 payload 与构建后自检5.1 给 SendMessage 补一个请求-响应模式SendMessage 是单向的前端发完就没了。要做“调用并等结果”得在信封里带 idUnity 处理完再用 .jslib 把同 id 的结果发回来// 前端发一条带 id 的消息并等待回包 function callUnity(type, payload, timeout 5000) { const id r Date.now() Math.random().toString(36).slice(2, 8); return new Promise((resolve, reject) { const timer setTimeout(() { window.removeEventListener(unity:reply, handler); reject(new Error(unity 响应超时: type)); }, timeout); function handler(e) { if (e.detail.id ! id) return; clearTimeout(timer); window.removeEventListener(unity:reply, handler); resolve(e.detail.payload); } window.addEventListener(unity:reply, handler); window.unityInstance.SendMessage( Bridge, OnMessageFromWeb, JSON.stringify({ id, type, payload }) ); }); }C# 侧收到带 id 的消息后把结果通过Send(reply, ...)回传前端只要在UnityBridge.onMessage里对reply类型额外抛一次unity:reply事件即可。超时值按业务定加载大场景给到 10 秒以上普通 UI 指令 1 到 3 秒够用。id 用时间戳加随机后缀避免同一毫秒内多次调用撞号。5.2 大 payload 的序列化与内存开销一次传几百 KB 的 JSON两边都会卡一下原因是字符串在 wasm 堆和 JS 堆里各存了一份UTF8ToString还要逐字节扫。能拆就拆成多条小消息能传索引就别传整表。确实要传大块数据时可以绕开字符串转换直接在堆上读写mergeInto(LibraryManager.library, { // 按指针和长度读原始字节省掉 UTF8ToString 的额外扫描 PostBinaryToWeb: function (ptr, len) { // 从 Emscripten 堆视图上切片slice 不能省 var bytes new Uint8Array(HEAPU8.buffer, ptr, len).slice(); window.UnityBridge.onBinary(bytes); } });HEAPU8是 Emscripten 暴露的堆视图。slice()那一步不能省——指针指向的是 Unity 的内存池函数返回后这块内存随时会被复用直接持有视图会读到脏数据。C# 侧拼指针要用GCHandle或把数组固定住写起来比字符串版本麻烦只在确实要传二进制或大数组时才用。5.3 构建后对着三处自检打包产物结构变了、模板被改过通信最容易静默失败。上线前扫这三处检查项怎么看异常表现.jslib 是否进了 framework.js在产物里搜PostToWeb搜不到说明 .jslib 没被识别检查是否在Assets/Plugins/WebGL/unityInstance是否挂到全局控制台执行window.unityInstance返回 undefined模板没暴露需要手动改 index.htmlGameObject 名字是否唯一在场景里按名字搜索重名时 SendMessage 可能打到另一个对象上行为随机这些检查做完最后一步一定是在真实浏览器里跑一遍而不是只在编辑器点 Play。WebGL 平台特有的一些毛病——比如用 IDBFS 做数据持久化时数据目录写不进去、音频自动播放被拦——只在浏览器里才出现编辑器完全看不见。真机浏览器加一次完整流程的验证比在编辑器里改十次代码管用。本文还有配套的精品资源点击获取
返回列表