Unity WebGL 项目中,JavaScript 桥接与 IndexedDB 缓存实现
在 Unity WebGL 项目中,JavaScript 桥接允许 C# 代码调用浏览器 API(如 IndexedDB),核心是通过 .jslib 插件实现双向通信。以下是具体实现步骤与技术细节:
1. 创建 jslib 插件文件
在 Unity 项目的 Assets/Plugins/WebGL 目录下新建一个 .jslib 文件(如 IDBWrapper.jslib),编写 JavaScript 代码与浏览器交互。
// IDBWrapper.jslib
mergeInto(LibraryManager.library, {
// 初始化 IndexedDB 数据库
IDB_Init: function () {
return new Promise((resolve, reject) => {
const request = indexedDB.open('AssetCache', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('assets')) {
db.createObjectStore('assets', { keyPath: 'key' });
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = (e) => reject(e);
});
},
// 存储资源到 IndexedDB
IDB_SaveAssetBundle: function (keyPtr, dataPtr, size) {
const key = UTF8ToString(keyPtr);
const data = HEAPU8.subarray(dataPtr, dataPtr + size);
return new Promise((resolve, reject) => {
indexedDB.open('AssetCache').onsuccess = (event) => {
const db = event.target.result;
const tx = db.transaction('assets', 'readwrite');
tx.objectStore('assets').put({ key: key, data: data });
tx.oncomplete = () => resolve();
tx.onerror = (e) => reject(e);
};
});
},
// 从 IndexedDB 读取资源
IDB_LoadAssetBundle: function (keyPtr) {
const key = UTF8ToString(keyPtr);
return new Promise((resolve, reject) => {
indexedDB.open('AssetCache').onsuccess = (event) => {
const db = event.target.result;
const tx = db.transaction('assets', 'readonly');
const request = tx.objectStore('assets').get(key);
request.onsuccess = () => {
if (request.result) {
const data = request.result.data;
const buffer = _malloc(data.length);
HEAPU8.set(data, buffer);
resolve(buffer);
} else {
reject('Resource not found');
}
};
request.onerror = (e) => reject(e);
};
});
}
});
2. C# 封装桥接接口
在 C# 脚本中声明外部函数,并封装为异步方法。
// IDBManager.cs
using System;
using System.Runtime.InteropServices;
using UnityEngine;
using System.Collections;
public class IDBManager : MonoBehaviour
{
// 声明 JavaScript 函数
[DllImport("__Internal")]
private static extern void IDB_Init(Action<string> onSuccess, Action<string> onError);
[DllImport("__Internal")]
private static extern void IDB_SaveAssetBundle(string key, IntPtr data, int size, Action onSuccess, Action<string> onError);
[DllImport("__Internal")]
private static extern void IDB_LoadAssetBundle(string key, Action<IntPtr> onSuccess, Action<string> onError);
// 初始化数据库
public IEnumerator InitDB()
{
bool isDone = false;
string error = null;
IDB_Init(
(success) => { isDone = true; },
(err) => { error = err; isDone = true; }
);
yield return new WaitUntil(() => isDone);
if (!string.IsNullOrEmpty(error)) {
Debug.LogError("初始化 IndexedDB 失败: " + error);
}
}
// 保存 AssetBundle 到 IndexedDB
public IEnumerator SaveAssetBundle(string key, byte[] data)
{
IntPtr unmanagedArray = Marshal.AllocHGlobal(data.Length);
Marshal.Copy(data, 0, unmanagedArray, data.Length);
bool isDone = false;
string error = null;
IDB_SaveAssetBundle(key, unmanagedArray, data.Length,
() => { isDone = true; },
(err) => { error = err; isDone = true; }
);
yield return new WaitUntil(() => isDone);
Marshal.FreeHGlobal(unmanagedArray);
if (!string.IsNullOrEmpty(error)) {
Debug.LogError("保存资源失败: " + error);
}
}
// 从 IndexedDB 加载 AssetBundle
public IEnumerator LoadAssetBundle(string key, Action<byte[]> onLoaded)
{
bool isDone = false;
string error = null;
IntPtr bufferPtr = IntPtr.Zero;
IDB_LoadAssetBundle(key,
(ptr) => { bufferPtr = ptr; isDone = true; },
(err) => { error = err; isDone = true; }
);
yield return new WaitUntil(() => isDone);
if (bufferPtr != IntPtr.Zero) {
// 假设数据长度为已知(需实际处理)
int length = 1024 * 1024; // 示例值,实际需记录大小
byte[] data = new byte[length];
Marshal.Copy(bufferPtr, data, 0, length);
onLoaded?.Invoke(data);
Marshal.FreeHGlobal(bufferPtr);
} else {
Debug.LogError("加载资源失败: " + error);
}
}
}
3. Unity 中的使用示例
在 Unity 中加载并缓存 AssetBundle。
// 示例:缓存并加载 AssetBundle
public class ResourceLoader : MonoBehaviour
{
private IDBManager idbManager;
IEnumerator Start()
{
idbManager = gameObject.AddComponent<IDBManager>();
yield return idbManager.InitDB();
// 下载并缓存资源
string url = "https://example.com/assetbundle";
string cacheKey = "asset_123";
UnityWebRequest webRequest = UnityWebRequest.Get(url);
yield return webRequest.SendWebRequest();
byte[] data = webRequest.downloadHandler.data;
yield return idbManager.SaveAssetBundle(cacheKey, data);
// 从缓存加载
yield return idbManager.LoadAssetBundle(cacheKey, (cachedData) => {
AssetBundle bundle = AssetBundle.LoadFromMemory(cachedData);
Instantiate(bundle.LoadAsset<GameObject>("Character"));
bundle.Unload(false);
});
}
}
4. 关键技术与注意事项
-
内存管理:
• C# 到 JavaScript 的数据传递:使用Marshal.AllocHGlobal分配非托管内存,通过HEAPU8.set()将数据复制到 JavaScript 的堆中。• 释放内存:在 JavaScript 返回数据指针后,C# 需调用
Marshal.FreeHGlobal避免内存泄漏。 -
异步处理:
• Promise 转协程:JavaScript 的异步操作(如IndexedDB的Promise)通过 C# 的Action回调转换为协程的yield return等待点。 -
缓存策略优化:
• 资源版本控制:为每个资源生成唯一 Hash 键(如MD5),避免重复缓存。• 缓存淘汰机制:设置最大缓存空间,按LRU(最近最少使用)策略删除旧资源。
-
跨浏览器兼容性:
• 检测 IndexedDB 支持:在 JavaScript 中先检查window.indexedDB是否存在。• 回退方案:若不支持 IndexedDB,可降级为使用
UnityWebRequestAssetBundle的内存或磁盘缓存。
5. 实际应用场景
• 游戏资源预加载:首次加载时将常用资源(如UI贴图、音效)缓存到 IndexedDB,后续启动时直接读取本地数据。
• 离线模式支持:通过缓存关键资源,允许玩家在无网络时访问部分内容。
• 大型资源分块缓存:将巨型场景拆分为多个AB包,按需加载和缓存。
总结
通过 jslib 桥接,Unity WebGL 项目可深度集成浏览器 API,利用 IndexedDB 实现资源持久化缓存,提升加载速度并减少网络流量。核心在于正确处理 C# 与 JavaScript 间的内存交互与异步通信,并结合业务需求设计合理的缓存策略。

浙公网安备 33010602011771号