简介:本资源为Unity3D开发的WebGL项目打包文件,面向游戏开发者、Unity初学者及WebGL技术研究者。包内包含核心代码、配置文件、Unity工程结构说明、相关插件和测试数据等,帮助用户快速搭建WebGL实验环境、理解Unity与Web平台交互机制、验证关键功能实现。资源共165个文件,以txt配置说明、Unity工程目录、Python辅助脚...
1. Unity 打包 WebGL 并使用 MQTT:一个看似简单却极易翻车的组合
你刚在 Unity 里调通了传感器数据模拟逻辑,信心满满点下「Build and Run」——目标平台选 WebGL,通信协议用 MQTT,毕竟浏览器里跑实时数据流是刚需。结果页面白屏、控制台报WebSocket connection to 'ws://...' failed、甚至打包后连index.html都打不开……这不是玄学,是 Unity + WebGL + MQTT 三者交界处堆满的隐性依赖坑:Unity 的 WebGL 构建链不自带 WebSocket 支持、MQTT.js 版本与 Unity 的 JS 插件桥接方式强耦合、CORS 和跨域策略在本地文件协议下直接失效。这套组合不是不能跑,而是必须按特定版本链路组装——Unity 2021.3 LTS 是目前最稳的基线,MQTT.js 4.3.x 是唯一能与 Unity 的Application.ExternalEval和SendMessage安全握手的版本,而所有 JS 插件必须绕过 Unity 的自动压缩机制手动注入。适合正在做工业数据看板、IoT 设备远程监控 Demo、或需要将 Unity 3D 场景嵌入现有 Web 管理后台的开发者。装错一个环节,就是整整两天查控制台日志、翻 GitHub issue、重装 Unity Editor 的血泪经验。
2. 准备环境:Unity 版本、MQTT 库与构建链的硬性对齐
Unity 对 WebGL 的支持不是“有就行”,而是版本间存在 ABI 兼容断层。2020.x 系列对WebGLTemplates的 JS 注入机制不完善;2022.x 起强制启用 IL2CPP 后,部分旧版 MQTT 插件的回调函数签名会崩溃;只有Unity 2021.3.34f1(LTS)在稳定性、JS 插件兼容性、以及对WebSocket原生 API 的封装完整性上达到平衡点。这不是推荐,是实测踩坑后的强制选择。
2.1 下载并验证 Unity 2021.3.34f1
前往 Unity 官方 Archive 页面(unity.com/releases/archive),搜索2021.3.34f1,下载对应操作系统安装包。注意:不要用 Hub 自动安装最新 LTS——Hub 默认拉取的是2021.3.35f1,该版本在 WebGL 构建时会静默丢弃Plugins/WebGL/libmqtt.js的导出符号,导致运行时报ReferenceError: mqtt is not defined。
安装完成后,在 Unity Editor 中打开项目,进入Edit → Preferences → External Tools,确认WebGL Templates路径指向Unity/Editor/Data/PlaybackEngines/WebGLSupport/BuildTools。这是后续手动注入 JS 的基础路径。
2.2 获取兼容的 MQTT.js 4.3.7(非 npm install)
MQTT.js 官方 npm 包(v4.3.7)本身是纯 JS,但 Unity WebGL 构建流程会对其做静态分析并剔除“未引用”的全局变量。直接npm install mqtt后拷贝node_modules/mqtt/dist/mqtt.min.js进项目,90% 概率失败。正确做法是使用已预处理的 UMD 构建版:
# 在空目录执行,不依赖 node_modules curl -o mqtt-umd.js https://unpkg.com/mqtt@4.3.7/dist/mqtt.umd.js提示:
mqtt.umd.js是唯一被 Unity WebGL 构建器识别为“可导出全局mqtt对象”的格式。.min.js和.esm.js均会触发符号剥离。该文件大小约 186 KB,无额外依赖,开箱即用。
2.3 创建最小化 WebGL 模板结构
Unity 默认 WebGL 模板(Default)不提供 JS 注入钩子。必须创建自定义模板:
# 在 Unity 项目根目录下执行 mkdir -p Assets/Plugins/WebGL mkdir -p Templates/MyWebGLTemplate cp -r "$UNITY_PATH/Editor/Data/PlaybackEngines/WebGLSupport/BuildTools/TemplateData" Templates/MyWebGLTemplate/ cp -r "$UNITY_PATH/Editor/Data/PlaybackEngines/WebGLSupport/BuildTools/Template" Templates/MyWebGLTemplate/编辑Templates/MyWebGLTemplate/TemplateData/index.html,在<body>末尾插入:
<!-- TemplateData/index.html --> <script type="text/javascript" src="libmqtt.js"></script> <script type="text/javascript" src="mqtt-bridge.js"></script>再创建Templates/MyWebGLTemplate/TemplateData/libmqtt.js(内容即上一步下载的mqtt-umd.js),以及空文件Templates/MyWebGLTemplate/TemplateData/mqtt-bridge.js(留待第 3 章填充)。此结构确保 JS 文件在 Unity 主脚本加载前就绪,且不被压缩混淆。
2.4 验证环境闭环:检查构建输出是否含预期文件
配置完模板后,在 Unity 中进入File → Build Settings,Platform 选WebGL,点击Switch Platform。然后点击Player Settings → Publishing Settings,将Template下拉框设为MyWebGLTemplate。最后点击Build,输出到Build/WebGL目录。
进入Build/WebGL/TemplateData,应看到:
libmqtt.js(186 KB,开头为(function (global, factory) { ...)mqtt-bridge.js(空文件,0 KB)index.html中包含你插入的两行<script>标签
若libmqtt.js缺失或大小异常(如 2 KB),说明模板路径未生效或文件未放对位置——Unity 构建时只会复制TemplateData下的文件,libmqtt.js必须在此目录内,而非Assets/Plugins/WebGL/。
3. 实现 Unity 与 MQTT 的双向通信桥接
Unity WebGL 不允许直接new WebSocket()或fetch(),所有网络操作必须通过 JS 插件桥接。核心是两层:JS 层维护 MQTT Client 实例并暴露函数,C# 层用Application.ExternalEval初始化、用SendMessage接收消息。关键在于避免闭包污染和回调丢失——这是 80% 白屏问题的根源。
3.1 编写 mqtt-bridge.js:暴露全局可控接口
编辑Templates/MyWebGLTemplate/TemplateData/mqtt-bridge.js,填入以下内容(逐行说明):
// mqtt-bridge.js let mqttClient = null; let messageCallback = null; // 1. 初始化 MQTT 客户端(由 C# 调用) window.initMqttClient = function(brokerUrl, clientId, options) { // options 是 JSON 字符串,需解析 const opts = JSON.parse(options || '{}'); // 强制指定协议为 ws/wss,禁用自动重连(Unity 无法监听 reconnect 事件) opts.protocol = brokerUrl.startsWith('wss') ? 'wss' : 'ws'; opts.reconnectPeriod = 0; // 关键:禁用自动重连,由 C# 控制 opts.clientId = clientId || 'unity-webgl-' + Date.now(); try { mqttClient = mqtt.connect(brokerUrl, opts); // 2. 绑定连接成功回调(通知 C#) mqttClient.on('connect', function() { if (typeof window.unityInstance !== 'undefined') { window.unityInstance.SendMessage('MQTTManager', 'OnConnected', ''); } }); // 3. 绑定消息接收回调(通知 C#) mqttClient.on('message', function(topic, payload) { // 将 ArrayBuffer 转为字符串(Unity 只能接收 string) const strPayload = new TextDecoder().decode(payload); if (messageCallback && typeof messageCallback === 'function') { messageCallback(topic, strPayload); } }); // 4. 绑定错误回调(通知 C#) mqttClient.on('error', function(err) { if (typeof window.unityInstance !== 'undefined') { window.unityInstance.SendMessage('MQTTManager', 'OnError', err.toString()); } }); } catch (e) { console.error('MQTT init failed:', e); if (typeof window.unityInstance !== 'undefined') { window.unityInstance.SendMessage('MQTTManager', 'OnError', e.toString()); } } }; // 5. 订阅主题(由 C# 调用) window.mqttSubscribe = function(topic, qos) { if (mqttClient && mqttClient.connected) { mqttClient.subscribe(topic, { qos: qos || 1 }); } }; // 6. 发布消息(由 C# 调用) window.mqttPublish = function(topic, payload, qos, retain) { if (mqttClient && mqttClient.connected) { mqttClient.publish(topic, payload, { qos: qos || 1, retain: retain || false }); } }; // 7. 设置消息接收回调(由 C# 注册) window.setMqttMessageCallback = function(callbackName) { messageCallback = function(topic, payload) { if (typeof window.unityInstance !== 'undefined') { // 拼接 topic 和 payload,用 \0 分隔(避免 JSON 解析失败) const msg = topic + '\0' + payload; window.unityInstance.SendMessage('MQTTManager', callbackName, msg); } }; };逻辑说明:
initMqttClient接收 JSON 字符串参数,避免 JS 插件传参时的类型丢失;setMqttMessageCallback是关键设计:它让 C# 动态注册一个回调函数名(如"OnMessageReceived"),JS 层收到 MQTT 消息后,用SendMessage触发该函数,并将topic\0payload作为单个字符串参数传递——这样 C# 无需解析 JSON,直接Split('\0')即可分离;- 所有
SendMessage的第一个参数'MQTTManager'是 C# 脚本挂载的 GameObject 名,必须严格一致;reconnectPeriod: 0是硬性要求:Unity WebGL 无法捕获reconnect事件,自动重连会导致连接状态与 C# 认知不一致,引发publish失败却无报错。
3.2 编写 C# MQTTManager.cs:统一管理 JS 交互
在Assets/Scripts/下创建MQTTManager.cs:
using UnityEngine; using System.Runtime.InteropServices; public class MQTTManager : MonoBehaviour { // 1. 用于存储 JS 回调函数名 private string onMessageCallback = "OnMessageReceived"; // 2. 初始化 MQTT(在 Start 中调用) public void Initialize(string brokerUrl, string clientId = "", string optionsJson = "") { // 确保只在 WebGL 平台执行 #if UNITY_WEBGL && !UNITY_EDITOR string jsOptions = string.IsNullOrEmpty(optionsJson) ? "{}" : optionsJson.Replace("'", "\\'"); string jsCode = $"initMqttClient('{brokerUrl}', '{clientId}', '{jsOptions}');"; Application.ExternalEval(jsCode); Debug.Log($"MQTT initialized: {brokerUrl}"); #endif } // 3. 订阅主题 public void Subscribe(string topic, byte qos = 1) { #if UNITY_WEBGL && !UNITY_EDITOR Application.ExternalEval($"mqttSubscribe('{topic}', {qos});"); #endif } // 4. 发布消息 public void Publish(string topic, string payload, byte qos = 1, bool retain = false) { #if UNITY_WEBGL && !UNITY_EDITOR // 转义单引号,避免 JS 语法错误 string safePayload = payload.Replace("'", "\\'"); Application.ExternalEval($"mqttPublish('{topic}', '{safePayload}', {qos}, {retain.ToString().ToLower()});"); #endif } // 5. 注册消息接收回调(必须在 Initialize 后调用) public void SetMessageCallback(string callbackName) { onMessageCallback = callbackName; #if UNITY_WEBGL && !UNITY_EDITOR Application.ExternalEval($"setMqttMessageCallback('{callbackName}');"); #endif } // 6. JS 调用:连接成功 public void OnConnected() { Debug.Log("MQTT connected successfully."); } // 7. JS 调用:发生错误 public void OnError(string errorMsg) { Debug.LogError($"MQTT error: {errorMsg}"); } // 8. JS 调用:收到消息(由 setMqttMessageCallback 触发) public void OnMessageReceived(string topicAndPayload) { string[] parts = topicAndPayload.Split('\0'); if (parts.Length >= 2) { string topic = parts[0]; string payload = parts[1]; Debug.Log($"MQTT received: {topic} -> {payload}"); // 在此处处理业务逻辑,如更新 UI、触发事件等 } } }参数说明:
Initialize的optionsJson示例:{"clean": true, "keepalive": 60},必须是合法 JSON 字符串;Publish中safePayload的单引号转义是必须的,否则payload = "user's data"会导致 JS 语法错误;OnMessageReceived的\0分隔方案规避了 Unity 对 JSON 字符串中双引号的转义问题,比传 JSON 数组更鲁棒。
3.3 在场景中挂载并配置 MQTTManager
- 创建空 GameObject,命名为
MQTTManager; - 将
MQTTManager.cs拖拽到该 GameObject 上; - 在
Inspector中,确保MQTTManager脚本的onMessageCallback字段值为OnMessageReceived(与代码中默认值一致); - 添加
Start()方法调用初始化(示例):
void Start() { // 初始化 MQTT(测试用公共 Broker) Initialize("ws://test.mosquitto.org:8080", "unity-client-001"); // 设置消息回调(必须在 Initialize 之后) SetMessageCallback("OnMessageReceived"); // 订阅测试主题 Subscribe("unity/test", 1); // 5 秒后发布一条测试消息 Invoke("PublishTestMessage", 5f); } void PublishTestMessage() { Publish("unity/test", "Hello from Unity WebGL!", 1, false); }注意:
test.mosquitto.org是公开测试 Broker,仅用于验证。生产环境必须使用自有 Broker 并配置 TLS(wss://)。
4. 常见问题排查:那些让你怀疑人生的白屏与静默失败
这套方案看似步骤清晰,但实际部署时 70% 的失败都集中在几个经典陷阱。以下是我在某高校物联网课程项目、某工业设备可视化 Demo 中反复验证过的 5 条血泪经验,每条都按「现象 → 原因 → 解决」结构整理,拒绝模糊描述。
4.1 现象:浏览器控制台报Uncaught ReferenceError: mqtt is not defined,且libmqtt.js已确认存在于TemplateData目录
原因:Unity 构建时对TemplateData下的 JS 文件做了内容哈希校验,若libmqtt.js文件权限为只读(常见于 macOS/Linux 从 zip 解压后),Unity 会跳过复制,导致构建输出中该文件为空或缺失。
解决:在终端中执行chmod 644 Templates/MyWebGLTemplate/TemplateData/libmqtt.js,然后彻底删除Build/WebGL目录,重新 Build。
4.2 现象:index.html能打开,但控制台报WebSocket connection to 'ws://...' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED,且 Broker 地址确认无误
原因:本地直接双击index.html打开,浏览器使用file://协议,而 WebSocket 不允许在file://下连接ws://(安全限制)。
解决:必须通过 HTTP 服务访问。最简方式:在Build/WebGL目录下执行python3 -m http.server 8000(Python 3),然后浏览器访问http://localhost:8000。切勿双击打开 HTML。
4.3 现象:连接成功日志出现,但OnMessageReceived完全不触发,Subscribe后无任何消息到达
原因:MQTT Broker 启用了 ACL(访问控制列表),而测试客户端 ID(如unity-client-001)未被授权订阅unity/test主题。
解决:换用无 ACL 的测试 Broker(如test.mosquitto.org),或登录 Broker 管理后台,为该 client ID 添加unity/#的订阅权限。验证方法:用mosquitto_sub -h test.mosquitto.org -t "unity/test"在命令行确认消息可达。
4.4 现象:Publish后 Broker 收到消息,但OnMessageReceived中payload为空字符串或乱码
原因:MQTT.js 4.3.7 默认将 payload 作为ArrayBuffer传递,而new TextDecoder().decode(payload)在某些浏览器(如 Safari 15)中对空 ArrayBuffer 抛异常,导致回调中断。
解决:修改mqtt-bridge.js中message事件处理块,增加 ArrayBuffer 判空:
mqttClient.on('message', function(topic, payload) { let strPayload = ''; if (payload instanceof ArrayBuffer) { try { strPayload = new TextDecoder().decode(payload); } catch (e) { strPayload = '[Binary payload, not decoded]'; } } else if (typeof payload === 'string') { strPayload = payload; } if (messageCallback && typeof messageCallback === 'function') { messageCallback(topic, strPayload); } });4.5 现象:Unity Editor 中运行正常(通过模拟 MQTT),但 WebGL 构建后OnConnected不触发,且无任何 JS 错误
原因:MQTTManagerGameObject 未设置为DontDestroyOnLoad,导致 WebGL 加载场景时该对象被销毁,SendMessage目标不存在。
解决:在MQTTManager.cs的Awake()中添加:
void Awake() { DontDestroyOnLoad(gameObject); }并确保该脚本挂载在场景启动时即存在的 GameObject 上(如GameManager),而非动态生成的对象。
5. 验证与调试:用三步法确认你的 MQTT 通道真正跑通
装完不验证,等于没装。我见过太多人卡在“以为跑通了”这一步——控制台有connected日志,就以为万事大吉,结果上线后数据根本收不到。真正的验证必须覆盖连接、收、发三个原子动作,且每步都要有可观察、可复位的证据。下面是我每次交付前必走的三步法,耗时不超过 3 分钟。
5.1 第一步:用浏览器开发者工具直连 Broker,绕过 Unity 做基线验证
打开Build/WebGL/index.html后,按F12打开 DevTools,切换到Console标签页。粘贴以下代码并回车:
// 手动创建 MQTT 连接,验证 Broker 可达性 const client = mqtt.connect('ws://test.mosquitto.org:8080', { clientId: 'debug-' + Date.now(), protocol: 'ws', reconnectPeriod: 0 }); client.on('connect', () => { console.log('✅ Direct WS connect OK'); client.subscribe('unity/debug', { qos: 1 }, (err) => { if (!err) console.log('✅ Subscribed to unity/debug'); }); }); client.on('message', (topic, payload) => { console.log(`📩 Received on ${topic}:`, new TextDecoder().decode(payload)); }); // 发送一条测试消息 setTimeout(() => { client.publish('unity/debug', 'Hello from browser console!', { qos: 1 }); }, 2000);✅ 预期结果:
- 2 秒后控制台输出
✅ Direct WS connect OK和✅ Subscribed to unity/debug;- 再 2 秒后输出
📩 Received on unity/debug: Hello from browser console!。
若此步失败,说明 Broker、网络或 WebSocket 协议本身有问题,不必继续往下走——Unity 层面的任何调试都是徒劳。
5.2 第二步:在 Unity 中注入实时日志探针,定位 JS-C# 通信断点
修改MQTTManager.cs,在OnConnected、OnError、OnMessageReceived三个方法开头添加一行:
Debug.Log($"[JS→C#] {System.Reflection.MethodBase.GetCurrentMethod().Name} called with: {System.Text.Json.JsonSerializer.Serialize(new { topicAndPayload })}");然后重新 Build。打开http://localhost:8000,在 DevTools 的Console中输入:
// 检查 Unity 实例是否就绪 console.log('Unity instance:', window.unityInstance); // 手动触发一次连接成功回调 if (window.unityInstance) { window.unityInstance.SendMessage('MQTTManager', 'OnConnected', ''); }✅ 预期结果:
- 控制台同时出现
[JS→C#] OnConnected called with: {...}(来自 JS 调用)和MQTT connected successfully.(来自 C# 原始日志);- 若只看到前者,说明
SendMessage成功但 C# 方法未执行(检查脚本是否挂载、GameObject 是否激活);- 若只看到后者,说明 C# 方法被调用但 JS 未触发(检查
initMqttClient是否执行、mqttClient.on('connect')是否绑定)。
5.3 第三步:构造端到端闭环测试,用真实消息流验证全链路
这是最终审判。准备一个外部 MQTT 客户端(如mosquitto_pub或 MQTTX 工具),向 Unity 订阅的主题发消息,并观察 Unity 是否响应:
- Unity 启动后,确保已执行
Subscribe("unity/test", 1); - 在终端执行:
mosquitto_pub -h test.mosquitto.org -t "unity/test" -m "Live data: {\"temp\":25.3,\"humid\":62}" -q 1 - 观察 Unity 控制台(DevTools Console)是否输出:
MQTT received: unity/test -> Live data: {"temp":25.3,"humid":62}
✅ 关键指标:
- 消息延迟 ≤ 500ms(公网测试 Broker 下属正常);
- 连续发送 10 条消息,Unity 全部接收且顺序一致;
- 修改
Publish方法,向外部主题发消息,用mosquitto_sub确认对方收到。
若此步通过,恭喜——你的 Unity WebGL + MQTT 通道已具备生产级可用性。从那以后我每次新配环境,都强制走一遍这三步:先用浏览器直连验 Broker,再用 JS 注入验桥接,最后用真实消息流验闭环。少走一步,上线后就要多花十倍时间在客户现场抓包。希望帮到你。
本文还有配套的精品资源,点击获取