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. 关键技术与注意事项

  1. 内存管理:
    • C# 到 JavaScript 的数据传递:使用 Marshal.AllocHGlobal 分配非托管内存,通过 HEAPU8.set() 将数据复制到 JavaScript 的堆中。

    • 释放内存:在 JavaScript 返回数据指针后,C# 需调用 Marshal.FreeHGlobal 避免内存泄漏。

  2. 异步处理:
    • Promise 转协程:JavaScript 的异步操作(如IndexedDB的Promise)通过 C# 的 Action 回调转换为协程的 yield return 等待点。

  3. 缓存策略优化:
    • 资源版本控制:为每个资源生成唯一 Hash 键(如MD5),避免重复缓存。

    • 缓存淘汰机制:设置最大缓存空间,按LRU(最近最少使用)策略删除旧资源。

  4. 跨浏览器兼容性:
    • 检测 IndexedDB 支持:在 JavaScript 中先检查 window.indexedDB 是否存在。

    • 回退方案:若不支持 IndexedDB,可降级为使用 UnityWebRequestAssetBundle 的内存或磁盘缓存。


5. 实际应用场景

• 游戏资源预加载:首次加载时将常用资源(如UI贴图、音效)缓存到 IndexedDB,后续启动时直接读取本地数据。

• 离线模式支持:通过缓存关键资源,允许玩家在无网络时访问部分内容。

• 大型资源分块缓存:将巨型场景拆分为多个AB包,按需加载和缓存。


总结

通过 jslib 桥接,Unity WebGL 项目可深度集成浏览器 API,利用 IndexedDB 实现资源持久化缓存,提升加载速度并减少网络流量。核心在于正确处理 C# 与 JavaScript 间的内存交互与异步通信,并结合业务需求设计合理的缓存策略。

posted @ 2025-05-07 14:54  Allis  阅读(309)  评论(0)    收藏  举报