Unity 華為快遊戲JS橋接 實現寫日誌等功能

lunoctis發表於2024-10-18

之前接入微信小遊戲本身程式碼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;
            // ...
        }
    }
}

相關文章