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一律用.jslibJS调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修改了HEAPmyData中的值也会改变因为传递的是指针 }对于其他类型数组如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内容已经正确实例化。现代推荐的方式是使用createUnityInstance2020.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.”); } } /scriptUnity C#端你需要确保场景中存在名为Player、GameManager、VolumeController的GameObject并且它们上面挂载的脚本有对应的公共方法。// 挂载在名为“Player”的GameObject上 public class PlayerController : MonoBehaviour { // 方法名必须与SendMessage的第二个参数完全匹配 public void Jump() { // 实现跳跃逻辑 GetComponentRigidbody().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 BridgeGameObject。所有网页端的调用都指向这个桥接器再由它分发到游戏内的各个系统。步骤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.FromJsonWebEventData(eventDataJson); EventDispatcher.Instance.Dispatch(data.eventType, data.params); } // 一个更具体的例子更新玩家数据 public void UpdatePlayerData(string dataJson) { PlayerData data JsonUtility.FromJsonPlayerData(dataJson); FindObjectOfTypePlayerInventory().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的某个接收方法。5.3 场景三处理二进制数据如图片、音频传递大的二进制数据如从Unity下载纹理或在网页端生成音频流给Unity播放是性能瓶颈。使用HEAPU8视图你可以将字节数组从C#传到JSJS可以将其转换为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返回字符串时如果使用了_mallocC#端必须用_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调用SendMessageUnity端没反应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.FromJsonPageEventData(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项目稳定性的关键。