多平台统一MaxSDK的设计实现
MaxSDK介绍
研发对接SDK,同一个游戏需要对接安卓、iOS等系统,在我们集团,因业务拆分,可能同游戏还需要对接37网游、37手游、37Games,不同的部门、系统对接接口存在差异,同时研发对Android的Java语言、iOS的Object-C语言不熟悉,进一步导致SDK对接成本、沟通成本飙升,对游戏研发来说,耗费了过多的精力和成本在处理对接SDK的事上。如图所示:
如果有一套多平台统一SDK,直接输出游戏研发语言级接口(例如unity的c#),研发可以直接调用,方便研发对接,将大大降低对接和沟通成本。
根据以上的背景和需求,我们设计并实现了一套多平台统一的SDK:MaxSDK。
MaxSDK是跨引擎、跨平台的统一对接SDK,通过直接输出游戏研发语言级接口(例如unity的c#),来提高研发对接SDK的效率,降低对接时的认知、沟通成本,减少对接时候研发的精力损耗,让研发能把精力集中提升到游戏内容和品质上,提高竞争力。
接入MaxSDK后的情况如图所示:
由上图可见,MaxSDK 屏蔽了复杂的跨语言桥接转调流程,通过统一的接口设计,统一的参数处理,统一的回调处理,灵活的参数扩展和方法扩展,实现了跨引擎、跨业务线、跨平台的封装实现。
如何实现MaxSDK
2.1、SDK整体架构设计
MaxSDK如何实现跨引擎和跨多平台统一,这里重点介绍其分层架构思路和具体实现 。首先我们抛开技术,从业务的核心需求出发,去调研拆解,找到各部门和研发的痛点。
目前集团现状:
研发需要对接安卓、iOS等平台,后续可能覆盖鸿蒙、小游戏等
可能需要对接37网游、37Games、37手游
国内国外,SDK要求上存在客观差异,体现在接口不同、参数差异等
从研发调研意见来看:
研发希望能直接用引擎语言实现对接(例如unity的c#),最好只有一个入口类
希望能屏蔽跨语言的实现细节
希望接口尽量简洁,易于理解,调用和回调逻辑清晰
希望支持直接导入package文件,方便集成
从拆解情况来简单分层,引擎层是必不可少,涉及研发的对接,以及引擎接口的调用。而要接入平台,那平台层也不可或缺,平台层涉及具体功能的实现和回调。而多个业务线,业务逻辑区分,可体现在平台层中。总体梳理后,大体结构如图所示:
引擎层。包括unity、cocos等,负责call调用 MaxSDK 的相关引擎接口,处理回调结果。
平台层。包括各业务线在不同平台SDK的具体功能实现,并将处理结果callback回调给引擎层。
2.2、引擎层架构设计
先从引擎层的架构设计出发,以unity引擎为例。研发需要在游戏场景中调用MaxSDK的c#接口来实现相关功能,在MaxSDK中要调用目标平台,例如安卓,就需要将c#接口桥接转调安卓的java接口,然后在安卓中回调处理结果给c#接口。整个架构如下:
研发接入层。研发集成MaxSDK,调用相关功能接口。
统一接口层(c#)。实现MaxSDK,c#统一接口声明和参数定义。
接口桥接层。实现跨语言的接口桥接转调。
平台实现抽象层。平台层的统一接口声明和参数定义,例如安卓的接口定义层。
平台具体实现层。平台层具体功能实现,例如安卓的登录接口的具体实现。
2.3、引擎层功能实现
将概要层级分好之后,我们以unity为例,可以进一步细化分层架构,按照之前调研研发的反馈,将整个类关系初步设计为:
研发只需要调用MaxSDK的接口,实现MaxSDKListener的回调,即可完成MaxSDK的对接。
图中的 UnityEventHandler类 ,继承自 MonoBehaviour,可供Unity的场景绑定为对应脚本。
MaxSDK 作为统一的调用出口,封装了不同平台的具体实现的接口调用分发。
具体实现层,根据不同的平台SDK,实现逻辑不同。
2.3.1、环境安装
类的交互架构设计好之后,就可以开始具体coding了。在unity中,编码前需要安装相关引擎环境。主要包括:
Unity(c#)
https://unity.cn/releases
Visual Studio
https://visualstudio.microsoft.com/zh-hans/vs/mac/
VS Code
https://code.visualstudio.com/
在unity安装完成后,项目依赖的unity版本要特别注意,之间版本的差异可能导致项目报错,最好保持版本一致。
打开后,在unity 编辑器的外部工具设置中,可将code的IDE关联起来,可选VS或者VS CODE。
进入unity编辑器后,我们可以通过拖拽实现一个简单的SDK demo 界面:
2.3.2、核心接口
开发环境准备就绪后,首先我们需要根据各业务部门的需求,拆出接口,定义出MaxSDK要支持的功能。首先定义一个接口base类 MaxSDKUnitySupportBase,其中包含了各种核心接口,例如初始化init、登录Login、登出Logout、支付Pay等。
/*** unity调用平台base类** 具体平台,可以通过继承该base类,按需复写*/namespace maxsdk{public abstract class MaxSDKUnitySupportBase{public abstract int GetSDKType();public abstract void SetListener(MaxSDKListener listener);public abstract void Init(MaxSDKInitInfo info);public abstract void Login(MaxSDKLoginInfo info);public abstract void Logout(MaxSDKLogoutInfo info);public abstract void Pay(MaxSDKPayInfo info);public abstract void ReportRoleInfo(MaxSDKRoleInfo info);public abstract void SwitchAccount(MaxSDKSwitchAccountInfo info);public abstract void ReportEvent(MaxSDKReportEventInfo info); //埋点事件上报public abstract void OpenActionExt(MaxSDKActionInfo info);public abstract void Share(MaxSDKShareInfo info);public abstract string DispatchSync(MaxSDKDispatchInfo info);public abstract void DispatchASync(MaxSDKDispatchInfo info);public abstract bool IsActionSupported(int type);/* ----------- 我是一条华丽的分割线 ------------ */public virtual string GetAppConfig(){return "";}public virtual void ExitGame(MaxSDKExitInfo info){}public virtual string DispatchSync(string methodName, MaxSDKBaseInfo info){return "";}public virtual void DispatchASync(string methodName, MaxSDKBaseInfo info){}}}
2.3.3、单例入口类
接口定义清楚后,在研发调用层,必须有一个统一的接口入口,此处定义一个单例类MaxSDK,方便研发调用。
/*** MaxSDK,统一接口单例类*/namespace maxsdk{public sealed class MaxSDK{private static readonly MaxSDK INSTANCE = new MaxSDK();private MaxSDKUnitySupportBase supportBase;public static MaxSDK GetInstance(){return INSTANCE;}//........省略后续代码..............}}
此处,我们将前面定义的核心接口类 `MaxSDKUnitySupportBase`,以组合方式集成进来,方便后续接口的调用。
注意:此处为啥用组合而不用继承?一个考虑点是后续接口有扩展需更新SDK,如果用继承的方式,即使不做具体实现,那研发必须复写新接口,对研发侵入性高。改为组合模式集成后,研发按需调用和实现接口即可,无侵入性。
2.3.4、对外接口设计
研发调用对外的单例接口实现相关功能,必然要有对外的接口层和具体实现关联起来。在MaxSDK类中,直接转调 MaxSDKUnitySupportBase 接口中的方法。
/*** 获取当前 SDK 的类型*/public int GetSDKType(){if (supportBase == null){return MaxSDKType.MAX_SDK_UNKNOWN;}return supportBase.GetSDKType();}public string GetAppConfig(){if (supportBase == null){return "";}return supportBase.GetAppConfig();}/*** 设置监听器*/public void SetListener(MaxSDKListener listener){supportBase?.SetListener(listener);}/*** 初始化*/public void Init(MaxSDKInitInfo initInfo){supportBase?.Init(initInfo);}/*** 登录*/public void Login(MaxSDKLoginInfo loginInfo){supportBase?.Login(loginInfo);}/*** 数据上报接口*/public void ReportRoleInfo(MaxSDKRoleInfo roleInfo){supportBase?.ReportRoleInfo(roleInfo);}/*** 支付*/public void Pay(MaxSDKPayInfo orderInfo){supportBase?.Pay(orderInfo);}//........省略后续类似转调代码..............
2.3.5、参数设计
接口方法的调用,不可缺少参数,参数包括入参和出参。在参数设计上,涉及不同业务部门,必须要统一标准,降低对接认知负担。
接口方法入参:
统一入参规范,XXXInfo,代表入参规范类名,例如`MaxSDKLoginInfo`;
所有接口的 XXXInfo 均继承 `MaxSDKBaseInfo`,内置`extData`,方便json参数返回扩展
将多个不同接口参数统一封装在`MaxSDKInfo.cs`文件中
接口参数封装和接口关联的常量和变量
/*** XXXInfo,代表入参规范类名*/namespace maxsdk{//通用请求参数,封装对象 MaxSDKBaseInfopublic class MaxSDKBaseInfo{public string extData; //扩展字段,json格式}//初始化接口,请求infopublic class MaxSDKInitInfo : MaxSDKBaseInfo{public string appId; //应用ID}//登录接口,请求infopublic class MaxSDKLoginInfo : MaxSDKBaseInfo{}// 支付接口,订单infopublic class MaxSDKPayInfo : MaxSDKBaseInfo{public string productId; //商品IDpublic string productName; //商品名称public string serverId; //区服IDpublic string serverName; //区服名称public string roleId; //角色IDpublic string roleName; //角色名称public string roleType = ""; // 角色类型,战士/道士/法师…………public int roleLevel; // 角色等级public string roleBronLevel = ""; //转生等级,默认传0public string roleLevelMTime; //角色等级变化时间(单位:秒)public string partyName; //帮派名public string cpOrderId; //研发订单号public string orderTime; //订单服务器时间public string sign; //订单签名,由服务器生成,参与sign的订单时间和orderTime保持一致public int radio = 10; //充值比例public float money; //支付金额public int gameCoin = -1; //游戏币,用于生成signpublic bool subscription; //该商品是否为订阅制商品,默认为false}}//........省略后续类似代码..............//
接口回调出参:
参数设计,统一出参接口规范。
统一出参规范, XXXBean,代表返回参数规范类名;例如 `MaxSDKBean`
所有接口的返回 XXXBean 均继承 `MaxSDKBaseBean`,内置`extData`,方便json参数返回扩展
将多个不同接口返回参数统一封装在`MaxSDKBean.cs`文件中
接口参数封装和接口关联的常量和变量
/*** XXXBean,代表返回参数规范类名*/namespace maxsdk{/*** 通用返回参数,封装对象 MaxSDKBaseBean*/public class MaxSDKBaseBean{public string extData; //扩展字段,json格式}// 失败信息public class MaxSDKFailBean : MaxSDKBaseBean{/** 错误码*/public string code;/** 错误提示*/public string msg;}//init初始化接口,返回beanpublic class MaxSDKInitBean : MaxSDKBaseBean{//todo 待补充}// 用户信息,登录回调中使用public class MaxSDKLoginBean : MaxSDKBaseBean{public string token; //用户的tokenpublic string puid; //渠道侧的用户IDpublic string uid; //SDK侧的用户IDpublic string uname; //SDK侧用户帐号}// 支付信息,支付回调中使用public class MaxSDKPayBean : MaxSDKBaseBean{/** 平台订单号*/public string platformOrderID;}}//........省略后续类似代码..............//
2.3.6、功能实现类
引擎要调用多个平台不同的实现。在unity中,可利用unity的特定编译标识符来实现区分:
> UNITY_ANDROID - Android平台
> UNITY_IOS - iOS平台
> UNITY_STANDALONE_WIN - windows
> UNITY_STANDALONE_OSX - mac OSX
> UNITY_EDITOR - unity中编辑器
/*** MaxSDK,统一接口单例类*/namespace maxsdk{public sealed class MaxSDK{private static readonly MaxSDK INSTANCE = new MaxSDK();private MaxSDKUnitySupportBase supportBase;public static MaxSDK GetInstance(){return INSTANCE;}//构造函数private MaxSDK(){Debug.Log("开始设置Unity-平台桥接,当前平台:" + Application.platform);#if UNITY_ANDROID && !UNITY_EDITORsupportBase = new MaxSDKUnitySupportAndroid();#elif UNITY_IOS && !UNITY_EDITORsupportBase = new MaxSDKUnitySupportIOS();#elif UNITY_STANDALONE_WIN && !UNITY_EDITORsupportBase = new MaxSDKUnitySupportWin();#endif}//........省略后续类似代码..............//}
还是以安卓为例,声明了当前编译环境是导出安卓平台工程时,实例化 MaxSDKUnitySupportAndrioid:
supportBase = new MaxSDKUnitySupportAndrioid();2.3.7、扩展接口
有时候我们的核心接口并不能满足研发的需求,可能不同业务线有额外的接口需要对接,为此设计了扩展方法接口:同步的DispatchSync接口和异步 DispatchASync接口。
接口声明:
/*** unity调用平台base类** 具体平台,可以通过继承该base类,按需复写*/namespace maxsdk{public abstract class MaxSDKUnitySupportBase{public virtual string DispatchSync(string methodName, MaxSDKBaseInfo info){return "";}public virtual void DispatchASync(string methodName, MaxSDKBaseInfo info){}}}
接口实现:
2.3.8、接口桥接转调
在具体实现类,实现 c# --> 目标平台接口的桥接转调,这也是跨平台的核心,不同目标平台有不同的桥接实现。以上述安卓平台为例,MaxSDKUnitySupportAndrioid就是具体的核心实现:
以 Login 登录为例,MaxSDKUnitySupportAndrioid在实例化时,在构造函数实例化AndroidJavaObject (AndroidJavaObject是c#调用Java 类实例的封装)对象unityActivity,研发在调用MaxSDK的 Login接口时,通过unityActivity对象的Call方法来实现接口转调。
其他平台的实例化也类似,例如iOS的转调实现:
/*** unity调用ios中的方法*/using System.Runtime.InteropServices;namespace maxsdk{//#if UNITY_IOS && !UNITY_EDITORpublic class MaxSDKUnitySupportIOS : MaxSDKUnitySupportBase{public override int GetSDKType(){int sdkType = MaxSDKType.MAX_SDK_UNKNOWN;string typeStr = MaxSDK_Call_Sync("getSDKType", "");int.TryParse(typeStr, out sdkType);return sdkType;}public override void SetListener(MaxSDKListener listener){Debug.Log("gameObject is " + listener.gameObject.name);if (listener == null){Debug.LogError("set SQSDKListener error, listener is null");return;}string gameObjectName = listener.gameObject.name;MaxSDK_Call_Async("setGameObject", gameObjectName);}public override void Init(MaxSDKInitInfo info){MaxSDK_Call_Async("init", JsonConvert.SerializeObject(info, Formatting.Indented));}public override void Login(MaxSDKLoginInfo info){MaxSDK_Call_Async("login", JsonConvert.SerializeObject(info, Formatting.Indented));}public override void Logout(MaxSDKLogoutInfo info){MaxSDK_Call_Async("logout", JsonConvert.SerializeObject(info, Formatting.Indented));}public override void Pay(MaxSDKPayInfo info){MaxSDK_Call_Async("pay", JsonConvert.SerializeObject(info, Formatting.Indented));}public override void Share(MaxSDKShareInfo info){MaxSDK_Call_Async("share", JsonConvert.SerializeObject(info, Formatting.Indented));}public override void ReportRoleInfo(MaxSDKRoleInfo info){MaxSDK_Call_Async("reportRoleInfo", JsonConvert.SerializeObject(info, Formatting.Indented));}public override void SwitchAccount(MaxSDKSwitchAccountInfo info){MaxSDK_Call_Async("switchAccount", JsonConvert.SerializeObject(info, Formatting.Indented));}public override void ReportEvent(MaxSDKReportEventInfo info){MaxSDK_Call_Async("reportEvent", JsonConvert.SerializeObject(info, Formatting.Indented));}public override void ExitGame(MaxSDKExitInfo info){MaxSDK_Call_Async("exitGame", JsonConvert.SerializeObject(info, Formatting.Indented));}public override void OpenActionExt(MaxSDKActionInfo info){MaxSDK_Call_Async("openActionExt", JsonConvert.SerializeObject(info, Formatting.Indented));}public override bool IsActionSupported(int type){bool isSupported = false;string resultStr = MaxSDK_Call_Sync("isActionSupported", type.ToString());bool.TryParse(resultStr, out isSupported);return isSupported;}/*** 扩展接口(同步),支持返回值*/public override string DispatchSync(MaxSDKDispatchInfo info){return MaxSDK_Call_Sync(info.apiName, JsonConvert.SerializeObject(info, Formatting.Indented));}/*** 扩展接口(异步),带统一回调方法 OnDispatchResult(MaxSDKDispatchBean bean)*/public override void DispatchASync(MaxSDKDispatchInfo info){MaxSDK_Call_Async(info.apiName, JsonConvert.SerializeObject(info, Formatting.Indented));}[DllImport("__Internal")]private static extern void MaxSDK_Call_Async(string method, string data);[DllImport("__Internal")]private static extern string MaxSDK_Call_Sync(string method, string data);}//#endif}
2.3.9、接口回调
上述介绍了接口调用Call的流程,在完整的接口调用中,CallBack接口回调的设计也是不可或缺的重要环节。
接口回调,首要需要注册回调。基于研发接口调用性的考虑,设计了一个对外的统一设置接口:
/*** 设置监听器*/public void SetListener(MaxSDKListener listener){supportBase?.SetListener(listener);}
其中MaxSDKListener实现了MonoBehaviour,方便将回调方法和UI脚本绑定相关的点击事件。MonoBehaviour 是一个基类,所有 Unity 脚本都派生自该类。
namespace maxsdk{public abstract class MaxSDKListener : MonoBehaviour{/* --------- 初始化回调 ----------- */public void CallInitSuccess(string json){MaxSDKInitBean initBean = JsonConvert.DeserializeObject<MaxSDKInitBean>(json);OnInitSuccess(initBean);}public void CallInitFail(string json){OnInitFail(JsonToErrorInfo(json));}public abstract void OnInitSuccess(MaxSDKInitBean initBean);public abstract void OnInitFail(MaxSDKFailBean bean);/* --------- 登录回调 ----------- */public void CallLoginSuccess(string json){MaxSDKLoginBean userInfo = JsonConvert.DeserializeObject<MaxSDKLoginBean>(json);OnLoginSuccess(userInfo);}public void CallLoginFail(string json){OnLoginFail(JsonToErrorInfo(json));}public abstract void OnLoginSuccess(MaxSDKLoginBean bean);public abstract void OnLoginFail(MaxSDKFailBean bean);//..................省略其他的pay、logout等类似回调接口..............}}
在上述回调接口中,实现了统一处理,包括接口名称规范统一和参数统一。
- 所有的从平台回调过来的接口名称,供平台内部跨平台使用,以Call_[interfaceName]_Success或 Call_[interfaceName]_Fail命名,而供研发实现的接口,统一定义为abstract,以OnXXX格式作为通用的回调名字命名,降低认知成本。
- 所有的Call_[interfaceName]的接口,参数都是 json格式的字符串,这样方便跨平台参数序列化的传递,后续即使平台侧接口参数更改,也无需调整回调通信的方式,处理更灵活。
2.3.10、demo调用调试
接口的call和callback都实现之后,就需要在demo中测试效果,首先在unity的编辑器中,将具体UI绑定点击事件:
//SQWYUnityEventHandler.cspublic void OnClickLogin(){#if UNITY_EDITOREditorUtility.DisplayDialog("提示", "点击了登录", "Yes", "No");#endif#if UNITY_ANDROID || UNITY_IOS || UNITY_STANDALONE_WINMaxSDKLoginInfo info = new MaxSDKLoginInfo();MaxSDK.GetInstance().Login(info);#endif}
点击demo中的按钮,例如【登录】,将弹出提示框。整体效果如下:
这里要注意,当处于播放模式时,修改不会最终生效。
最终,我们从demo的按钮点击开始,调用接口,处理回调,完成了引擎层的接口调用流程实现。
2.4、平台层架构设计
上述完成了引擎层的接口调用,有个疑问没有解除,就是具体的SDK功能是如何实现的?实现的结果是如何回调给引擎的?这些答案都要在平台层实现里面找到。
平台层的实现,以安卓为例,在导出了安卓相关工程后,如何在一个工程里面让37网游、37手游、37Games不同的业务线调用各自的业务具体实现呢?还是从需求的拆解和分析出发,得到平台层的分层架构:
接口层统一声明,各接口名、接口数和作用对齐
桥阶层统一规范,包括回调方法名,回调参数
不同平台实现具体实现逻辑
不同部门实现不同的demo和业务逻辑
2.5、平台层架构实现
2.5.1、模块设计
完成平台层分层架构之后,首先进行平台层模块设计,以业务独立、功能解耦为原则,分为三部门共用的base模块和各业务线实现模块
base:
- sdkBridge - 通用接口模块
- unityLibrary - unity工程导出产物
- unitySdkLibrary - unity接口桥接实现层,包括Android接口回调通知到unity实现
sq_wy为例:
- sdkUnityDemo - unitydemo的入口类
- sdkImpl - 业务线对接现有业务SDK的具体实现类。
2.5.2、平台接口设计
模块设计完成,进一步需要设计先设计业务接口。和引擎层业务接口类似,在平台层将接口分为业务相关接口和生命周期接口。
业务相关接口,方法名和unity调用方法名必须保持一致。
回调接口调用方法名和unity层实现的方法名也必须保持一致:
2.5.3、平台回调unity
在平台层实现具体功能后,需要将功能的结果回调给unity。Android中调用unity方法,最终是调用unityLibrary中的 UnityPlayer.UnitySendMessage() 方法来实现:
2.5.4、平台接口实现
在SQWySDKImpl具体实现类中,功能接口和回调串联起来。
以init为例,init()接口是核心实现的业务接口,在完成初始化后,调用callback的onInitSuccess,在onInitSuccess完成对unity接口的调用,最终完成了整个接口调用流程。
最终,引擎层触发接口call调用,平台层完成接口功能实现,并callback回调给引擎层,完整流程如下:
MaxSDK在Unity中的使用
开发完成,一切就绪后,如何提供给研发使用呢?这涉及导出平台工程、导出unitypackage包、输出对外SDK文档给到研发对接。
1、导出目标平台工程,以安卓为例:
注意:每次导出unity的安卓工程,都需要更新 `assets` 和 `jniLibs` 目录的资源
2、导出 unitypackage文件,提供给研发导入使用
unitypackage文件是Unity引擎中用于打包和共享项目资源和代码的文件格式。通过使用unitypackage文件,可以方便地共享和传递项目,并快速导入和使用所需的资源和代码。
导出方式很简单,选择菜单栏的 "Assets",然后选择 "Export Package",按需选择文件即可。在MaxSDK项目,选择需要导出的接口.cs文件,选中【包括依赖项】,就可以得到一个可供导入的 .unitypackage 文件。
3、研发导入unitypackage文件
在Unity编辑器中,选择菜单栏的 "Assets",然后选择"Import Package",再选择"Custom Package",选择 .unitypackage 文件即可完成导入。
MaxSDK的扩展探索
前文引擎层以unity为例,平台层以安卓层为例,讲解了MaxSDK架构思路和实现过程,介绍了接口的完整调用过程。那MaxSDK是否支持更多引擎(例如cocos、UE),支持更多平台(例如鸿蒙、小游戏)呢?
答案是肯定的,MaxSDK从架构上支持扩展:
借助Unity团结版,可以实现鸿蒙、微信小游戏的快速适配。
基于可扩展的架构,可灵活实现引擎层的适配。
总结
本文介绍了MaxSDK的背景和收益,讲解了MaxSDK的架构设计思路以及实现,包括unity引擎层使用c#实现MaxSDK的思考,以及平台层Android工程兼容多业务线的实现。最后介绍如何在unity中导出/更新安卓工程(其他平台类似),一起探索MaxSDK扩展鸿蒙、其他引擎的可行性。最终实现研发对接SDK降本增效的目的。