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#的回调:

csharp 复制代码
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

js 复制代码
/*
*   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#调用

csharp 复制代码
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;
            // ...
        }
    }
}
相关推荐
WarPigs3 小时前
Unity阴影
unity·游戏引擎
一只一只4 小时前
Unity之Invoke
unity·游戏引擎·invoke
tealcwu6 小时前
【Unity踩坑】Simulate Touch Input From Mouse or Pen 导致检测不到鼠标点击和滚轮
unity·计算机外设·游戏引擎
ThreePointsHeat7 小时前
Unity WebGL打包后启动方法,部署本地服务器
unity·游戏引擎·webgl
迪普阳光开朗很健康7 小时前
UnityScrcpy 可以让你在unity面板里玩手机的插件
unity·游戏引擎
陈言必行1 天前
Unity 之 设备性能分级与游戏画质设置与设备自动适配指南
游戏·unity·游戏引擎
CreasyChan1 天前
Unity DOTS技术栈详解
unity·c#·游戏引擎
在路上看风景1 天前
1.1 Unity资源生命周期管理与内存机制
unity
CreasyChan1 天前
Unity的ECS(Entity Component System)架构详解
unity·架构·游戏引擎
神码编程1 天前
【Unity】 HTFramework框架(六十九)Log在编辑器日志中自定义点击事件
unity·编辑器·游戏引擎