游戏SDK(三)架构设计之代码实现1
前言
上一篇介绍了游戏SDK架构设计的思路,大体的项目结构如下:
- sdk-demo // sdk 测试 demo
- sdk-api // sdk 接口模块
- sdk-manager // sdk 业务分发管理
- sdk-channel // 登录支付渠道
- sdk-channel-huawei // 具体的渠道sdk ,这里仅做示例
- sdk-channel-xiaomi
.... // 其他
- sdk-channel-googleplay // 公司自行开发的Google play渠道sdk 也放在这一层
- sdk-channel-guoneiguanwang // 公司自行开发的国内官网渠道sdk 也放在这一层
- sdk-data // sdk数据上报
- sdk-data-appsflyer // AF 数据上报 ,这里仅做示例
....// 其他
- sdk-plugin // 游戏sdk插件
- sdk-plugin-yuyin // 语音插件,这里仅做示例
... // 其他
- sdk-core // 游戏SDK公共业务
- sdk-common // 基础库
- buildSrc // 一些打包编译脚本
- doc // 文档
... // 其他分类看项目需要
渠道/数据上报平台/插件管理
配置文件:(以下仅作示例)
AppId=1001
#渠道名称
ChannelName=googleplay
#需要上传的数据平台
DataChannel=appsflyer,facebook,···
#需要增加的插件
PluginName=twitter,···
以上配置文件是一个 .properties 的文件,定义了当前的应用(AppId)等参数,指定当前需要运行什么渠道(ChannelName),需要的数据平台(DataChannel),需要增加的插件(PluginName),等等还会有其他参数,这里只是列出示例,格式可以根据项目需要自定义。
渠道管理模块根据配置文件制定的渠道名加载对应的渠道实现以及数据平台模块、插件模块。
P.S: 以上配置文件应由游戏SDK后台配置参数,并生成配置文件,配合打包工具打包到对应渠道的.apk中,测试时可以放置一份在 demo 模块中。
代码实现
-
API 接口设计,定义抽象类,定义对外接口。区分抽象接口和非抽象接口,抽象接口是子类必须实现的,比如渠道的登录、支付,一般渠道都有这些功能。除业务功能接口外,还有Application 和 Activity 的生命周期接口。
// 接口可以分类定义,便于阅读,比如 IApplicationLifeCycle 是 applicaiton 的生命周期接口 public abstract class AbstractChannel implements IApplicationLifeCycle, IActivityLifeCycle, IDataMonitor, IActivity, IShare, ISocial, IChat, IPush { /****************************基础接口*******************/ /** * 设置UserCallBack,初始化及用户登录结果会通过此回调对象通知给游戏 * * @param userCallBack */ public abstract void setUserCallBack(UserCallBack userCallBack); /** * 登录接口 * * @param activity - 当前活跃的Activity * @param customParams - 扩展参数,JSON格式,默认为null */ public abstract void login(Activity activity, String customParams); // ...其他接口 } // IActivityLifeCycle 的接口 public interface IActivityLifeCycle { /** * activity 启动 * * @param activity -游戏当前活动的页面 * @param savedInstanceState -存储的bundle */ void onCreate(Activity activity, Bundle savedInstanceState); /** * activity 销毁时调用到 * * @param activity -游戏当前活动的页面 */ void onDestroy(Activity activity); /** * 活动页面 显示时调用到 * * @param activity -游戏当前活动的页面 */ void onResume(Activity activity); //··· 其他接口类似 } // ····其他接口类似,再次不一一列举
-
由于预定义对外的接口定义好了之后一般不会做更改,但有时候不同的渠道确实有不同的功能需要实现,所以预定义一个自定义接口及通用回调,根据传入的方法名称去调用。
/** * 提供给游戏的动态接口 * * @param methodName 方法名称 * @param methodParam 方法参数,可为null * @param callback 通用回调 * @param customInfo 透传字段,回调中回传给游戏 * @return 返回, 可为Null */ public abstract Object callMethod(String methodName, Object methodParam, GenericCallBack callback, String customInfo);
-
游戏研发只接入SDK的 api 层,不需要关心及引用其他的模块,所以这里需要一个反射获取渠道管理类,再由渠道管理类具体实现应该由哪个渠道实现CP调用的接口。
public class ProjectManager { // 渠道管理类类名 private static String CHANNEL_MANAGER_CLASS_NAME = "com.xxx.client.xxx.ChannelManager"; /********************* 同步锁双重检测机制实现单例模式(懒加载)********************/ private volatile static ProjectManager projectManager; public static ProjectManager init() { if (projectManager == null) { synchronized (ProjectManager.class) { if (projectManager == null) { projectManager = new ProjectManager(); } } } return projectManager; } public static ProjectManager getInstance() { return projectManager; } /********************* 同步锁双重检测机制实现单例模式 ********************/ private ProjectManager() { } /** * 加载渠道管理类 * * @return * @throws RuntimeException */ public synchronized AbstractChannel loadChannelManager() throws RuntimeException { AbstractChannel p = null; Class<?> glass = null; if (TextUtils.isEmpty(CHANNEL_MANAGER_CLASS_NAME)) { LogUtil.e(TAG, "loadChannelManager: the class name is empty!"); return p; } try { glass = Class.forName(CHANNEL_MANAGER_CLASS_NAME); } catch (ClassNotFoundException e) { LogUtil.i(TAG, "loadChannelManager: " + "do not find " + CHANNEL_MANAGER_CLASS_NAME); } try { //尝试调用getInstance Method m = glass.getDeclaredMethod("getInstance"); m.setAccessible(true); p = (AbstractChannel) m.invoke(null, new Object[]{}); } catch (NoSuchMethodException e1) { //调用getInstance失败后,尝试new其对象 try { p = (AbstractChannel) glass.newInstance(); } catch (Exception exception) { LogUtil.w(TAG, "loadChannelManager", "glass.newInstance(): " + "do not find " + CHANNEL_MANAGER_CLASS_NAME); } } catch (Exception exception) { LogUtil.e(TAG, "loadChannelManager", "glass.getInstance(): " + "do not find " + CHANNEL_MANAGER_CLASS_NAME, exception.toString()); } if (p == null) { LogUtil.w(TAG, CHANNEL_MANAGER_CLASS_NAME + " is empty."); } return p; } }
关于Java 反射的内容可以看这篇文章:Java反射机制-框架设计的灵魂
-
定义渠道的父类、数据平台的父类、插件父类(如果插件功能差别很大,没有共同点,可以单独使用反射获取),可以在父类里处理渠道接口共同的地方,比如自定义方法的实现。
以渠道的父类为例:
public class SdkChannel extends AbstractChannel { @Override public void setUserCallBack(UserCallBack userCallBack) { } @Override public void login(Activity activity, String customParams) { } /**************************** Basic interface ******/ @Override public void logout(final Activity activity, final String customParams) { if (mUserCallBack != null) { mUserCallBack.onLogoutFinish(ErrorCode.SUCCESS, ""); } } @Override public void pay(Activity activity, PayInfo payInfo, PayCallBack payCallBack) { } // ··· 其他接口类似 }
-
在Applicaiton 启动的时候获取配置文件的 ChannelName 的值,判断需要加载的渠道
// 读取配置文件 PropertiesUtil.loadFromAssetsConfig(context); // 给渠道定义统一规则类名 public static final String CHANNEL_CLASS_PATTERN = "com.xxx.client.{渠道名}.inner.ChannelImpl"; // 读取配置文件中的 ChannelName ,拼接成最后的类名 // 加载类 public static <T> T loadClass(ClassLoader loader, String className, Class<T> claz) { if (loader == null) { return null; } try { Class<?> loaded = loader.loadClass(className); if (claz.isAssignableFrom(loaded)) { return (T) loaded.newInstance(); } } catch (ClassNotFoundException e) { LogUtil.i("can not find class " + className); } catch (Exception e) { LogUtil.e("can not create instance for class " + className, e); } return null; }
-
在渠道管理类统一调用接口
public class ChannelManager extends AbstractChannel { @Override public void login(Activity activity, String customParams) { // 尽量在外层catch 异常,避免因为游戏SDK崩溃 try { // 获取渠道类 SdkChannel loginChannel = getLoginChannel(); if (loginChannel != null) { // 调用渠道的登录接口 loginChannel.login(activity, customParams); } else { LogUtil.i(TAG, "can not find channel implement, please check if your main activity is inherited from SDKSDK activity or call SDKSDK application lifecyle interfaces(such as onCreate, onStart and etc.)"); } } catch (Exception e) { LogUtil.e(CATCH_UNEXPECT_EXCEPTION, e); } } } // 渠道实现类集成父类SdkChannel public class ChannelImpl extends SdkChannel { @Override public void login(Activity activity, String customParams) { // 渠道的登录实现 } }
-
项目回调设计:模块之间难免有互相调用,每个事件的事件流都有结果,这就需要回调机制。
// 以下的都是示例,其他的回调都是类似 // 在API层定义CP可调用的回调 public interface UserCallBack { /** * 登录成功,游戏需要根据authInfo到SG服务器进行登录验证 * * @param code * @param authInfo */ void onLoginSuccess(int code, String authInfo); /** * 登录失败, 建议游戏返回账号登录界面 * * @param code * @param msg * @param channelCode */ void onLoginFail(int code, String msg, String channelCode); } // 再在管理类里包装一层,统一处理最后的 用户信息 public class UserCallBackWrapper implements UserCallBack { @Override public void onLoginSuccess(int code, String authInfo) { try { // 在这里统一处理共同操作,比如上报登录成功数据 if (getDataMonitors() != null) { for (DataMonitor dataMonitor : getDataMonitors()) { dataMonitor.onLoginSuccess(code, authInfo); } } } catch (Exception e) { LogUtil.e(UNKOWN_EXCEPTION_MSG, e); } try { LogUtil.i("login success, begin to call game callback"); this.gameCallback.onLoginSuccess(code, authInfo); } catch (Exception e) { LogUtil.e(UNKOWN_EXCEPTION_MSG, e); } } }
-
其他就是具体的渠道实现。数据和插件类似。
以上,从CP调用到渠道的实现,再从渠道实现的接口回调给CP的全过程已经完成。其他在之后的篇幅介绍。