Unity 华为快游戏JS桥接 实现写日志等功能

之前接入微信小游戏本身代码js桥接比较完善,抖音小游戏有缺少但也没缺的这么多,华为这边的API,大残啊!官方转换插件Github仓库上一次提交在3月份。(截至现在)API给的很简略,接入js代码那里说看demo,但unity的demo里面没jslib,另一个比较完善的demo看起来像是cocos的,比较无奈。

还好用unity导出的webgl产物,和导出rpk这两部是分开的,测试调整jslib可以之间改xxx.framework.js来快速尝试,不用浪费大量的时间用来打包。

华为官方的JS API文档中给出的API全局对象是qg, 在转换插件内部的代码QGSDK-Call-JS.jslib中可以找到大量的QG_开头的函数,没有qg字段,但API看起来都使用ral作为入口,再看打包插件里面的ral.js是调用qg的。

测试发现 ral === window.ral, qg === window.qg, 直接通过Object.keys(x).ForEach()遍历打印qgral两个对象,键值对的文本是能对的上的。直接调用又有些内容不同,比如有qg.showToast但没有ral.showToast,都使用qg,测试qg.showToast是能用的,不知道会不会有其他问题。

以下为桥接代码的思路展示,一些接口基于实际需求进行了简化。

处理js返回C#的回调:


using LitJson;
using System.Collections.Generic;
using UnityEngine;

namespace Plugins.SDK {
    public delegate void SDKCallback(int code, string msg, string dataStr);

    public static class SDKCode {
        public const int Succeed = 1;
        public const int Failed = -1;
        public const int Cancel = -2;
    }

    // 用于处理回调
    public class CsJsEventHandler : MonoBehaviour
    {
        private static CsJsEventHandler s_Instance = null;
        public static CsJsEventHandler Instance
        {
            get
            {
                if (s_Instance == null)
                {
                    GameObject go = new GameObject("CsJsEventHandler");
                    GameObject.DontDestroyOnLoad(go);
                    s_Instance = go.AddComponent<CsJsEventHandler>();
                }
                return s_Instance;
            }
        }
    }

    private readonly Dictionary<string, SDKCallback> m_Callbacks = new Dictionary<string, SDKCallback>();
    private readonly HashSet<string> m_PersistentCallbacks = new HashSet<string>();

    public void StartUp()
    {
    }

    public string AddCallback(string funcName, SDKCallback callback, bool persistent = false)
    {
        string callbackId = $"{funcName}_{System.DateTime.Now:ddHHmmssfff}";
        m_Callbacks[callbackId] = callback;
        if (persistent)
        {
            m_PersistentCallbacks.Add(callbackId);
        }
        return callbackId;
    }

    public void RemoveCallback(string callbackId)
    {
        m_Callbacks.Remove(callbackId);
        m_PersistentCallbacks.Remove(callbackId);
    }

    // 经常调用又是异步的函数
    public string AddCallbackSingleton(string funcName, SDKCallback callback)
    {
        string callbackId = $"{funcName}_singleton";
        m_Callbacks[callbackId] = callback;
        m_PersistentCallbacks.Add(callbackId);
        return callbackId;
    }

    public string GetCallbackSingleton(string funcName)
    {
        string callbackId = $"{funcName}_singleton";
        return m_PersistentCallbacks.Contains(callbackId) ? callbackId : string.Empty;
    }

    public string RemoveCallbackSingleton(string funcName)
    {
        string callbackId = $"{funcName}_singleton";
        RemoveCallback(callbackId);
    }

    public void ClearCallbacks()
    {
        m_Callbacks.Clear();
        m_PersistentCallbacks.Clear();
    }

    private static string GetJsonString(JsonData obj, string key)
    {
        if (!obj.ContainsKey(key)) return string.Empty;
        return obj[key]?.ToString() ?? string.Empty;        
    }

    private static int GetJsonInt(JsonData obj, string key, int defaultValue = 0)
    {
        string s = GetJsonString(obj, key);
        if (int.TryParse(s, out int v))
        {
            return v;
        }
        return defaultValue;
    }

    public void HandleJsEvent(string jsonStr)
    {
        if (string.IsNullOrEmpty(jsonStr)) return;

        JsonData jsonData = JsonMapper.ToObject(jsonStr);
        string callbackId = GetJsonString(jsonData, "callbackId");
        if (m_Callbacks.TryGetValue(callbackId, out SDKCallback callback))
        {
            int code = GetJsonInt(jsonData, "code", SDKCode.Failed);
            string msg = GetJsonString(jsonData, "msg");
            string dataStr = GetJsonString(jsonData, "data");

            if (!m_PersistentCallbacks.Contains(callbackId))
            {
                m_Callbacks.Remove(callbackId);
            }
            callback.Invoke(code, msg, dataStr);
        }
    }
}

jslib

/*
*   handler return code:
*       SUCCEED: 1
*       FAILED: -1
*       CANCEL: -2
*
*   只展示部分函数,请根据实际需求自行添加
*/

var js_bridge_sdk_api = {

// File System
// 主要是调试\日志用, 能用同步的都用同步了

    JSB_GetUserDataPath: function() {
        return JSBHelper.stringToBuffer(qg.env.USER_DATA_PATH);
    },
    JSB_FSMAccessSync: function(path) {
        var fsm = qg.getFileSystemManager();
        try {
            fsm.accessSync(UTF8ToString(path));
            return JSBHelper.stringToBuffer("access:ok");
        } catch(error) {
            return JSBHelper.stringToBuffer(`error:${error}`);
        }
    },
    JSB_FSMCopyFileSync: function(path) { /* ... */ },
    JSB_FSMMkdirSync: function(dirPath) { /* ... */ },
    JSB_FSMRmdirSync: function(dirPath) { /* ... */ },
    JSB_FSMReaddirSync: function(dirPath) {
        var fsm = qg.getFileSystemManager();
        try {
            var result = fsm.readdirSync(UTF8ToString(dirPath));
            var text = JSON.stringify(result);
            return JSBHelper.stringToBuffer(text);
        } catch(error) {
            return JSBHelper.stringToBuffer("[]");
        }
    },
    JSB_FSMUnlinkSync: function(path) { /* ... */ },
    JSB_FSMReadTxtFile: function(filePath) {
        // readFileSync可以读utf8和binary, 不会写byte[]数组怎么从js传递到C#, 但因为没有需求, 先直接跳过了
        var fsm = qg.getFileSystemManager();
        try {
            var result = fsm.readFileSync(UTF8ToString(dirPath), "utf8");
            return JSBHelper.stringToBuffer(result);
        } catch(error) {
            return JSBHelper.stringToBuffer("");
        }
    },
    JSB_FSMWriteTxtFile: function(filePath, text) {
        // 同上
        var fsm = qg.getFileSystemManager();
        try {
            fsm.readFileSync(UTF8ToString(filePath), UTF8ToString(text), "utf8");
            return JSBHelper.stringToBuffer("ok");
        } catch(error) {
            return JSBHelper.stringToBuffer(`error:${error}`);
        }
    },
    JSB_FSMAppendTxtFile: function(filePath, text) {
        // 同上
        var fsm = qg.getFileSystemManager();
        try {
            fsm.appendFileSync(UTF8ToString(filePath), UTF8ToString(text), "utf8");
            return JSBHelper.stringToBuffer("ok");
        } catch(error) {
            return JSBHelper.stringToBuffer(`error:${error}`);
        }
    },

// UI

    JSB_ShowToast: function(config) {
        // 参数尽量简单, 用一个json字符串传递
        var options = JSON.parse(UTF8ToString(config));
        qg.showToast(options);
    },
    JSB_HideToast: function(config) {
        qg.hideToast({});
    },
    JSB_ShowModal: function(config) {
        // 快应用加载器里面这个modal好像能点穿的, 略坑
        var options = JSON.parse(UTF8ToString(config));
        var callbackId = options.callbackId; // 这里的callbackId是C#传递过来的, 后面展示
        delete options.callbackId;
        options.success = function(res) {
            if (res.confirm) {
                JSBHelper.returnEvent(callbackId, 1, "", "");
            }
            else if (res.cancel) {
                JSBHelper.returnEvent(callbackId, -2, "", "");
            }
        };
        options.fail = function() {
            JSBHelper.returnEvent(callbackId, -1, "", "");
        };
        qg.showModal(options);
    },
    JSB_ShowLoading: function(title) {
        qg.showLoading({
            title: UTF8ToString(title),
            mask: true,
        });
    },
    JSB_HideLoading: function() {
        qg.hideLoading({});
    },

// Misc

    JSB_GetBatteryLevel: function() {
        var info = qg.getBatteryInfoSync();
        return parseInt(info.level);
    },
    JSB_TriggerGC: function() {
        qg.triggerGC();
    },
    JSB_GetNetworkType: function() {
        var callbackId_j = UTF8ToString(callbackId);
        qg.getNetworkType({
            success: function(res) {
                var networkType = res.networkType;
                if (networkType === undefined) networkType = "unknown";
                JSBHelper.returnEvent(callbackId_j, 1, "", networkType);
            },
            fail: function() {
                JSBHelper.returnEvent(callbackId_j, -1, "", "");
            }
        });
    },
    JSB_OnNetworkStatusChange: function(callbackId) {
        // 因为回调一旦挂上就没有移除的需求, 没有把callback存起来
        var callbackId_j = UTF8ToString(callbackId);
        qg.onNetworkStatusChange(function(res) {
            var data = JSON.stringify(res);
            JSBHelper.returnEvent(callbackId_j, 1, "", data);
        });
    },
    JSB_Vibrate: function(mode) {
        if (mode == 0) {
            qg.vibrateShort({});
        }
        else {
            qg.vibrateLong({});
        }
    },
    JSB_PreviewImage: function(url) {
        qg.previeImage({
            urls: [
                UTF8ToString(url)
            ]
        });
    },

// Helper

    $JSBHelper: {
        stringToBuffer: function(valueStr) {
            var bufferSize = lengthBytesUTF8(valueStr) + 1;
            var buffer = _malloc(bufferSize);
            stringToUTF8(valueStr, buffer, bufferSize);
            return buffer;
        },
        returnEvent: function(callbackId, code, msg, dataStr) {
            var obj = {
                callbackId: callbackId,
                code: code,
                msg: msg,
                data: dataStr,
            };
            var text = JSON.stringify(obj);
            qg.unityInstance.Module.SendMessage("CsJsEventHandler", "HandleJsEvent", text);
            // Unity2021 文档给的是"MyGameInstance", 需要到index.html里面自己新建一个var并获取unityInstance
            // 但在这里不行, 测试"SendMessage"和"qg.unityInstance.Module.SendMessage"可用
        }
    }
};

autoAddDeps(js_bridge_sdk_api, "$JSBHelper");
mergeInfo(LibraryManager.library, js_bridge_sdk_api);

C#调用


using LitJson;
using System.Runtime.InteropServices;

namespace Plugins
{
    public static partial class JSBridgeExterns
    {
#if !UNITY_EDITOR
        [DllImport("__Internal")]
        public static extern string JSB_GetUserDataPath();

        // ... 省略, 函数签名对上即可
#else
        // 如果调用的代码不想写#if, 可以再写一遍UNITY_EDITOR的版本
        public static string JSB_GetUserDataPath() => default;

        // ...
#endif
    }

    // 调用展示
    public static class Test
    {
        public static void Test()
        {
            // 基础调用

            // 获取文件目录            
            string userDataPath = JSBridgeExterns.JSB_GetUserDataPath();

            // toast
            JsonData toastOptions = new JsonData();
            toastOptions["title"] = "Hello, world!";
            toastOptions["icon"] = "none";
            toastOptions["mask"] = true;
            toastOptions["duration"] = 2000;
            JSBridgeExterns.JSB_ShowToast(toastOptions.ToJson());

            // 带回调的调用
            SDKCallback callback = (code, msg, dataStr) => {
                if (code == SDKCode.Succeed)
                {
                    // ...
                }
                else if (code == SDKCode.Cancel)
                {
                    // ...
                }
                else if (code == SDKCode.Failed)
                {
                    // ...
                }
            };
            string callbackId = CsJsEventHandler.Instance.AddCallback("showModal", callback);
            JsonData modalOptions = new JsonData();
            modalOptions["title"] = "系统";
            modalOptions["content"] = "您有新短消息, 请注意查收";
            modalOptions["callbackId"] = callbackId;
            JSBridgeExterns.JSB_ShowModal(modalOptions.ToJson());

            // 挂网络状态监听
            string singletonCallbackId = CsJsEventHandler.Instance.AddCallbackSingleton("onNetworkStatusChange", _OnNetworkStatusChange);
            JSBridgeExterns.JSB_OnNetworkStatusChange(singletonCallbackId);
        }

        private static void _OnNetworkStatusChange(int code, string msg, string dataStr)
        {
            string json = dataStr;
            // ...
        }
    }
}

posted @ 2024-10-18 16:33  lunoctis  阅读(273)  评论(0编辑  收藏  举报