iOS 游戏渠道SDK 抽象工程封装(上)
一款手机游戏,要是想挣钱,接入渠道SDK是很重要滴。但是渠道SDK有那么多家,每一家的接口也不一样,那么是否需要每一家渠道SDK都来接入一次呢?游戏的研发同学,每次想到这边,都表示一个头,两个大。
那么为了给研发的同学减轻负担,让他们专心搞研发,给所有渠道SDK封装一个抽象工程,是很有必要的一件事情。这样,游戏接入一次抽象工程就OK了,到时候要接入渠道SDK,只需要把文件替换一下,省时又省力,岂不是美美哒。
什么是渠道SDK的抽象工程?
抽象工程,可以说是渠道SDK的驱壳。这个驱壳,可以装下各种各样的渠道SDK。
虽然渠道SDK种类繁多,但是细心一看,他们的接口也是大同小异的。大体上有这么几个:
- 初始化
- 用户登陆
- 用户退出
- 用户支付
- 用户中心
- 工具栏打开关闭
摸清了他们的套路,咱们也可以大大方方地出手了。
抽象工程的总入口
游戏与抽象工程的所有交互,都是由这个类来完成。我们把它命名为SDKAccount。
先来个SDKAccount.h的代码
//获取单例
+ (instancetype)sharedInstance;
//sdk用户初始化
- (void)doInit:(NSString*)gameVersion;
//sdk登陆
- (void)doLogin;
//sdk退出
- (void)doLogout;
//sdk切换用户
- (void)doSwitchAccount;
//sdk支付
- (void)doPay:(SDKPayInfo *)sdkPayInfo;
//调用暂停页面
- (void)doPause;
//设置工具栏 YES打开/NO关闭
- (void)doSetting:(BOOL)visible;
//打开用户中心
- (void)doUserCenter;
为了使我们的抽象工程更方便地使用,我们这边采用单例的模式,使用sharedInstance 来获取抽象工程的单例。
然后大家是不是以为,接下来就是在SDKAccount.m里头来实现渠道SDK的代码啦。no no no,这样会使我们抽象工程的业务代码,和渠道SDK的业务混杂在一起,使代码变得杂乱不堪,这是我所不能容忍的。我们把渠道SDK的代码,统统放在另外一个地方,这个后面再讲。
先来讲讲SDKAccount.m
- 获取单例,我们用最常见的gcd方式来创建。
+ (instancetype)sharedInstance {
static SDKAccount* instance = nil;
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
instance = [[SDKAccount alloc] init];
});
return instance;
}
- 初始化
- (void)doInit:(NSString*)gameVersion {
[[SDKContainer sharedInstance] doThirdInit:self gameVersion:gameVersion];
}
- 登陆
- (void)doLogin {
[[SDKContainer sharedInstance] doLogin];
}
- 退出
- (void)doLogout {
[[SDKContainer sharedInstance] doLogout];
}
- 支付
- (void)doPay:(SDKPayInfo *)sdkPayInfo {
SDKUser *sdkUser = [SDKUser sharedInstance];
if(sdkUser.uuid == nil || sdkUser.uuid.length == 0) {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAY object:self userInfo:[SDKResponseBase dict:SDK_RESP_NO_LOGIN]];
return;
}
if(sdkPayInfo == nil) {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAY object:self userInfo:[SDKResponseBase dict:SDK_RESP_PARAM_ERROR]];
return;
}
if([sdkPayInfo.amount intValue] <= 0 || [sdkPayInfo.roleId length] == 0 || [sdkPayInfo.serverId length] == 0 || [sdkPayInfo.productId length] == 0) {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAY object:self userInfo:[SDKResponseBase dict:SDK_RESP_PARAM_ERROR]];
return;
}
SDKPayReq *payReq = [[SDKPayReq alloc] init];
payReq.amount = sdkPayInfo.amount;
payReq.platformUserId = [[SDKUser sharedInstance] uid];
payReq.appOrderId = sdkPayInfo.orderId;
payReq.appProductId = sdkPayInfo.productId;
payReq.appProductName = sdkPayInfo.productName;
payReq.appUserId = sdkPayInfo.userId;
payReq.appUserName = sdkPayInfo.username;
payReq.appRoleId = sdkPayInfo.roleId;
payReq.appRoleName = sdkPayInfo.roleName;
payReq.appRoleLevel = sdkPayInfo.roleLevel;
payReq.appServerId = sdkPayInfo.serverId;
payReq.appServerName = sdkPayInfo.serverName;
payReq.channelId = [self getChannelId];
payReq.deviceId = [self getDeviceId];
payReq.appNotifyUri = sdkPayInfo.notifyUri;
payReq.appExt = sdkPayInfo.ext;
payReq.vipLevel = sdkPayInfo.vipLevel;
[payReq post:^(NSHTTPURLResponse *response, NSDictionary *data) {
SDKLog(@"订单信息---%@", data);
NSNumber *code = data[@"code"];
if([code intValue] == 0) {
NSDictionary *info = data[@"data"];
NSString *orderId = info[@"order_id"];
NSString *amountStr = [NSString stringWithFormat:@"%d", [[payReq amount] intValue]/100];//单位为元
NSString *productId = payReq.appProductId;
NSString *productName = payReq.appProductName;
NSString *roleId = payReq.appRoleId;
NSString *serverId = payReq.appServerId;
NSString *serverName = payReq.appServerName;
NSString *payDesc = [NSString stringWithFormat:@"Product_%@", payReq.appProductId];
NSDictionary *orderMsg = @{
@"orderId":CleanNil(orderId),
@"amountStr":CleanNil(amountStr),
@"productId":CleanNil(productId),
@"productName":CleanNil(productName),
@"roleId":CleanNil(roleId),
@"serverId":CleanNil(serverId),
@"serverName":CleanNil(serverName),
@"payDesc":CleanNil(payDesc)
};
[[SDKContainer sharedInstance] doPayWithOrder:orderMsg];
} else {
[self notifitionCreateOrderError];
}
} failure:^(NSHTTPURLResponse *response, NSDictionary *data, NSError *error) {
[self notifitionCreateOrderError];
}];
}
到这边,有些朋友会问,为什么doPay的代码会多出这么多?那是因为这些代码,是我们自己的业务逻辑。
在调用渠道SDK的支付之前,我们需要先把支付信息传到我们的服务器上面,生成一个订单号,这个订单号,会记录在数据库中,作为以后游戏用户的支付凭据。生成完以后,再回传回来。这时候我们才能用这个订单号,来调用渠道SDK的支付接口。
- 暂停页面
- (void)doPause {
[[SDKContainer sharedInstance] doPause];
}
有些渠道SDK(比如91助手),要求在按下home键回到主界面,再切换回游戏时,会弹出一个暂停页面,用来展示广告。这时就要在AppDelegate中的applicationWillEnterForeground方法中,调用doPause这个方法。
- 工具栏(悬浮球)开启关闭
- (void)doSetting:(BOOL)visible {
[[SDKContainer sharedInstance] doSetting:visible];
}
- 用户中心开启关闭
- (void)doUserCenter {
[[SDKContainer sharedInstance] doUserCenter];
}
- 切换账号
- (void)doSwitchAccount {
[[SDKContainer sharedInstance] doSwitchAccount];
}
切换账号实际上也就是先退出,再调用登陆窗口。
盛放渠道SDK代码的容器
前面讲到,为了不使我们的代码变得杂乱不堪,我们把业务逻辑和渠道SDK的代码分开来。创建一个新的类,用来盛放渠道SDK代码。这个类大家也猜到了,就叫做SDKContainer。
先上SDKContainer.h的代码。
@protocol SDKContainerDelegate <NSObject>
- (void)initFinish:(NSDictionary*)initMsg;
- (void)loginFinished:(NSDictionary*)loginMsg;
- (void)logoutFinished:(NSDictionary*)logoutMsg;
- (void)payFinished:(NSDictionary*)payMsg;
//登录成功
- (void)notifitionLoginSuccess:(SDKUser*)sdkUser;
//登录失败
- (void)notifitionLoginError;
//登录取消
- (void)notifitionLoginCancel;
//注销成功
- (void)notifitionLogoutSuccess;
//创建订单失败
- (void)notifitionCreateOrderError;
//充值用户未登录
- (void)notifitionPayNoLogin;
//充值成功
- (void)notifitionPaySuccess;
//充值失败
- (void)notifitionPayError;
//充值取消
- (void)notifitionPayCancel;
//充值发货中
- (void)notifitionPayShipping;
//充值网络异常
- (void)notifitionPayNetError;
@end
首先一上来是一个协议,有协议就有人遵守。没错,这个协议是为SDKAccount准备的。
先在SDKAccount.h的头部写上
@interface SDKAccount : NSObject<SDKContainerDelegate>
然后在SDKAccount.m中实现这些方法
- (void)initFinish:(NSDictionary*)initMsg {
[self doLogin];
}
- (void)loginFinished:(NSDictionary*)loginMsg {
// 登录成功,开发者可继续游戏逻辑
SDKLoginReq *loginReq = [[SDKLoginReq alloc] init];
loginReq.code = loginMsg[@"code"];
loginReq.uid = loginMsg[@"uid"];
loginReq.username = loginMsg[@"username"];
loginReq.nickname = loginMsg[@"nickname"];
[loginReq post:^(NSHTTPURLResponse *response, NSDictionary *data) {
//打印日志
SDKLog(@"登录信息---%@", data);
NSNumber *code = data[@"code"];
if([code intValue] == 0) {
NSDictionary *dataDic = data[@"data"];
SDKUser *sdkUser = [SDKUser sharedInstance];
sdkUser.uuid = dataDic[@"uuid"];
sdkUser.token = dataDic[@"check_token"];
sdkUser.platform = FYSDK_PLATFORM_NAME;
NSDictionary *user = dataDic[@"user"];
NSString *uid = user[@"id"];
NSString *username = user[@"name"];
NSString *nickname = user[@"nickname"];
if (loginReq.uid.length != 0) {
sdkUser.uid = loginReq.uid;
} else {
sdkUser.uid = uid;
}
if (loginReq.username.length != 0) {
sdkUser.username = loginReq.username;
} else {
sdkUser.username = username;
}
if (loginReq.nickname.length != 0) {
sdkUser.nickname = loginReq.nickname;
} else {
sdkUser.nickname = nickname;
}
self.sdkUser = sdkUser;
[self notifitionLoginSuccess:sdkUser];
} else {
[self notifitionLoginError];
}
} failure:^(NSHTTPURLResponse *response, NSDictionary *data, NSError *error) {
[self notifitionLoginError];
}];
}
- (void)logoutFinished:(NSDictionary*)logoutMsg {
[self notifitionLogoutSuccess];
}
- (void)payFinished:(NSDictionary*)payMsg {
}
//-------------------------各种通知-------------------------------
//登录成功
- (void)notifitionLoginSuccess:(SDKUser*)sdkUser{
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_LOGIN object:self userInfo:[SDKResponseBase dict:SDK_RESP_SUCCESS sdkUser:sdkUser]];
}
//登录失败
- (void)notifitionLoginError {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_LOGIN object:self userInfo:[SDKResponseBase dict:SDK_RESP_LOGIN_ERROR]];
}
//登录取消
- (void)notifitionLoginCancel {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_LOGIN object:self userInfo:[SDKResponseBase dict:SDK_RESP_CANCEL_ERROR]];
}
//创建订单失败
- (void)notifitionCreateOrderError {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAY object:self userInfo:[SDKResponseBase dict:SDK_RESP_CREATE_ORDER_ORDER]];
}
//注销成功
- (void)notifitionLogoutSuccess {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_LOGOUT object:self userInfo:[SDKResponseBase dict:SDK_RESP_SUCCESS]];
}
//注销失败
- (void)notifitionLogoutError {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_LOGOUT object:self userInfo:[SDKResponseBase dict:SDK_RESP_PARAM_ERROR]];
}
//充值用户未登录
- (void)notifitionPayNoLogin {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAY object:self userInfo:[SDKResponseBase dict:SDK_RESP_NO_LOGIN]];
}
//充值成功
- (void)notifitionPaySuccess {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAY object:self userInfo:[SDKResponseBase dict:SDK_RESP_SUCCESS]];
}
//充值失败
- (void)notifitionPayError {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAY object:self userInfo:[SDKResponseBase dict:SDK_RESP_PAY_ERROR]];
}
//充值取消
- (void)notifitionPayCancel {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAY object:self userInfo:[SDKResponseBase dict:SDK_RESP_CANCEL_ORDER]];
}
//充值发货中
- (void)notifitionPayShipping {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAY object:self userInfo:[SDKResponseBase dict:SDK_RESP_PAY_ING]];
}
//充值网络异常
- (void)notifitionPayNetError {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAY object:self userInfo:[SDKResponseBase dict:SDK_RESP_NET_ERROR]];
}
//暂停页面关闭通知
- (void)notifitionPuasePageClose {
[[NSNotificationCenter defaultCenter] postNotificationName:SDK_CALLBACK_PAUSE_PAGE_CLOSE object:self userInfo:nil];
}
在SDKAccount的 doInit中,将self传给SDKContainer。这样SDKContainer就可以回调SDKAccount啦。
上面的loginFinished方法,也是调用了我们的业务逻辑。在渠道SDK登陆完以后,需要将token发到我们的服务器中,然后再由我们的服务器,转发给渠道SDK服务器去验证登陆。
我们来继续SDKContainer的内容。
SDKContainer.h
@interface SDKContainer : NSObject<XXXSDKDeletage>
+ (instancetype)sharedInstance ;
- (void)doThirdInit:(id<SDKContainerDelegate>)delegate gameVersion:(NSString*)gameVersion;
- (void)doLogin;
- (void)doLogout;
- (void)doPayWithOrder:(NSDictionary*)orders;
- (void)doPause;
- (void)doSetting:(BOOL)visible;
- (void)doUserCenter;
- (void)doSwitchAccount;
@end
有没有很眼熟,和SDKAccount很像,是吧。
然后是SDKContainer.mm
#import "SDKContainer.h"
@interface SDKContainer()
@property (nonatomic) id<SDKContainerDelegate> delegate;
@end
@implementation SDKContainer
+ (instancetype)sharedInstance {
static SDKContainer* instance = nil;
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
instance = [[SDKContainer alloc] init];
});
return instance;
}
- (void)doThirdInit:(id<SDKContainerDelegate>)delegate gameVersion:(NSString*)gameVersion {
self.delegate = delegate;
//----------------打印平台版本号---------------
NSString *platformVersion = @"";//更新SDK必填
NSLog(@"---Platform Version---%@", platformVersion);
//---------------sdk初始化代码-----------------
//0横屏 1竖屏
if([SDK_CONFIG_ORIENTATION isEqual:@"0"]) {
//-----------sdk横屏设置-----------
} else {
//-----------sdk竖屏设置-----------
}
}
- (void)doLogin {
//-----------sdk登陆接口-----------
}
- (void)doLogout {
//-----------sdk退出接口-----------
}
- (void)doPayWithOrder:(NSDictionary*)orders {
NSString *orderId = orders[@"orderId"]; //订单
NSString *amountStr = orders[@"amountStr"]; //金额,单位为元
NSString *productName = orders[@"productName"]; //商品名
NSString *roleId = orders[@"roleId"]; //角色名
NSString *serverId = orders[@"serverId"]; //区服id
NSString *payDesc = orders[@"payDesc"]; //额外支付信息
//-----------sdk支付接口-----------
}
- (void)doPause {
//------------sdk暂停页面-------------
}
- (void)doSetting:(BOOL)visible {
if (visible) {
//-----------sdk打开工具栏-----------
} else {
//-----------sdk关闭工具栏-----------
}
}
- (void)doUserCenter {
//-----------sdk打开个人中心-----------
}
- (void)doSwitchAccount {
[self doLogout];
[self doLogin];
}
//---------------渠道sdk回调接口----------------
//-----------------------------------------
这边我们为渠道SDK预留了位置,将来要接入渠道SDK的时候,只需要将渠道SDK的代码,放到SDKContainer.mm中对应的位置就行了。是不是很方便呢?
细心的朋友发现,SDKContainer.mm多了一个m出来。这是因为有些渠道SDK是用C++和Obj-C混合写的。所以要求我们要将.m改为.mm。平时我们使用.mm也不妨碍我们的代码。
当渠道SDK需要回调游戏的时候该怎么办?只需要调用一下delegate中的方法,就可以了。
*(1)初始化结束调用以下代码
[self.delegate initFinish:nil];
*(2)登陆结束调用以下代码
NSString *code = ; //登陆时的token
NSString *uid = ; //游戏账号的唯一值
NSString *username = ; //游戏账号名
NSDictionary *loginMsg = @{
@"code":code,
@"uid":uid,
@"username":username
};
[self.delegate loginFinished:loginMsg];
*(3)登陆取消调用以下代码
[self.delegate notifitionLoginCancel];
*(4)登陆失败调用以下代码
[self.delegate notifitionLoginError];
*(5)支付成功调用以下代码
[self.delegate notifitionPaySuccess];
*(6)支付取消调用以下代码
[self.delegate notifitionPayCancel];
*(7)支付失败调用以下代码
[self.delegate notifitionPayError];
*(8)支付用户未登陆调用以下代码
[self.delegate notifitionPayNoLogin];
*(9)创建订单失败调用以下代码
[self.delegate notifitionCreateOrderError];
*(10)充值发货中调用以下代码
[self.delegate notifitionPayShipping];
*(11)充值网络异常调用以下代码
[self.delegate notifitionPayNetError];
*(12)退出成功调用以下代码
[self.delegate notifitionLogoutSuccess];
然后由SDKAccount统一去回调游戏。
这样是不是将渠道SDK的代码和我们的业务代码,完全地分离开来了呢?