1. 项目概述:为什么我们需要深入jslib与浏览器API
如果你在Unity WebGL开发中,还停留在用Application.OpenURL打开网页,或者对如何让Unity内容与网页表单、摄像头、麦克风甚至本地文件系统进行深度交互感到束手无策,那么这篇文章就是为你准备的。Unity WebGL将我们的游戏或应用带到了浏览器,但这仅仅是开始。真正的挑战和机遇在于如何让这个“孤岛”与广阔的Web世界无缝连接。jslib(JavaScript Library)文件,就是架设在Unity C#世界与浏览器JavaScript世界之间最核心、最灵活的那座桥梁。
网络上关于jslib的基础教程不少,大多教你如何弹出一个“Hello World”对话框。但当我们面对真实项目需求时,比如从网页获取用户输入的复杂数据、调用设备硬件API、与第三方网页SDK(如支付、社交分享)对接,或者实现高性能的定制化图形操作时,就会发现那些基础示例远远不够。我们需要的是“进阶应用”,是深入理解数据如何跨边界安全高效地传递,是如何绕过Unity的默认限制直接操作浏览器底层API,以及如何构建稳定、可维护的交互架构。
最近在社区里看到不少相关的问题:WebGL加载Addressable包后资源丢失、与前端通信数据获取不到、需要集成特定的Web服务等等。这些问题的根源,往往都指向对jslib和浏览器集成机制的理解不够透彻。本文将从一个拥有多年踩坑经验的开发者视角,带你突破边界,不仅理解jslib的运作原理,更掌握一系列实战中提炼出的进阶技巧,让你能游刃有余地解决上述复杂集成需求。
2. jslib核心机制深度解析
2.1 jslib的本质:Emscripten的桥梁
首先必须明确一点:Unity WebGL的构建底层依赖于Emscripten编译器,它将C/C++(以及我们的C#脚本转换后的代码)编译成WebAssembly(Wasm)和JavaScript“胶水代码”。jslib文件并不是普通的JS脚本,它是专门设计给Emscripten链接器(Linker)的“库文件”。
当你把一个.jslib文件放在Assets/Plugins目录下,构建过程中,Emscripten会将其内容“合并”到最终生成的“胶水代码”(通常是framework.js或build.wasm的加载器)中。这就是为什么jslib中的函数可以直接被C#通过[DllImport("__Internal")]调用的原因——它们在编译后属于同一个模块(Module)作用域。
jslib的标准结构围绕mergeInto函数展开:
mergeInto(LibraryManager.library, { // 你的函数定义在这里 MyFunction: function () { // JavaScript 实现 }, });这里的LibraryManager.library就是Emscripten为C/C++导出函数准备的命名空间。mergeInto把你的函数注入进去,使其对编译后的Wasm模块可见。
关键理解:
jslib中的代码执行环境,是Emscripten的“模块运行时”,而非纯粹的网页全局环境。这意味着它可以直接访问Emscripten提供的一系列内置对象和内存模型,比如Module、HEAP、Pointer_stringify(旧版)等。但同时,它也可以通过window对象访问浏览器全局API。
2.2 复杂数据类型传递:指针、堆与类型化数组
基础教程只告诉你数字和字符串可以传,但一遇到数组、结构体或需要返回复杂数据时就懵了。核心在于理解Emscripten的堆(HEAP)。
当C#传递一个非基本类型(如float[])到jslib函数时,传递的实际上是一个指向Emscripten线性内存(堆)的指针(整数地址)。在JavaScript侧,我们需要通过不同的HEAP视图来读写这块内存。
例如,C#侧定义:
[DllImport("__Internal")] private static extern void ProcessFloatArray(float[] array, int length);在jslib中,对应的实现需要接收指针和长度:
ProcessFloatArray: function (arrayPtr, length) { // 将指针转换为HEAPF32视图的索引。右移2位是因为F32是4字节(2^2),指针地址是字节偏移。 var startIndex = arrayPtr >> 2; var heapArray = Module.HEAPF32.subarray(startIndex, startIndex + length); // 现在heapArray是一个JavaScript Float32Array视图,直接操作它就是在操作C#传来的数组内存。 for (var i = 0; i < length; i++) { heapArray[i] = heapArray[i] * 2; // 示例:将每个元素乘以2 } // 修改会直接反映回C#侧的原始数组! }为什么是HEAPF32?因为C#的float对应32位浮点数。Emscripten提供了多种视图来匹配不同数据类型:
HEAP8/HEAPU8: 8位有符号/无符号整数 (对应sbyte/byte)HEAP16/HEAPU16: 16位有符号/无符号整数 (对应short/ushort)HEAP32/HEAPU32: 32位有符号/无符号整数 (对应int/uint)HEAPF32: 32位浮点数 (对应float)HEAPF64: 64位浮点数 (对应double)
实操心得:在传递指针时,务必确保C#侧的数组是“pinned”(固定的)或者在调用期间不会被垃圾回收器移动。在WebGL中,由于托管堆与Wasm内存的交互方式,从C#传递基本类型的数组(如float[],int[])通常是安全的。但对于复杂对象,最可靠的方式是在C#侧使用GCHandle固定,或者更常见的做法是:在JavaScript侧分配内存并返回指针给C#,由C#负责后续释放。
2.3 字符串处理的演进与陷阱
字符串传递是另一个高频坑点。历史版本中,我们使用Pointer_stringify来将指针转换为JS字符串。但在较新的Emscripten和Unity版本中,这个函数可能已被标记为废弃,推荐使用更现代的UTF8ToString/stringToUTF8组合。
安全且兼容的字符串处理示例:
在jslib中接收C#字符串:
MyFunction: function (strPtr) { // 方法1(现代,推荐):使用UTF8ToString var jsString = UTF8ToString(strPtr); console.log("Received: " + jsString); // 方法2(兼容旧版):如果UTF8ToString未定义,回退到Pointer_stringify // var jsString = typeof UTF8ToString !== 'undefined' ? UTF8ToString(strPtr) : Pointer_stringify(strPtr); // ... 处理jsString }在jslib中创建字符串并返回给C#:
GetStringFromJS: function () { var jsString = "Hello from JS"; // 计算所需缓冲区大小(字节数),+1 用于C风格的结束符'\0' var bufferSize = lengthBytesUTF8(jsString) + 1; // 在Emscripten堆中分配内存 var bufferPtr = _malloc(bufferSize); // 将JS字符串写入分配的内存 stringToUTF8(jsString, bufferPtr, bufferSize); // 返回内存指针给C#,C#端需使用[MarshalAs(UnmanagedType.LPStr)]或相应的方式接收并释放 return bufferPtr; }在C#侧,对应的函数声明和调用需要处理这个指针:
[DllImport("__Internal")] private static extern IntPtr GetStringFromJS(); // 调用并转换 IntPtr ptr = GetStringFromJS(); string result = Marshal.PtrToStringUTF8(ptr); // .NET Core / .NET 5+ 或使用 Marshal.PtrToStringAnsi // !!!关键:必须释放由_malloc分配的内存 FreeJavaScriptString(ptr); // 需要另一个jslib函数来调用_free // 对应的释放函数 [DllImport("__Internal")] private static extern void FreeJavaScriptString(IntPtr ptr);在jslib中定义FreeJavaScriptString:
FreeJavaScriptString: function (ptr) { _free(ptr); }严重警告:内存泄漏是WebGL应用性能恶化和崩溃的常见原因。对于任何在JavaScript侧通过
_malloc分配并返回给C#的内存,必须在C#侧使用完毕后,通过对应的_free调用(同样通过jslib)进行释放。忘记这一步是许多“WebGL内存使用量只增不减”问题的根源。
3. 浏览器API集成实战:突破Unity的默认沙箱
Unity WebGL出于安全和跨平台一致性考虑,对许多浏览器API进行了限制或未暴露。通过jslib,我们可以直接调用这些API,实现强大功能。
3.1 访问设备硬件:摄像头与麦克风
Unity的WebCamTexture在WebGL上功能有限。我们可以通过jslib直接调用WebRTC的getUserMediaAPI,获得更原始的媒体流,进而实现高级功能(如自定义滤镜、音频分析)。
实现思路:
- 在
jslib中定义函数,使用navigator.mediaDevices.getUserMedia获取媒体流。 - 将媒体流绑定到一个隐藏的HTML
<video>元素上。 - 将这个
<video>元素作为纹理源,通过WebGL API(GLctx)上传到GPU,供Unity的Texture2D使用。
核心jslib代码片段:
var videoElement = null; var mediaStream = null; InitializeCamera: function (constraintsPtr) { var constraintsStr = UTF8ToString(constraintsPtr); var constraints = JSON.parse(constraintsStr); // 例如 {"video": true, "audio": false} return new Promise(function(resolve, reject) { if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { navigator.mediaDevices.getUserMedia(constraints) .then(function(stream) { mediaStream = stream; if (!videoElement) { videoElement = document.createElement('video'); videoElement.playsInline = true; videoElement.autoplay = true; videoElement.style.display = 'none'; document.body.appendChild(videoElement); } videoElement.srcObject = stream; videoElement.onloadedmetadata = function() { videoElement.play(); resolve(); }; }) .catch(function(err) { console.error("getUserMedia error: ", err); reject(err.name); // 将错误名传回C# }); } else { reject("MediaDevices not supported"); } }).then(function() { return 1; // 成功 }).catch(function(err) { return 0; // 失败,可通过其他方式传回错误信息 }); }, GetCameraFrameTexture: function () { if (!videoElement || videoElement.readyState < videoElement.HAVE_CURRENT_DATA) { return 0; // 返回0表示纹理ID无效 } // 假设我们已经有一个WebGL纹理对象,并绑定到了TEXTURE_2D // GLctx是Emscripten提供的WebGL上下文,通常指向主上下文 GLctx.texImage2D(GLctx.TEXTURE_2D, 0, GLctx.RGBA, GLctx.RGBA, GLctx.UNSIGNED_BYTE, videoElement); return 1; // 成功更新纹理 }, StopCamera: function () { if (mediaStream) { mediaStream.getTracks().forEach(track => track.stop()); mediaStream = null; } if (videoElement && videoElement.srcObject) { videoElement.srcObject = null; } }在C#侧,你需要创建一个Texture2D,并在每帧(或按需)调用GetCameraFrameTexture来更新它。这需要你理解Unity如何与外部WebGL纹理交互,通常涉及Texture2D.CreateExternalTexture和GL.IssuePluginEvent。
3.2 本地文件系统交互:File API与拖放上传
Unity的System.IO在WebGL中受到严格限制。通过集成浏览器的File API,我们可以实现丰富的文件操作。
场景:让用户拖放一个图片文件到网页上,然后在Unity中加载并显示。
实现步骤:
- 在网页的JavaScript中(可以是独立的
.js文件,也可以将逻辑写在jslib中并通过C#触发),为某个DOM元素(或整个页面)添加dragover和drop事件监听器。 - 在
drop事件中,使用event.dataTransfer.files获取文件列表。 - 使用
FileReader将文件读取为ArrayBuffer或DataURL。 - 将读取到的二进制数据或Base64字符串通过某种方式传递回Unity。这里有两种主流方式:
- 方式A:通过
SendMessage发送Base64字符串。适用于中小文件。将FileReader.readAsDataURL的结果通过unityInstance.SendMessage发送给Unity中的一个GameObject。C#端使用System.Convert.FromBase64String解码。 - 方式B:通过Emscripten堆传递二进制数据。适用于大文件,性能更好。在
jslib中分配一块内存(_malloc),将FileReader.readAsArrayBuffer的结果(Uint8Array)复制到这块内存中,然后将内存指针和文件大小传回C#。C#端使用Marshal.Copy将数据从指针复制到byte[]中。
- 方式A:通过
方式B的jslib核心代码:
HandleFileDrop: function (event) { event.preventDefault(); var file = event.dataTransfer.files[0]; if (!file) return; var reader = new FileReader(); reader.onload = function(e) { var arrayBuffer = e.target.result; var uint8Array = new Uint8Array(arrayBuffer); var fileSize = uint8Array.length; // 在堆中分配内存 var bufferPtr = _malloc(fileSize); // 将文件数据复制到堆内存中 Module.HEAPU8.set(uint8Array, bufferPtr); // 调用C#中注册的回调函数,传递指针和大小 // 假设我们有一个C#方法 `ReceiveFileData(IntPtr dataPtr, int dataSize)` unityInstance.SendMessage('FileManagerObject', 'ReceiveFileData', bufferPtr); unityInstance.SendMessage('FileManagerObject', 'ReceiveFileSize', fileSize); // 注意:内存释放应在C#端确认数据复制完成后,通过另一个jslib调用_free来进行。 }; reader.readAsArrayBuffer(file); }C#端处理:
public class FileManager : MonoBehaviour { // 由JavaScript调用 public void ReceiveFileData(string ptrStr) // SendMessage只能传字符串或简单数字 { // 将字符串形式的指针转换为IntPtr if (IntPtr.TryParse(ptrStr, out IntPtr dataPtr)) { // 需要先知道大小,这里假设我们通过另一个SendMessage收到了大小 // int fileSize = ...; // byte[] data = new byte[fileSize]; // Marshal.Copy(dataPtr, data, 0, fileSize); // 处理data... // 处理完后,通知JS释放内存 FreeFileData(dataPtr); } } [DllImport("__Internal")] private static extern void FreeFileData(IntPtr ptr); }注意事项:
SendMessage只能传递简单类型(字符串、数字)。传递指针需要将其转换为字符串。对于大文件,多次SendMessage调用可能带来性能开销。更高级的做法是使用jslib函数直接设置C#端可访问的“回调函数指针”,但这涉及更复杂的C#委托与函数指针封送,对新手不友好。对于大多数情况,方式A(Base64)对于几MB以下的文件是更简单安全的选择。
3.3 与第三方Web SDK集成
假设你需要集成一个网页支付SDK(例如支付宝、Stripe的Checkout)或社交分享按钮。这些SDK通常要求在你的页面中引入一个<script>标签,并调用其全局JavaScript函数。
集成策略:
- 脚本加载:不要在
jslib里动态插入<script>标签。最好在HTML模板文件(index.html)中直接引入第三方SDK。jslib只负责调用已加载的SDK函数。 - 状态通信:第三方SDK操作通常是异步的(如支付成功回调)。我们需要在网页全局作用域(
window)下定义一个回调函数,当SDK操作完成时,这个回调函数被触发,然后再通过unityInstance.SendMessage通知Unity。
示例:模拟支付流程在index.html的<head>中引入SDK:
<script src="https://js.some-payment-sdk.com/v3"></script>在jslib中:
// 假设支付SDK提供了一个全局对象 `PaymentSDK` LaunchPayment: function (orderInfoPtr) { var orderInfoJson = UTF8ToString(orderInfoPtr); var orderInfo = JSON.parse(orderInfoJson); // 定义全局回调,供SDK调用 window.onPaymentCompleted = function(result) { // 将结果发送回Unity unityInstance.SendMessage('PaymentManager', 'OnPaymentResult', JSON.stringify(result)); // 清理 window.onPaymentCompleted = null; }; // 调用第三方SDK PaymentSDK.checkout({ order: orderInfo, onSuccess: function(data) { window.onPaymentCompleted({status: 'success', data: data}); }, onFailure: function(err) { window.onPaymentCompleted({status: 'failure', error: err.message}); } }); }在C#中:
public class PaymentManager : MonoBehaviour { [DllImport("__Internal")] private static extern void LaunchPayment(string orderInfoJson); public void StartPayment(OrderInfo info) { string json = JsonUtility.ToJson(info); LaunchPayment(json); } // 由JavaScript调用 public void OnPaymentResult(string resultJson) { PaymentResult result = JsonUtility.FromJson<PaymentResult>(resultJson); if (result.status == "success") { Debug.Log("Payment succeeded!"); } else { Debug.LogError("Payment failed: " + result.error); } } }这种模式清晰地将第三方SDK的异步逻辑封装在JavaScript层,通过事件驱动的方式与Unity通信,保持了代码的清晰度和可维护性。
4. 性能优化与内存管理实战
WebGL应用性能敏感,不当的jslib使用会导致严重性能问题。
4.1 减少跨语言调用开销
每一次从C#到JavaScript的[DllImport]调用,或从JavaScript到C#的SendMessage,都有一定的开销。对于高频操作(如每帧调用的函数),这个开销会累积。
优化策略:
- 批处理数据:不要每帧传递大量小数据。例如,如果需要传递多个物体的位置,可以在C#端将它们打包成一个大的数组或结构体,一次性传递。
- 在JavaScript端维护状态:对于频繁查询的浏览器状态(如窗口大小、输入设备状态),可以在
jslib中设置一个定时器或利用requestAnimationFrame在JS端持续更新这些状态到某个变量中。C#端只需在需要时读取这个“缓存”的状态,而不是每次都发起跨语言调用。 - 使用直接回调(高级):对于极高性能要求的场景,可以探索Emscripten的
ccall或cwrap工具,或者设置函数指针,允许JavaScript直接调用C#函数。但这需要处理复杂的Marshalling和生命周期管理,复杂度陡增,非必要不推荐。
4.2 WebGL上下文与Unity渲染的协同
在jslib中,你可以通过GLctx直接访问WebGL上下文。这非常强大,但也非常危险。Unity管理着自己的渲染循环和状态机,直接操作GL上下文极易造成状态冲突,导致渲染错误或崩溃。
安全准则:
- 仅在Unity渲染调用之间操作:最安全的时机是在Unity通过
GL.IssuePluginEvent发出的自定义渲染事件回调中执行你的WebGL命令。这确保了Unity的渲染状态机知道你将要进行的操作。 - 保存与恢复状态:如果你必须修改WebGL状态(如绑定纹理、设置混合模式),务必在修改前保存当前状态,操作完成后立即恢复。Emscripten提供了一些辅助函数,但更可靠的是手动记录你修改的状态。
- 避免与Unity资源冲突:不要直接使用
GL.textures数组去操作Unity创建的纹理ID,除非你完全清楚该纹理当前未被Unity使用。更好的做法是,在JavaScript侧创建独立的WebGL纹理、缓冲区等资源,并通过jslib函数将资源句柄(如纹理ID)传回C#,在C#侧使用System.IntPtr来持有,并在合适的时机(如OnDestroy)通知JS侧释放。
4.3 内存泄漏排查清单
WebGL内存泄漏难以察觉但后果严重。以下是自查清单:
_malloc必须配对_free:检查所有在jslib中分配内存并返回指针给C#的函数,C#端是否都有对应的释放调用。- 事件监听器清理:在
jslib或关联的全局JS代码中为DOM元素添加的事件监听器(如resize,click),在Unity应用卸载或场景切换时,是否被正确移除(removeEventListener)? - 定时器清理:是否使用了
setInterval或setTimeout?确保在不需要时用clearInterval/clearTimeout清除。 - 对象引用残留:在JavaScript中,将对象(如媒体流、Canvas元素)存储在全局或模块变量中,会导致其无法被垃圾回收。在Unity的
OnApplicationQuit或对应MonoBehaviour的OnDestroy中,调用jslib的清理函数,显式地将这些引用置为null。 - 纹理与缓冲区:通过WebGL API(
GLctx.createTexture,GLctx.createBuffer)创建的资源,是否在不再需要时调用了GLctx.deleteTexture,GLctx.deleteBuffer?
一个良好的实践是在jslib中创建一个统一的Cleanup函数,在Unity应用退出时由C#调用,集中释放所有JS侧资源。
5. 调试与问题排查实录
5.1 浏览器开发者工具是你的主战场
- Sources面板:找到你的
jslib文件。它通常会被打包并内联到生成的.js文件中,但通过搜索函数名或“mergeInto”可以定位。在这里可以设置断点,单步调试jslib中的JavaScript代码。 - Console面板:在
jslib中大量使用console.log,console.warn,console.error输出调试信息。注意查看调用栈,可以追踪函数是从C#调用过来,还是从网页事件触发。 - Network面板:如果你的
jslib涉及网络请求(如调用外部API),在这里查看请求是否发出、状态码、响应内容。 - Memory面板:使用“Heap snapshot”功能定期拍摄堆快照,对比不同时间点的内存占用,查找未被释放的JavaScript对象或分离的DOM节点,这是排查内存泄漏的利器。
5.2 常见错误与解决方案
问题1:undefinedis not a function (evaluating 'Module.MyFunction')
- 原因:C#试图调用一个在JavaScript中未定义的函数。可能原因:
jslib文件未正确放置在Assets/Plugins(或子目录)下。jslib文件中的函数名与C#中[DllImport]声明的函数名不匹配(大小写敏感!)。- 构建后,
jslib代码因语法错误未能成功合并。
- 排查:检查浏览器控制台是否有其他JS错误。在Sources面板搜索你的函数名,确认其是否存在。
问题2:无效的指针错误,或访问HEAP时崩溃
- 原因:传递了错误的指针,或指针所指向的内存已被释放/无效。
- 排查:
- 确保在C#侧,数组在传递期间是有效的(例如,不是局部变量在函数返回后被回收)。
- 检查指针计算。
arrayPtr >> 2只对HEAPF32/HEAP32等视图正确,因为它们是4字节对齐。对于HEAP8/HEAPU8(1字节),直接使用arrayPtr即可。 - 在
jslib中访问指针前,可加入空指针检查:if (!arrayPtr) return;
问题3:SendMessage调用成功,但Unity收不到消息
- 原因:
SendMessage的objectName参数是场景中GameObject的名字,且该GameObject必须处于激活状态。methodName参数是挂在该GameObject上任意脚本的公有方法名。- 方法参数必须匹配。
SendMessage的第三个参数value只能是string或数字(int,float等)。如果你传递了数字,C#方法参数应为int,float,string(数字会被转换)。传递复杂对象需序列化为字符串。
- 排查:在浏览器控制台打印
unityInstance对象,确认其存在且SendMessage函数可用。检查GameObject名称和脚本方法名的拼写。
问题4:在jslib中调用某些浏览器API失败(如getUserMedia)
- 原因:浏览器安全限制。许多强大的Web API(如媒体设备、地理位置)必须在安全的上下文中运行(HTTPS或localhost),并且通常需要用户手势触发(如click事件)。
- 解决方案:确保你的网站在HTTPS下,或者通过localhost访问。对于需要用户手势的API,将其调用绑定到一个由用户点击触发的C#事件上(例如,一个UI按钮的
OnClick事件里调用jslib函数),而不是在Start()或Awake()中自动调用。
5.3 构建与部署注意事项
- 开发构建:在Player Settings的WebGL发布设置中,启用Development Build和Autoconnect Profiler。这会在生成的代码中包含源映射,并允许你使用Unity Profiler进行远程分析,对于调试性能问题至关重要。
- 代码压缩:发布时,代码压缩(如Google Closure Compiler)可能会重命名或优化掉
jslib中的某些函数,导致DllImport找不到。确保你的jslib函数都被mergeInto正确包装,并且考虑在构建设置中为jslib文件设置合适的“平台设置”,确保其不被过度优化。 - 多上下文(Multiple Contexts)问题:如果你的页面可能嵌入多个Unity WebGL构建实例(虽然不常见),要确保你的
jslib代码和全局回调函数是针对正确的unityInstance进行操作。使用传入的Module或unityInstance引用,而不是依赖全局变量。
6. 架构设计:构建可维护的C#-JS通信层
对于中型以上项目,直接在各个MonoBehaviour里散落[DllImport]和SendMessage调用会变得难以维护。建议设计一个中间层。
设计一个WebGLBridge单例管理器:
using UnityEngine; using System.Runtime.InteropServices; using System; using System.Collections.Generic; public class WebGLBridge : MonoBehaviour { public static WebGLBridge Instance; // 统一的事件中心,用于从JS接收消息 public event Action<string, string> OnJSMessage; // eventName, dataJson void Awake() { if (Instance == null) Instance = this; else Destroy(gameObject); DontDestroyOnLoad(gameObject); } // 供JS调用的统一入口点 public void ReceiveMessage(string messageJson) { var msg = JsonUtility.FromJson<JSMessage>(messageJson); OnJSMessage?.Invoke(msg.eventName, msg.data); } // 封装常用的jslib调用 [DllImport("__Internal")] private static extern void JS_InitializeCamera(string constraints); public void InitializeCamera(CameraConstraints constraints) { string json = JsonUtility.ToJson(constraints); JS_InitializeCamera(json); } [DllImport("__Internal")] private static extern void JS_LoadFileDialog(string acceptFilter); public void OpenFileDialog(string acceptFilter = ".png,.jpg,.jpeg") { JS_LoadFileDialog(acceptFilter); } // ... 其他封装方法 [Serializable] private struct JSMessage { public string eventName; public string data; } } // 在其他脚本中订阅事件 public class UIManager : MonoBehaviour { void OnEnable() { WebGLBridge.Instance.OnJSMessage += HandleJSMessage; } void OnDisable() { if (WebGLBridge.Instance != null) WebGLBridge.Instance.OnJSMessage -= HandleJSMessage; } void HandleJSMessage(string eventName, string data) { if (eventName == "FileSelected") { // 处理文件选择 } else if (eventName == "PaymentResult") { // 处理支付结果 } } }在jslib侧,所有对Unity的通信都通过这个单一的ReceiveMessage函数:
// 文件选择完成后 function onFileSelected(fileData) { var message = { eventName: "FileSelected", data: JSON.stringify({fileName: fileData.name, content: fileData.contentBase64}) }; unityInstance.SendMessage('WebGLBridge', 'ReceiveMessage', JSON.stringify(message)); }这种架构将通信逻辑集中,使C#与JavaScript的耦合度降低,更易于测试和扩展。当需要增加新的JS功能时,只需在WebGLBridge中添加对应的封装方法,并在事件中心注册新的事件类型即可。