之前接入微信小游戏本身代码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()
遍历打印qg
和ral
两个对象,键值对的文本是能对的上的。直接调用又有些内容不同,比如有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; // ... } } }