1. 项目概述:为什么Unity WebGL交互是个“技术深水区”?
如果你已经成功把Unity项目打包成了WebGL,并在浏览器里看到了自己的作品,恭喜你,你已经跨过了第一道门槛。但很快,你就会遇到下一个更棘手的问题:这个运行在浏览器里的“黑盒子”,怎么跟外面的网页世界“说上话”?比如,你想让网页上的一个按钮控制游戏里角色的跳跃,或者想把游戏里的得分实时显示在网页的某个角落。这,就是Unity WebGL与网页深度交互要解决的核心问题。
很多教程会告诉你用Application.ExternalCall或者SendMessage,但当你真正上手,会发现坑远比想象的多:为什么我的JS函数调不到?为什么传字符串乱码了?为什么在编辑器里好好的,一打包就失效?这背后,是Unity WebGL特殊的运行机制在作祟。它并非一个原生的网页应用,而是通过Emscripten编译成WebAssembly和JavaScript,运行在一个高度封装的“模拟环境”中。与网页的交互,本质上是跨越这个模拟环境与浏览器主环境两个“世界”的通信。
这篇文章,我将抛开那些浅尝辄止的基础教程,带你深入C#与JavaScript互调的实战腹地。我会从原理讲起,拆解.jslib插件的运作机制,手把手教你处理各种数据类型的传递(尤其是让人头疼的字符串和数组),并分享如何从网页JS反向调用Unity中的方法。更重要的是,我会把我在多个商业项目中踩过的坑、总结的最佳实践和调试技巧毫无保留地分享给你。无论你是想实现复杂的网页UI与3D场景联动,还是构建基于浏览器的数据可视化应用,这些实战经验都能让你少走弯路。
2. 交互原理深度拆解:跨越“两个世界”的桥梁
要玩转交互,首先得明白Unity WebGL在浏览器里是怎么“活”着的。你不能把它简单理解为一个<canvas>标签,它更像是一个拥有自己独立内存和运行时的“虚拟机”。
2.1 Unity WebGL的运行时架构
当你打包WebGL时,Unity会调用Emscripten编译器,将你的C#代码(经由IL2CPP转换的C++代码)编译成WebAssembly模块。这个模块连同必要的JavaScript“胶水”代码一起,被加载到一个<canvas>元素中。关键点来了:这个WebAssembly模块运行在一个独立的、线性的内存空间(即Emscripten的HEAP)中,它与浏览器主线程的JavaScript环境是隔离的。
两个核心对象你需要牢记:
Module对象:这是Emscripten生成的JavaScript运行时环境的总入口。Unity的WebGL构建会创建一个全局的Module对象(或者通过UnityLoader实例化的gameInstance.Module),里面包含了内存堆(HEAP)、函数表以及各种运行时方法。SendMessage函数:这是Unity暴露给外部JavaScript环境的一个关键函数,用于从网页向Unity内部发送消息。
交互的本质,就是在Module所管理的WebAssembly内存,与浏览器的DOM/JavaScript环境之间,安全、正确地交换数据。
2.2 两种互调路径的对比与选型
Unity官方手册和历史版本中提到了多种方式,但并非所有都适用于现代项目。
路径一:C#调用JavaScript(推荐方式:.jslib插件)这是最稳健、性能最好的方式。你创建一个后缀为.jslib的JavaScript文件,放在项目的Assets/Plugins目录下。这个文件中的函数会被Emscripten直接编译链接到Module中,C#通过[DllImport(“__Internal”)]来调用它们。这种方式函数调用开销小,类型映射相对清晰。
- 为什么推荐?直接集成到构建中,无需担心异步加载问题,执行效率高。
- 底层原理:
.jslib中的函数被编译后,其指针被注册到Module中。C#调用时,实际上是WebAssembly模块在调用同一模块内的JavaScript“胶水”函数。
路径二:JavaScript调用C#(唯一方式:SendMessage或GameInstance)从网页的JavaScript环境主动调用Unity中的方法,必须通过Unity提供的消息传递机制。
- 传统全局方式:
SendMessage(“GameObjectName”, “MethodName”, argument)。这种方式简单,但依赖于全局函数,在多实例场景下容易冲突。 - 现代实例方式:当你使用
UnityLoader.instantiate(或新的createUnityInstance)异步加载游戏后,会得到一个游戏实例对象(如gameInstance)。你应该使用gameInstance.SendMessage()来调用,这是隔离的、安全的。 - 重要限制:
SendMessage只能向挂载在GameObject上的公共方法传递消息,且参数只能是string、int、float等基本类型,或一个可转换为字符串的单一参数。它无法直接获取返回值。
路径三:C#调用JavaScript(已过时方式:Application.ExternalCall/Eval)在Unity 5.6之前,这是主要方式。它通过eval方式执行全局JS代码。强烈不推荐在新项目中使用,因为它要求你的JS代码必须在全局作用域,极易造成命名污染和冲突,且在多实例环境下完全不可用。Unity官方也仅出于兼容性保留,未来可能移除。
我的经验之谈:对于任何新的WebGL项目,我的建议是,C#调JS一律用
.jslib,JS调C#一律用gameInstance.SendMessage。这个组合能覆盖99%的需求,并且是最面向未来的做法。
3. 核心实战:C#调用JavaScript的.jslib详解
让我们从最常用的场景开始:在C#脚本中触发一个网页端的操作,比如显示浏览器弹窗、操作DOM元素、调用第三方JS库。
3.1 创建与配置.jslib插件
首先,在你的Unity项目Assets文件夹下,创建一个Plugins文件夹(如果没有的话)。然后在该文件夹内,新建一个文本文件,将其后缀名改为.jslib,例如WebGLCommunication.jslib。
用代码编辑器打开这个.jslib文件,它的基本结构如下:
mergeInto(LibraryManager.library, { // 你的函数定义在这里 MyFunction: function () { // JavaScript 代码 console.log("Hello from .jslib!"); }, });mergeInto是Emscripten的特定语法,作用是将你定义的对象合并到最终的LibraryManager.library中,这样编译后你的函数才能被C#找到。
3.2 处理不同数据类型的参数与返回值
这是最容易出错的地方。简单类型(int,float,double)可以直接传递,复杂类型则需要手动处理内存。
1. 数字与布尔值:最简单
// .jslib 文件 mergeInto(LibraryManager.library, { AddTwoNumbers: function (a, b) { return a + b; // 直接返回数字 }, ToggleVisibility: function (isVisible) { // isVisible 是 0 或 1 (C#的bool会被转为整数) var element = document.getElementById(“myUI”); element.style.display = isVisible ? “block” : “none”; } });// C# 脚本 using System.Runtime.InteropServices; public class CallJS : MonoBehaviour { [DllImport(“__Internal”)] private static extern int AddTwoNumbers(int a, int b); [DllImport(“__Internal”)] private static extern void ToggleVisibility(bool isVisible); void Start() { int result = AddTwoNumbers(5, 3); // result = 8 ToggleVisibility(true); } }2. 字符串:需要编码/解码字符串在C#(WebAssembly内存)中是UTF-16或UTF-8编码的字节序列,需要转换。Emscripten提供了辅助函数。
- C#传字符串给JS:使用
Pointer_stringify(ptr)。ptr是一个指向字符串内存地址的指针(在JS中表现为一个数字)。
// .jslib mergeInto(LibraryManager.library, { ShowAlert: function (messagePtr) { var message = Pointer_stringify(messagePtr); // 关键转换 alert(message); }, });// C# [DllImport(“__Internal”)] private static extern void ShowAlert(string message); // Unity会自动处理字符串到指针的转换 void Start() { ShowAlert(“Hello from Unity!”); }- JS返回字符串给C#:这是高级操作,需要手动分配和释放内存。你必须使用Emscripten提供的
_malloc和_free,以及stringToUTF8等函数。
// .jslib mergeInto(LibraryManager.library, { GetUserName: function () { var userName = “Player123”; // 假设从某处获取 var lengthBytes = lengthBytesUTF8(userName) + 1; // 计算UTF8字节长度,+1是为了C字符串的结束符‘\0‘ var buffer = _malloc(lengthBytes); // 在Emscripten堆上分配内存 stringToUTF8(userName, buffer, lengthBytes); // 将字符串写入分配的内存 return buffer; // 返回内存地址(指针) }, });// C# [DllImport(“__Internal”)] private static extern IntPtr GetUserName(); // 返回指针 // 另外,通常还需要声明对应的释放函数(如果JS端分配了内存) [DllImport(“__Internal”)] private static extern void _free(IntPtr ptr); void Start() { IntPtr ptr = GetUserName(); string userName = Marshal.PtrToStringUTF8(ptr); // 将指针转换为C#字符串 _free(ptr); // 非常重要!释放JS端分配的内存,防止泄漏 Debug.Log(“User: “ + userName); }踩坑警告:从JS返回字符串时,如果忘记在C#端调用
_free释放内存,会造成内存泄漏。更安全的做法是,在.jslib中避免返回需要C#释放的字符串,改为通过回调函数或SendMessage将字符串“推送”回C#。
3. 数组:通过指针和长度传递传递数组需要传递数组在HEAP中的起始地址(指针)和数组长度。
// .jslib - 处理浮点数数组 mergeInto(LibraryManager.library, { ProcessFloatArray: function (arrayPtr, length) { // HEAPF32 是Float32数组的视图,arrayPtr >> 2 是因为HEAPF32是32位视图,地址需要除以4(右移2位) for (var i = 0; i < length; i++) { var value = HEAPF32[(arrayPtr >> 2) + i]; console.log(“Element “ + i + “: “ + value); // 可以修改值,它会直接写回Unity的内存 // HEAPF32[(arrayPtr >> 2) + i] = value * 2; } } });// C# [DllImport(“__Internal”)] private static extern void ProcessFloatArray(float[] array, int length); void Start() { float[] myData = new float[] { 1.1f, 2.2f, 3.3f }; ProcessFloatArray(myData, myData.Length); // 如果JS修改了HEAP,myData中的值也会改变(因为传递的是指针) }对于其他类型数组(如int[],byte[]),需要使用对应的HEAP视图:HEAP8(Int8),HEAPU8(UInt8),HEAP16,HEAP32等。
3.3 调用浏览器API与第三方库
.jslib的强大之处在于你可以直接在里面写任何浏览器端JavaScript。
// .jslib - 操作DOM、调用Web API mergeInto(LibraryManager.library, { UpdateHTMLText: function (elementIdPtr, textPtr) { var elementId = Pointer_stringify(elementIdPtr); var text = Pointer_stringify(textPtr); document.getElementById(elementId).innerText = text; }, GetBrowserLanguage: function () { var lang = navigator.language || navigator.userLanguage; var bufferSize = lengthBytesUTF8(lang) + 1; var buffer = _malloc(bufferSize); stringToUTF8(lang, buffer, bufferSize); return buffer; }, // 假设引入了Three.js库 CreateThreeJSCube: function () { if (typeof THREE !== ‘undefined’) { var scene = new THREE.Scene(); var camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000); // ... 更多Three.js代码 } else { console.error(“THREE is not defined. Make sure Three.js is included in the page.”); } } });4. 逆向控制:从网页JavaScript调用Unity
现在,我们解决另一个方向的问题:如何让网页上的按钮、滑块或者任何JavaScript事件,去驱动Unity游戏内的逻辑。
4.1 游戏实例的获取与消息发送
首先,你必须确保Unity WebGL内容已经正确实例化。现代推荐的方式是使用createUnityInstance(2020.3+)或UnityLoader.instantiate。
HTML/JavaScript端:
<button onclick=“onUnityButtonClick()”>让Unity中的角色跳跃</button> <canvas id=“unity-canvas”></canvas> <script> var unityInstance; // 使用 createUnityInstance (推荐) createUnityInstance(document.querySelector(“#unity-canvas”), { dataUrl: “Build/YourWebGL.data”, frameworkUrl: “Build/YourWebGL.framework.js”, codeUrl: “Build/YourWebGL.wasm”, }).then(function (instance) { unityInstance = instance; // 保存实例引用 console.log(“Unity loaded!”); }).catch(function (message) { alert(“Failed to load Unity: “ + message); }); function onUnityButtonClick() { if (unityInstance) { // 方式1:向特定GameObject发送消息 unityInstance.SendMessage(“Player”, “Jump”); // 方式2:发送带字符串参数的消息 unityInstance.SendMessage(“GameManager”, “AddScore”, “100”); // 方式3:发送数字参数(实际上也会被转为字符串传递) unityInstance.SendMessage(“VolumeController”, “SetVolume”, 0.5); } else { console.warn(“Unity instance not ready yet.”); } } </script>Unity C#端:你需要确保场景中存在名为Player、GameManager、VolumeController的GameObject,并且它们上面挂载的脚本有对应的公共方法。
// 挂载在名为“Player”的GameObject上 public class PlayerController : MonoBehaviour { // 方法名必须与SendMessage的第二个参数完全匹配 public void Jump() { // 实现跳跃逻辑 GetComponent<Rigidbody>().AddForce(Vector3.up * 5, ForceMode.Impulse); } } // 挂载在名为“GameManager”的GameObject上 public class GameManager : MonoBehaviour { // 方法可以接收一个字符串参数 public void AddScore(string scoreStr) { if (int.TryParse(scoreStr, out int score)) { totalScore += score; UpdateScoreUI(); } } } // 挂载在名为“VolumeController”的GameObject上 public class VolumeController : MonoBehaviour { // 方法接收一个字符串参数,但需要自己解析为float public void SetVolume(string volumeStr) { if (float.TryParse(volumeStr, out float volume)) { AudioListener.volume = Mathf.Clamp01(volume); } } }4.2 超越SendMessage:使用事件监听与自定义协议
SendMessage虽然简单,但有局限性:只能调用公共无返回值方法,参数处理笨拙。对于复杂交互,我们可以建立更优雅的通信层。
思路:在Unity中创建一个“消息桥接器”(Message Bridge)GameObject。所有网页端的调用都指向这个桥接器,再由它分发到游戏内的各个系统。
步骤1:在Unity中创建C#桥接器
// MessageBridge.cs using UnityEngine; using System; public class MessageBridge : MonoBehaviour { // 单例模式方便访问 public static MessageBridge Instance { get; private set; } void Awake() { if (Instance == null) { Instance = this; DontDestroyOnLoad(gameObject); } else { Destroy(gameObject); } } // 供JS调用的通用方法 public void OnWebEvent(string eventDataJson) { // 解析JSON,包含事件类型和参数 // 例如:{"eventType":"PLAY_SOUND", "params":{"clipName":"jump", "volume":0.8}} WebEventData data = JsonUtility.FromJson<WebEventData>(eventDataJson); EventDispatcher.Instance.Dispatch(data.eventType, data.@params); } // 一个更具体的例子:更新玩家数据 public void UpdatePlayerData(string dataJson) { PlayerData data = JsonUtility.FromJson<PlayerData>(dataJson); FindObjectOfType<PlayerInventory>().LoadData(data); } // 从C#向JS发送消息的封装方法 public void SendToJS(string functionName, string data) { #if UNITY_WEBGL && !UNITY_EDITOR // 这里调用我们之前定义的.jslib函数 JSLibHelper.CallJSFunction(functionName, data); #endif } } // 对应的数据类 [System.Serializable] public class WebEventData { public string eventType; public string @params; // 可以是一个嵌套的JSON字符串 }步骤2:在.jslib中创建对应的接收函数
// 在 .jslib 中 mergeInto(LibraryManager.library, { // 供C#调用的函数,用于触发网页端的全局事件 TriggerWebEvent: function (eventNamePtr, eventDataPtr) { var eventName = Pointer_stringify(eventNamePtr); var eventData = Pointer_stringify(eventDataPtr); // 触发一个自定义的浏览器事件,让网页其他部分监听 var event = new CustomEvent(‘unityEvent’, { detail: { name: eventName, data: JSON.parse(eventData) } }); window.dispatchEvent(event); }, });步骤3:网页端使用事件监听
// 网页JS // 监听Unity发出的事件 window.addEventListener(‘unityEvent’, function (e) { console.log(‘Unity Event:‘, e.detail.name, e.detail.data); // 根据事件名更新UI等 if (e.detail.name === ‘SCORE_UPDATE’) { document.getElementById(‘scoreDisplay’).textContent = e.detail.data.score; } }); // 向Unity发送复杂指令 function sendComplexCommandToUnity() { var command = { eventType: ‘EQUIP_ITEM’, params: JSON.stringify({ itemId: ‘sword_01’, slot: ‘mainHand’ }) }; if (unityInstance) { // 还是通过SendMessage,但传递的是结构化的JSON字符串 unityInstance.SendMessage(‘MessageBridge’, ‘OnWebEvent’, JSON.stringify(command)); } }这种方式将松耦合发挥到极致,Unity和网页只需约定好事件类型和数据格式(JSON),内部的实现可以独立变化。
5. 高级应用场景与性能优化
掌握了基础互调,我们可以探索一些更高级的应用场景,并关注性能问题。
5.1 场景一:网页UI与Unity 3D界面同步
这是一个常见需求:用精美的HTML/CSS/JS制作游戏UI(如开始菜单、设置面板),然后控制Unity内的3D场景。
- 实现:网页UI按钮触发
unityInstance.SendMessage,调用Unity中控制场景加载、角色生成、动画播放等方法。反之,Unity内状态变化(如血量减少)通过.jslib函数TriggerWebEvent,触发网页端更新血条DOM元素的样式。 - 优化:避免每帧都从Unity向网页发送数据(如角色坐标)。改为在发生显著变化时(如血量变化超过5%)或由网页UI主动轮询(通过
setInterval调用Unity的一个“获取状态”函数)来更新。
5.2 场景二:与后端API通信
游戏数据需要保存到服务器,或从服务器拉取配置。
- 安全原则:永远不要在
.jslib或网页JS中硬编码敏感信息(如数据库密码、私钥)。所有与后端服务器的通信,应尽量由网页端的JavaScript发起(使用fetch或axios),然后将结果通过SendMessage传递给Unity。因为网页端的代码是公开的,而Unity的WebAssembly代码相对更难被直接破解。 - 流程:
- Unity C#调用
.jslib中的FetchFromServer(url, requestData)。 .jslib中的函数使用浏览器fetchAPI向服务器发送请求。- 收到响应后,在
.jslib的回调函数中,通过unityInstance.SendMessage将结果数据传回Unity的某个接收方法。
- Unity C#调用
5.3 场景三:处理二进制数据(如图片、音频)
传递大的二进制数据(如从Unity下载纹理,或在网页端生成音频流给Unity播放)是性能瓶颈。
- 使用
HEAPU8视图:你可以将字节数组从C#传到JS,JS可以将其转换为Blob或ArrayBuffer,用于创建图片URL或音频上下文。
// .jslib - 接收字节数组并创建图片 mergeInto(LibraryManager.library, { CreateImageFromBytes: function (byteArrayPtr, length, imgElementIdPtr) { var imgElementId = Pointer_stringify(imgElementIdPtr); var byteArray = new Uint8Array(HEAPU8.buffer, byteArrayPtr, length); var blob = new Blob([byteArray], {type: ‘image/png’}); var url = URL.createObjectURL(blob); document.getElementById(imgElementId).src = url; // 注意:之后需要 revokeObjectURL 来释放内存 } });- 使用
UnityWebRequest:对于从网络加载资源到Unity,更推荐在C#端直接使用UnityWebRequest,因为它能更好地管理WebGL环境下的内存和线程。与网页的交互应专注于控制和元数据,而非大数据传输。
5.4 性能与内存管理要点
- 减少跨边界调用:C#与JS的互调是有开销的,尤其是在WebAssembly和JavaScript之间切换上下文。避免在
Update()循环中每帧都进行互调。 - 批处理消息:如果需要传递多个小数据,考虑将它们组合成一个JSON对象一次性传递,而不是多次调用
SendMessage。 - 及时释放内存:如前所述,从JS返回字符串时,如果使用了
_malloc,C#端必须用_free释放。对于.jslib中创建的DOM元素或对象引用,也要注意在适当时候置空,防止内存泄漏。 - 错误处理:在
.jslib函数中加入try-catch,并将错误信息通过SendMessage传回Unity的Debug系统。在网页JS调用Unity前,检查unityInstance是否存在。
6. 调试技巧与常见问题实录
即使理解了原理,实战中依然会遇到各种光怪陆离的问题。下面是我总结的“避坑指南”。
6.1 调试工具与方法
- 浏览器开发者工具:这是你最好的朋友。在Sources标签中,你可以找到你编写的
.jslib文件(通常被包含在一个大的.js文件中,可以搜索你的函数名)。在这里打断点,查看HEAP内存。 - Console日志:在
.jslib中大量使用console.log()、console.warn()、console.error()。在C#端使用Debug.Log。两者结合,可以清晰看到交互流程。 - Unity WebGL开发控制台:在浏览器中右键点击Unity Canvas,选择“开发者控制台”或类似选项(不同Unity版本可能不同),这里可以看到Unity内部的日志输出,对于调试
SendMessage的接收方非常有用。
6.2 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| C#调用JS函数,报错“DllNotFoundException: __Internal” | 1..jslib文件未放在Assets/Plugins下。2. 函数名在C#声明和 .jslib中不匹配(大小写敏感)。3. 在Unity编辑器中直接运行(非WebGL平台)。 | 1. 检查文件路径和名称。 2. 仔细核对函数名和签名。 3. 使用 #if UNITY_WEBGL && !UNITY_EDITOR预编译指令包裹[DllImport]和调用代码,在编辑器模式下用空实现或模拟逻辑。 |
JS调用SendMessage,Unity端没反应 | 1.SendMessage的目标GameObject名称错误或不存在于当前场景。2. 目标脚本未挂载到该GameObject上,或脚本被禁用。 3. 方法不是 public的。4. unityInstance未正确获取(游戏未加载完成)。 | 1. 确认GameObject名字,注意空格和大小写。 2. 检查脚本组件。 3. 确保方法是 public void MethodName([string parameter])。4. 确保在 createUnityInstance的then回调成功后再调用。 |
| 传递的字符串在JS端显示乱码 | 没有使用Pointer_stringify()进行转换。 | 在.jslib中,所有从C#传来的string参数都需要先用var str = Pointer_stringify(ptr);转换。 |
| 从JS返回字符串到C#,程序崩溃或得到空值 | 1. 内存未正确分配(lengthBytesUTF8计算错误,未+1)。2. 使用了错误的 stringToUTF8参数。3. C#端没有正确使用 Marshal.PtrToStringUTF8,或忘记调用_free。 | 1. 确保lengthBytes = lengthBytesUTF8(str) + 1;。2. 核对 stringToUTF8的参数顺序:(source, destination, maxBytes)。3. 使用 IntPtr接收,转换后立即_free。考虑改用回调方式避免内存管理。 |
| 在编辑器里正常,打包后交互失效 | 编辑器环境下,[DllImport(“__Internal”)]可能无法正确链接。 | 务必使用预编译指令隔离平台相关代码。这是最重要的实践之一。 |
| 多实例嵌入时,交互混乱 | 使用了全局的SendMessage函数,或者多个实例共用了同一个全局JS变量。 | 始终使用实例化的unityInstance.SendMessage()。在.jslib中,避免使用全局变量,或者通过Module对象来区分不同实例。 |
| 性能很差,页面卡顿 | 在Update()中频繁进行互调,或传递了非常大的数据(如图像原始数据)。 | 优化调用频率,使用节流(throttle)或防抖(debounce)。对于大数据,考虑分帧传输或使用共享内存(如SharedArrayBuffer,但需要注意浏览器安全策略)。 |
6.3 一个完整的、带错误处理的实战代码片段
最后,分享一个我项目中常用的、包含了错误处理和平台判断的桥接类片段,它把上述很多最佳实践封装在了一起:
// WebGLBridge.cs using UnityEngine; using System; using System.Runtime.InteropServices; public class WebGLBridge : MonoBehaviour { public static WebGLBridge Instance; void Awake() { Instance = this; } // 平台判断 #if UNITY_WEBGL && !UNITY_EDITOR // 声明.jslib函数 [DllImport(“__Internal”)] private static extern void JSLog(string message); [DllImport(“__Internal”)] private static extern void JSSendEvent(string eventName, string eventData); [DllImport(“__Internal”)] private static extern string JSGetUserData(); #else // 非WebGL平台(编辑器、PC等)的模拟实现 private static void JSLog(string message) { Debug.Log(“[SIM JS]: “ + message); } private static void JSSendEvent(string eventName, string eventData) { Debug.Log($“[SIM JS] Event {eventName}: {eventData}”); } private static string JSGetUserData() { return “{\”name\“: \”EditorUser\“}”; } #endif // 供C#其他模块调用的安全方法 public static void LogToBrowser(string message) { #if UNITY_WEBGL && !UNITY_EDITOR JSLog(message); #else Debug.Log(message); #endif } public static void SendEventToPage(string eventName, object eventData) { string jsonData = “”; try { jsonData = JsonUtility.ToJson(eventData); } catch (Exception e) { Debug.LogError($“Failed to serialize event data for {eventName}: {e}”); return; } #if UNITY_WEBGL && !UNITY_EDITOR JSSendEvent(eventName, jsonData); #else Debug.Log($“[Page Event] {eventName}: {jsonData}”); #endif } // 供网页JS调用的方法(必须public) public void OnPageEvent(string eventJson) { try { // 解析并处理来自网页的事件 PageEventData data = JsonUtility.FromJson<PageEventData>(eventJson); // ... 分发到内部事件系统 ... } catch (Exception e) { Debug.LogError($“Failed to process page event: {eventJson}. Error: {e}”); } } } // 对应的.jslib文件简化示例 // WebGLBridge.jslib mergeInto(LibraryManager.library, { JSLog: function (msgPtr) { console.log(‘[Unity] ‘ + Pointer_stringify(msgPtr)); }, JSSendEvent: function (namePtr, dataPtr) { var eventName = Pointer_stringify(namePtr); var eventData = Pointer_stringify(dataPtr); // 触发自定义事件,让主页面监听 if (window.unityEventDispatcher) { window.unityEventDispatcher(eventName, eventData); } else { // 降级方案:直接尝试调用全局函数(适用于简单页面) try { if (window[‘onUnity’ + eventName]) { window[‘onUnity’ + eventName](JSON.parse(eventData)); } } catch (e) { console.error(‘Failed to dispatch unity event:‘, e); } } }, JSGetUserData: function () { var data = localStorage.getItem(‘userData’) || ‘{}’; var bufferSize = lengthBytesUTF8(data) + 1; var buffer = _malloc(bufferSize); stringToUTF8(data, buffer, bufferSize); return buffer; } });这套模式将平台差异、错误处理、数据序列化都封装好了,业务代码只需要调用WebGLBridge.SendEventToPage或响应OnPageEvent即可,使得Unity与网页的交互变得清晰而健壮。记住,清晰的架构和充分的错误处理,是保证WebGL项目稳定性的关键。