☰
Unity WebGL与网页深度交互:C#与JavaScript互调实战指南
2026/10/25 5:34:31 网站建设 项目流程

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环境是隔离的。

两个核心对象你需要牢记:

  1. Module对象:这是Emscripten生成的JavaScript运行时环境的总入口。Unity的WebGL构建会创建一个全局的Module对象(或者通过UnityLoader实例化的gameInstance.Module),里面包含了内存堆(HEAP)、函数表以及各种运行时方法。
  2. 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代码相对更难被直接破解。
  • 流程:
    1. Unity C#调用.jslib中的FetchFromServer(url, requestData)。
    2. .jslib中的函数使用浏览器fetchAPI向服务器发送请求。
    3. 收到响应后,在.jslib的回调函数中,通过unityInstance.SendMessage将结果数据传回Unity的某个接收方法。

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 性能与内存管理要点

  1. 减少跨边界调用:C#与JS的互调是有开销的,尤其是在WebAssembly和JavaScript之间切换上下文。避免在Update()循环中每帧都进行互调。
  2. 批处理消息:如果需要传递多个小数据,考虑将它们组合成一个JSON对象一次性传递,而不是多次调用SendMessage。
  3. 及时释放内存:如前所述,从JS返回字符串时,如果使用了_malloc,C#端必须用_free释放。对于.jslib中创建的DOM元素或对象引用,也要注意在适当时候置空,防止内存泄漏。
  4. 错误处理:在.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项目稳定性的关键。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询