ARTICLE DETAIL

资讯详情

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

Unity WebGL接入MQTT:基于MQTT.js与jslib的完整桥接方案

Unity WebGL接入MQTT:基于MQTT.js与jslib的完整桥接方案 简介这是一份面向Unity开发者的完整项目资料专注解决如何将Unity项目打包为WebGL并接入MQTT协议实现实时通信的问题适合有基础Unity使用经验、希望拓展网页端IoT交互能力的开发者参考学习。压缩包共约2000个文件解压后约295MB主体包含大量C#脚本、材质、模型、图片、音频等Unity工程资源以及DLL插件、预设体、场景配置和Packages依赖目录可清晰看到MQTT客户端集成、WebGL平台设置与项目组织方式。已有701人学习。通过研究这份工程可以掌握Unity WebGL导出的完整流程、MQTT主题订阅与消息发布的具体写法并理解Assets、ProjectSettings、Packages等目录中各类文件在Unity项目中的作用为自行开发可运行在浏览器中的3D应用并联动远程设备提供可直接参考的样例。 把Unity项目打包成WebGL让用户在浏览器里直接打开运行这个流程现在相当成熟了。但一旦你的项目需要跟服务器做实时通信而且选的是MQTT这套物联网协议马上就会遇到一个让很多人挠头的现象桌面端用得好好的MQTTnet明明编译也过了打包成WebGL后就怎么也连不上Broker。浏览器控制台不是报Connection failed就是报跨域错误翻来覆去查不出个所以然。问题基本不在你的业务代码而是WebGL这套运行环境在传输层上就被“掐了腿”浏览器沙箱不允许页面建立原生TCP Socket而绝大多数Unity MQTT库依赖的恰恰就是TCP。这篇文章我会把我验证过的接入思路、完整代码和打包配置都放出来适合正在做数字孪生大屏、IoT设备管理后台、或者任何需要Unity WebGL跟消息服务交互的开发者参考看完直接照抄就行。1. 真凶不在业务代码浏览器沙箱改写了网络层规则1.1 为什么桌面端写好的MQTT代码一到WebGL就“失灵”先说现象。在Unity Editor或者PC打包版本里用MQTTnet连接Broker没有任何问题收发消息都正常。然后切到WebGL平台打包页面启动正常UI渲染正常唯独MQTT那一块全挂。点按钮执行连接等了几秒超时Broker侧也看不到连接日志。第一次遇到时我也以为是代码问题反复检查了Broker地址、端口、用户名密码甚至怀疑是跨域策略花了一整天。后来把问题定位到WebGL的运行时环境才反应过来。Unity WebGL不是“Unity的一种输出格式”这么简单它本质上是把你的C#代码用IL2CPP编译成WebAssembly跑在浏览器渲染引擎提供的一个隔离沙箱里。这个沙箱出于安全考虑没有提供原生TCP Socket能力。也就是说C#里System.Net.Sockets这条链路在WebGL下根本不可用。MQTTnet底层依赖Socket虽然编译能过但运行时只要走到建立连接的代码要么直接抛PlatformNotSupportedException要么因为浏览器根本没有这个API而失败。1.2 WebSocket是WebGL环境下的“网线”搞清楚原因之后思路就清晰了既然浏览器只提供WebSocket、XMLHttpRequest这些上层网络能力那MQTT就得通过这些通道传输。MQTT本身是应用层协议它并不关心底层是TCP还是WebSocket。只要有一个可靠的、双向的、有序的传输通道MQTT报文就能完整跑起来。WebSocket恰好满足这个要求。所以社区的标准做法就是走MQTT over WebSocketBroker地址从tcp://变成ws://报文的收发逻辑完全不变。这里有个生活化的类比TCP好比专线货车MQTT报文是货箱桌面端直接用货车上货到了WebGL这里专线不通了只能换成WebSocket这辆“快递车”货箱还是那个货箱但运输方式变了。你的Broker需要开启WebSocket监听端口比如EMQX默认提供8083端口mosquitto则需要单独配置web socket listener。传输方式桌面端/移动端浏览器WebGL适用场景原生TCP支持不支持MQTTnet直连BrokerWebSocket支持支持MQTT over WS浏览器唯一顺路方案UDP支持不支持实时音视频等和MQTT无关理解这个前提之后后面的所有方案都围绕“如何在Unity WebGL里发送WebSocket的MQTT流量”展开。2. 打包环境的几个关键开关没配好后面全白搭2.1 先确认WebGL Build Support装没装这道工序很基础但经常有人忽略。在Unity Hub里选中项目对应的Unity版本在Installs页签点“Add modules”把WebGL Build Support勾上。装了之后Unity编辑器里Build Settings的Platform列表才会出现WebGL选项否则你连WebGL目标都切不过去。如果项目从其他团队拉下来版本不一致也容易出问题。我建议固定Unity版本比如用长期支持版Unity 2022 LTS或Unity 6 LTS模块也保持一致不然A机器打包的产物到B机器上重新打包效果可能不一样。2.2 Player Settings里几个影响成败的配置切到WebGL平台后打开Player Settings几个配置项我强烈建议先调好别等跑起来再一个个填坑。配置项位置推荐值说明Compression FormatPublishing SettingsBrotli包体最小首次进页面加载稍慢体验最好WebGL Memory SizeOther Settings512MB起步默认256MB对大项目很容易触发内存不足Run In BackgroundResolution and Presentation勾选页面切到后台时Unity继续运行心跳不中断Exception HandlingOther SettingsExplicitly Thrown Exceptions OnlyFull模式调试方便但严重影响性能Data CachingPublishing Settings建议开启缓存wasm/data二次刷新加载更快内存大小这个配置比较容易被忽视。WebGL页面的内存是启动时预分配的你设置成512MB浏览器就会给Unity分配512MB的ArrayBuffer。如果实际跑起来内存需求量超过这个值Unity运行时不会自动动态扩容而是直接崩掉。项目里有大量贴图、运行时加载的AssetBundle建议直接设成1GB。当然设置太大会影响低端手机的页面稳定性要根据目标设备权衡。2.3 代码层面的兼容性检查在写MQTT桥接之前先把工程里其他可能出问题的代码过一遍。Unity WebGL下多线程相关的代码需要格外小心虽然Unity 2022之后支持了部分多线程模拟但系统层面的线程API是不完整的用unsafe关键字、直接操作指针、依赖System.Threading的库都要重点排查。文件读写也用不了一定伤脑筋PlayerPrefs在WebGL下底层是LocalStorage能用但容量有限。排查技巧先把非核心的系统全关掉只留UI和MQTT打包一个最小可运行版本。如果这个版本正常再逐步把业务模块加回来这样能快速定位出是哪些代码在WebGL下不兼容。3. 两条MQTT接入路线我为什么最终选了JS桥接3.1 路线AMQTT.js jslib插件桥接这是目前社区里最主流、也最稳妥的方案。思路是在WebGL的JavaScript环境里引入MQTT.js这个成熟的MQTT客户端库然后通过Unity的jslib插件机制用C#代码调用JS函数完成连接、订阅、发布操作。JS侧的on message事件再反向调用C#方法把消息传回Unity。MQTT.js是浏览器环境下的事实标准重连逻辑、心跳KeepAlive、QoS支持都很完善。而且它的包体很小压缩后大概40KB左右对WebGL产物体积的影响可以忽略。3.2 路线B在C#层直接实现MQTT over WebSocket另一种思路是找纯C#的MQTT实现比如M2Mqtt的某些分支或者自己基于WebSocket库再造轮子。M2Mqtt的WebGL分支确实存在但维护状况参差不齐而且在WebGL环境的测试覆盖相当有限。自己实现协议的话MQTT控制包的各种Flag、可变头部、剩余长度编码规则细节很多光调通QoS 1的ACK流程就够折腾好几天。3.3 选型对比和最终结论两条路线整理成一张表看得更清楚对比项MQTT.js jslibC#层WebSocket自研协议功能完整性成熟完善需要自己补齐自动重连与心跳内置配置项需要自行实现包体体积影响约40KB视实现复杂度而定排错难度可直接在Console调MQTT.js日志排查链路长长期维护成本低高我最后选了MQTT.js。原因很简单这是个被几十万生产环境验证过的客户端我没理由在WebGL这种受限环境里再去自造一个MQTT协议实现。真正需要我做的只是把Unity C#和JS之间这座桥搭好。开发时我采用双轨策略在Unity Editor里继续用MQTTnet直连TCP方便调试只有在WebGL构建时才走js桥接。用宏区分即可#if UNITY_WEBGL !UNITY_EDITOR // 走JS桥接 #else // 走MQTTnet #endif这个策略能保证开发效率不至于每次测试都得打包一次WebGL。4. MQTT.js jslib桥接的完整实现4.1 文件组织与自定义WebGL模板先准备三个文件Assets/Plugins/WebGL/MQTTPlugin.jslibC#调JS的桥接层Assets/Scripts/WebGLMqttBridge.csC#侧的封装脚本Assets/WebGLTemplates/MQTTTemplate/index.html自定义WebGL模板mqtt.min.js文件放在Assets/WebGLTemplates/MQTTTemplate/目录下和index.html同级。因为模板目录下的文件构建时会被原样复制到输出根目录这样在模板里直接写script srcmqtt.min.js/script就能正确加载。自定义模板不用从零写建议从Unity引擎自带的默认WebGL模板改起。关键改动就两处第一引入mqtt.min.js第二把Unity实例暴露到全局变量这样jslib里才能通过window.unityInstance.SendMessage调用C#方法。在默认模板找到createUnityInstance(...).then(unityInstance {...})这段改成script srcmqtt.min.js/script script var unityInstance null; // ... 原有配置代码 ... createUnityInstance(canvas, config, (progress) { // ... 原有进度条逻辑 ... }).then(function (instance) { unityInstance instance; }).catch(function (message) { alert(message); }); /script然后在Unity的Project Settings里把WebGL模板切换到MQTTTemplate命名要对应。4.2 jslib脚本连接、订阅、发布桥接jslib文件是Unity WebGL插件体系里的核心写起来思路比较直观C#声明一个[DllImport(__Internal)]的extern方法jslib里实现同名方法运行时Unity会把它们绑在一起。注意jslib里的方法名必须和C#里完全一致。mergeInto(LibraryManager.library, { MQTT_Connect: function (urlPtr, clientIdPtr, objectNamePtr, methodNamePtr) { var url UTF8ToString(urlPtr); var clientId UTF8ToString(clientIdPtr); var objectName UTF8ToString(objectNamePtr); var methodName UTF8ToString(methodNamePtr); if (window.__mqttClient ! null) { window.__mqttClient.end(true); window.__mqttClient null; } var client mqtt.connect(url, { clientId: clientId, keepalive: 30, reconnectPeriod: 3000, connectTimeout: 5000, clean: true }); window.__mqttClient client; client.on(connect, function () { if (window.unityInstance) { window.unityInstance.SendMessage(objectName, OnMqttConnected); } }); client.on(message, function (topic, payload) { var msg JSON.stringify({ topic: topic, payload: payload.toString() }); if (window.unityInstance) { window.unityInstance.SendMessage(objectName, methodName, msg); } }); client.on(error, function (error) { console.error([MQTT] error:, error); }); window.addEventListener(beforeunload, function () { if (window.__mqttClient) { window.__mqttClient.end(true); } }); }, MQTT_Subscribe: function (topicPtr, qos) { var topic UTF8ToString(topicPtr); if (window.__mqttClient) { window.__mqttClient.subscribe(topic, { qos: qos }); } }, MQTT_Publish: function (topicPtr, messagePtr, qos, retained) { var topic UTF8ToString(topicPtr); var message UTF8ToString(messagePtr); if (window.__mqttClient) { window.__mqttClient.publish(topic, message, { qos: qos, retain: retained 1 }); } }, MQTT_Disconnect: function () { if (window.__mqttClient) { window.__mqttClient.end(true); window.__mqttClient null; } } });几个细节说明一下。UTF8ToString是Unity WebGL提供给jslib的辅助函数负责把C#传过来的字符串指针转成JS字符串。mqtt.connect返回的client实例我挂到window.__mqttClient上避免被GC回收。beforeunload事件里主动断开连接防止用户关闭页面时Broker侧还保持着一个半死不活的连接。4.3 C#封装对外提供一个干净的接口C#侧做两层封装底层是DllImport声明上层是供业务逻辑调用的公开方法对外暴露事件即可业务层不用关心底层走的是TCP还是WebSocket。using System; using System.Runtime.InteropServices; using UnityEngine; public class WebGLMqttBridge : MonoBehaviour { public static WebGLMqttBridge Instance { get; private set; } public event Action OnConnected; public event Actionstring, string OnMessageReceived; [DllImport(__Internal)] private static extern void MQTT_Connect(string url, string clientId, string objectName, string methodName); [DllImport(__Internal)] private static extern void MQTT_Subscribe(string topic, int qos); [DllImport(__Internal)] private static extern void MQTT_Publish(string topic, string message, int qos, int retained); [DllImport(__Internal)] private static extern void MQTT_Disconnect(); [Serializable] private class MqttMessageData { public string topic; public string payload; } private void Awake() { Instance this; } public void Connect(string wsUrl, string clientId) { #if UNITY_WEBGL !UNITY_EDITOR MQTT_Connect(wsUrl, clientId, gameObject.name, OnMqttMessage); #else Debug.Log([WebGLMqttBridge] Editor模式未实现请使用桌面端MQTT客户端); #endif } public void Subscribe(string topic, int qos 0) { #if UNITY_WEBGL !UNITY_EDITOR MQTT_Subscribe(topic, qos); #endif } public void Publish(string topic, string message, int qos 0, bool retained false) { #if UNITY_WEBGL !UNITY_EDITOR MQTT_Publish(topic, message, qos, retained ? 1 : 0); #endif } public void Disconnect() { #if UNITY_WEBGL !UNITY_EDITOR MQTT_Disconnect(); #endif } /// summary /// 由JS通过SendMessage回调 /// /summary public void OnMqttConnected() { OnConnected?.Invoke(); } /// summary /// 由JS通过SendMessage回调json格式 {topic:,payload:} /// /summary public void OnMqttMessage(string json) { try { var data JsonUtility.FromJsonMqttMessageData(json); OnMessageReceived?.Invoke(data.topic, data.payload); } catch (Exception e) { Debug.LogError($[WebGLMqttBridge] 解析消息失败: {e.Message}); } } }把WebGLMqttBridge挂到一个专门的GameObject上名字建议就叫“MqttBridge”保证场景里不重名因为jslib里SendMessage依赖对象名定位。4.4 如何在本地Edge/Chrome里验证本地调试时我先起一个EMQX Docker容器开启1883和8083端口然后在Unity里写个最简单的UI按钮点击后调用Connect(ws://localhost:8083/mqtt, webgl-client-001)。这里注意MQTT.js通过ws连接时路径要和Broker配置的WebSocket路径一致EMQX默认是/mqttmosquitto默认通常是根路径/。如果本地用localhost访问WebGL页面通常没有跨域问题。但一旦部署到别的域名Broker必须配置跨域白名单或者用反向代理解决。EMQX侧可以通过设置listener.ws.external.allow_anonymous和跨域配置来放行这些在EMQX Dashboard里都能直接设置。5. 打包实测里的四个高频坑与排查链路5.1 启动闪屏和“团结闪屏”问题搜索关键词里“打包webgl就不会弹出团结闪屏unity”热度很高说明很多人被这个启动画面问题卡住过。Unity 6之后个人版已经允许直接关闭启动画面打开Project Settings - Player - Splash Image取消勾选Show Splash Screen即可。但如果你的项目是从Unity中国版的“团结引擎”流程迁移过来的WebGL模板里可能残留一段自定义的闪屏逻辑那段逻辑不是Splash Image设置能关掉的需要去模板的index.html和TemplateData目录下的js脚本里找相关代码手动移除。排查思路就是开浏览器DevTools看页面加载时那个闪屏DOM节点是哪个js创建的顺着文件引用的路径反查模板源码。5.2 The browser supports WebGL, but initialization failed这个报错在WebGL发布后很常见原因各异。最重要的一条排查链路是在浏览器地址栏输入chrome://gpu查看WebGL一行是Enabled还是Disabled。如果Disabled去浏览器设置里打开硬件加速然后重启浏览器。检查显卡驱动尤其是Windows本子和公司老电脑驱动太旧会让WebGL初始化失败。如果页面跑在虚拟机或远程桌面环境大概率没有可用的GPU Device这种情况只能在宿主机上测试。用第三方GPU检测工具来确认你的浏览器本身WebGL是否正常。排查这类问题最忌讳的是在Unity侧反复调代码改半天发现是浏览器环境问题。一旦看到这个报错第一反应应该是看浏览器环境而不是代码。5.3 WebGL内存不足与白屏内存问题是WebGL发布第二阶段最常见的坑。表象是页面运行一段时间后画面卡死浏览器标签页崩溃或者直接白屏。打开浏览器DevTools - Performance可以看到wasm内存区域异常增长。处理办法分两类一类是把Player Settings里WebGL Memory Size调大另一类是从资源层面减负比如压缩纹理、控制同时加载的AssetBundle数量、避免运行时频繁创建和销毁GameObject。内存设置只能给你更大的容器真正的根子在资源占用。5.4 帧率不稳定targetFrameRate在WebGL里的特殊性WebGL下Unity的渲染节奏受浏览器的requestAnimationFrame驱动单纯设置Application.targetFrameRate 60常常没效果。我实测下来需要同时把QualitySettings.vSyncCount设为0再配合设置targetFrameRate在多数主流浏览器上才能稳定到目标帧率。页面切到后台标签页时浏览器会自动降低JavaScript定时器频率甚至完全暂停渲染。如果项目里依赖Update里的计时逻辑后台时间长了容易累积误差恢复后可能出现逻辑跳变。我把计时代码从Update里的累积改成基于Time.realtimeSinceStartup的计算恢复后会平滑很多。5.5 调试方法论浏览器Console才是第一现场WebGL项目在Unity编辑器里跑得好好的到了浏览器出问题很多人还在Unity侧打Log这是最没效率的排查方式。正确姿势是直接在浏览器DevTools的Console里看Unity输出的Debug.Log配合Network面板观察WebSocket的帧流量Application面板看页面内存。jslib里的console.log也会输出到同一个Console排查桥接问题时我会在JS侧埋日志确认C#参数是否成功传到JS。6. 真正上线前连接安全与稳定性加固6.1 不要把Broker密码写在WebGL包里这是特别容易犯的错。WebGL包本质上是一堆静态文件部署到服务器上谁都能下载。你如果把Broker的正式用户名密码写死在代码里等于把门钥匙放在了门垫下面。浏览器端可以轻松打开JS文件搜索password字段甚至抓包直接看到MQTT的Connect报文里的密码。我建议的方案是前期接入一个轻量后端服务做令牌分发客户端先向后端拿一个短期有效的临时token再用这个token去连接MQTT Broker。Broker侧通过ACL配置让不同token只能订阅/发布对应权限的Topic。这样即使token被扒出来几分钟后自动失效最大程度降低风险。6.2 心跳、重连与页面切后台的协同MQTT.js的reconnectPeriod参数默认会自动重连但有个细节容易忽略页面从后台切回前台时如果心跳已经断过一轮重连成功后UISide要立刻更新连接状态。我在C#的OnMqttConnected事件里做统一处理恢复连接后重新请求一次最新的业务状态数据避免因为离线期间错过消息而出现UI不同步。KeepAlive设置不宜过短10秒左右在弱网环境容易误判。30秒是一个比较平衡的值Broker侧默认180秒无消息清理连接30秒足够在服务端清理之前发现问题。6.3 QoS和消息量的简单建议浏览器和MQTT.js跑QoS 2不是不行但链路更长握手次数更多在弱网环境下体验明显变差。实时大屏、设备状态同步这类场景QoS 1已经足够大部分业务甚至QoS 0也能接受。如果单条消息体量大比如超过几十KB建议在业务层做压缩或者拆成块消息传输WebGL的字符串转JSON再转C#字符串是有开销的别把大payload直接往里怼。最后的最后分享一个我个人的经验当你把Unity项目发布到WebGL并且要接MQTT时一定要先在浏览器技术栈下做通原理验证再回到Unity工程里整合。我一开始就是在Unity工程里反复试错绕了不少弯路。桥接方案本身并不复杂复杂的是对WebGL运行环境的认知转变。建议你跟着上面的代码跑通一个最小Demo再往自己的项目里迁移整体会顺畅很多。本文还有配套的精品资源点击获取
返回列表