前言
大约一个月前收到领导新布置的任务,要用Unity直接接入发行方ios sdk。当时我一下子就懵了,ios的Object-c没接触过啊,Unity和ios该怎么交互呀,完全什么都不懂。接到消息的那一刻整个人状态都不好了,查阅了很多资料完全没有头绪也看不进去任何有关OC的基础知识。还好有我们部门大杨哥耐心的讲了一遍怎么弄。经过大杨锅的讲解还有Google理解出来的一些知识,现已完整的对接完好几个IOS 的SDK的接入工作。其实ios sdk接入并没有你想的那么难,接下来我会举例说明,跟大家分享一下我学到的东西,让新手同学不要跟我一样上来就懵。
文章目录
Unity与IOS交互层C#代码编写
Unity接 iOS SDK你需要了解的Objective-C基础知识
Unity 与IOS交互工作原理
ios 小7手游sdk接入演示
编译器版本介绍
Unity :Unity 19.4.2f1 Personal
Xcode : Xcode 12.2
Unity与IOS交互层C#代码编写
Unity 广泛的支持原生插件,即用 C、C++、Objective-C 等编写的原生代码库。插件允许游戏代码(用 C# 编写)调用这些库中的函数。
为了使用原生插件,首先需要使用基于 C 的语言编写函数来访问所需的功能并将它们编译到库中。在 Unity 中,还需要创建一个 C# 脚本来调用本机库中的函数。
原生插件应提供一个简单的 C 接口供 C# 脚本随后向其他用户脚本公开。当某些低级渲染事件发生时(例如,创建图形设备时),Unity 也可以调用原生插件导出的函数。现在编写一下C#的脚本,此代码来源于实际项目部分截取。
using System.Runtime.InteropServices;
namespace GameChannel
{
public class ChannelManager : Singleton<ChannelManager>
{
#if UNITY_IOS //c#中宏的概念 ,意思是当前平台是iOS
[DllImport("__Internal")]
private static extern void SDk_Login(); //登录
[DllImport("__Internal")]
private static extern void SDk_Logout();//注销
[DllImport("__Internal")]
private static extern void SDk_SwitchAccount();//切换账号(可选参数)
[DllImport("__Internal")]
private static extern void SDk_Pay(string payData);//支付
[DllImport("__Internal")]
private static extern void SDk_Data(string thisdata); //向渠道发送游戏数据
//当前平台是安卓,交互层插件对外调用名称是通用的,但是方式上是略有不同用宏的概念做区分。
#elif UNITY_ANDROID
}
}
对上述代码做下说明:在 iOS 上,插件以静态方式链接到可执行文件中,因此我们必须使用“ __Internal” 作为库名。其他的平台会通过动态的方式加载插件 [DllImport ("PluginName")]名称。C#调用其他模块的接口都是通过DllImport的方式来实现的。例如c#定义了void SDk_Login()方法,在ios的object-c中 也一定有void SDk_Login()方法。
Unity接 iOS SDK你需要了解的Objective-C基础知识
Unity项目开发,iOS平台要接SDK的话,就需要写Objective-C原生代码的,对于没使用过Objective-C的小伙伴不要慌。我一说你就懂了。
.h : 头文件作为一种包含功能函数、数据接口声明的载体文件,主要用于保存程序的声明,而定义文件用于保存程序的实现
.m : 它是对.h头文件中方法的实现,外部不能访问
.mm : 源代码文件。和.m文件类似,唯一的不同点就是,除了可以包含Objective-C和C代码以外,还可以包含C++代码。
include与#import
当你需要在源代码中包含头文件的时候,你可以使用#include编译选项也可以使用#import ,但是OC官方更推荐的方法是:#import。这个跟java的import 导包思想上非常相似。
""和<>的区别
例如 #import "UnityIos.h" 和 #import <Foundation/Foundation.h>两种。使用""引入的是本地工程的文件,而使用<>引入的是系统库的文件。
@interface与@implementation
@interface是类为对象提供特性描述(接口),@implementation是对@interface定义接口具体的实现。这两个跟java中的接口的定义与实现上思想上是一致的。
方法前的+ 和 -
加号(+)的方法为类方法,这类方法是可以直接用类名来调用的。
减号(-)的方法为实例方法,必须使用这个类的实例才可以调用它。
打印日志
NSLog打印日志。如 NSlog(@"")
基本数据类型
NSString : 字符串
CGfloat : 浮点值的基本类型
NSInteger : 整型
BOOL : 布尔型
json使用
Unity和OC要传递数据,常用的就是json格式。但是OC还和java的不一样。
//json字符串转化成字典
-(NSDictionary*)getJsonDic:(NSString*)jsonString{
NSData* jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
return [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableLeaves error:nil];
}
//字典转化成json字符串
-(NSString*)arrayToJson:(NSMutableDictionary *)dic{
NSError *parseError = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:&parseError];
return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}
这些知识仅做作为基础了解,其他详细的用法大家可以找下Google和度娘
Unity 与IOS交互工作原理
熟悉工作原理前大家不妨看一下之前写的博客:Unity 导出Xcode 项目的结构 作为一个了解,每个Unity ios Xcode 项目都会有如下结构:
UnityFramework 库文件部分,其中包含源、插件和相关框架。它还生成 UnityFramework.framework 文件。
Unity-iPhone 主启动器部分,其中包含应用程序表示数据并会运行该库。Unity-iPhone 目标对 UnityFramework 目标具有单一依赖关系。
要将 Unity 集成到另一个 Xcode 项目中,必须将两个 Xcode 项目(原生项目和 Unity 生成的项目)合并到一个 Xcode 工作空间中,并将 UnityFramework.framework 文件添加到原生 Xcode 项目的应用程序 (Application) 目标的嵌入式二进制文件 (Embedded Binaries) 中。完成此操作后,可以使用 UnityFramework 类来控制 Unity 运行时。Unity直接导出Xcode工程目录结构如下图:
要想了解原理就要找到程序的入口,熟悉OC或者C的朋友一定知道main方法,这是整个程序的入口。我们先看下MainApp/main.mm,这个文件做了什么呢。
从代码中大概读懂的意思将/Frameworks/UnityFramework.framework库文件加载到应用程序中。然后看下UnityFramework/UnityFramework.h,通过UnityFramework Objective-C 类(该类是 UnityFramework.framework 的主体类)的实例来控制 Unity 运行时:其中的属性方法我罗列一下。
UnityFramework类
+(UnityFramework*)getInstance :单例类方法,可将实例返回到 UnityFramework。
-(UnityAppController*)appController :返回 UIApplicationDelegate 的 UnityAppController 子类。这是原生端的根 Unity 类,可以访问应用程序的视图相关对象,例如 UIView、UIViewControllers、CADisplayLink 或 DisplayConnection。
-(void)setDataBundleId:(const char*)bundleId:设置捆绑包,Unity 运行时应在其中查找 Data 文件夹。应在调用 runUIApplicationMainWithArgc 或 runEmbeddedWithArgc 之前调用此方法。
-(void)runUIApplicationMainWithArgc:(int)argc argv:(char*[])argv:从没有其他视图的主要方法中运行 Unity 的默认方式。
-(void)runEmbeddedWithArgc:(int)argc argv:(char[])argv appLaunchOpts:(NSDictionary)appLaunchOpts:存在其他视图时,如果需要运行 Unity,需要调用此方法。
-(void)unloadApplication :调用此方法可卸载 Unity,并在卸载完成后接收对 UnityFrameworkListener 的回调。Unity 将释放占用的大部分内存,但不会全部释放。
-(void)registerFrameworkListener:(id<UnityFrameworkListener>)obj :注册监听器对象,用于接收 UnityFramework 生命周期相关事件的回调。
-(void)unregisterFrameworkListener:(id<UnityFrameworkListener>)obj:取消注册监听器对象。
-(void)showUnityWindow:在显示非 Unity 视图时调用此方法,也会显示已经在运行的 Unity 视图。
- -(void)pause:(bool)pause:暂停 Unity
- -(void)setExecuteHeader:(const MachHeader*)header:必须在运行 Unity 之前调用此命令,CrashReporter 才能正常工作。
-(void)sendMessageToGOWithName:(const char)goName functionName:(const char)name message:(const char*)msg:此方法是 UnitySendMessage 的代理。它通过名称查找游戏对象,并使用单字符串消息参数来调用 functionName。
(void)quitApplication:(int)exitCode:调用此方法可完全卸载 Unity,并在 Unity 退出后接收对 UnityFrameworkListener 的回调。Unity 将释放所有内存。
注意:进行此调用后,将无法在同一进程中再次运行 Unity。可在 AppController 上设置 quitHandler 以覆盖默认进程终止
然后再看下Classes/main.mm,这个文件做了什么,根据代码得知(UIApplicaitonMain方法),程序需要创建UnityAppController对象,也就是说UnityAppController.mm才是真正的程序入口。
- 到UnityAppController.mm里先调用- (BOOL)application:(UIApplication)application didFinishLaunchingWithOptions:(NSDictionary)launchOptions生命周期方法,进行Unity界面初始化
- 然后则调用- (void)applicationDidBecomeActive:(UIApplication*)application方法,方法中设置了UnityPause(0);表示Unity为启动状态,在方法最后,执行[self performSelector:@selector(startUnity:) withObject:application afterDelay:0];.
- 最后调用到-(void)startUnity:(UIApplication*)application方法,展示Unity游戏界面,完成了原生OC和Unity的交互。
ios 小7手游sdk接入演示
正常情况下要接入小7苹果sdk,公司商务或者运营会提供相应的参数和接入文档。本文演示不提供文档和参数。
进入正题把小7 ios对应的库文件加入进来,首先工程Libraries 创建一个文件夹(例如SDK文件夹),把小7依赖所有库放到创建的SDK文件夹下,然后Libraries右键add files to Unity-iPhone ...把文件添加进来(下图是添加后的图),xcode 会自动把小7的文件添加到对应的库和引用文件上。
按小7的文档要求配置好info.plist文件然后需要设置的属性也都弄好,准备工作就完事,然后进行下一步。接入sdk其实可以在UnityAppController.mm文件中进行的。但是为了清晰,创建一个UnityIos.m(UnityIos.h可忽略)外部引用放在Libraries/SDK文件夹下。
有同学会奇怪我在Unity c#层定义好了例如登陆的方法,也没看到在OC中调用啊?怎么拉起登陆啊。年轻人勿要着急继续看。在UnityIos.m中我们定义一个和c#层SDk_Login()名一样的方法体。请看如下代码:(这一部分代码是真实项目中部分截取,其中包含了小7 sdk 完整的登陆 支付 切换账号等功能
#import "UnityIos.h"
#import <Foundation/Foundation.h>
#import <AdSupport/AdSupport.h>
#import <SMSDK/SMSDK.h>
#import "UnityAppController.h"
#import "UnityInterface.h"
@implementation UnityIos
//调用sdk登陆
void SDk_Login(){
NSLog(@"SDk_Login");
[SMSDK smLogin];
}
//调用sdk切换账号功能
void SDk_Logout(){
NSLog(@"SDk_Logout");
[SMSDK smLogout];
}
//这个方法只是为了兼容sdk
void SDk_SwitchAccount(){
NSLog(@"SDk_SwitchAccount");
}
//调用sdk支付,游戏传入sdk需要的参数数据(游戏客户端协定好字段要统一)
void SDk_pay(void *payData){
NSLog(@"SDk_pay");
//直接传json,oc无法识别所以要进行一个转化
NSString *idList = [NSString stringWithUTF8String:payData];
NSData *jsonData = [idList dataUsingEncoding:NSUTF8StringEncoding];
NSError *err;
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:jsonData
options:NSJSONReadingMutableContainers error:&err];
if(err) {
NSLog(@"json解析失败:%@",err);
return;
}
NSLog(@"dic解析:%@",dic);
NSString *_price = [NSString stringWithFormat:@"%@",[dic valueForKey:@"price"]];
NSString *_p_level = [NSString stringWithFormat:@"%@",[dic valueForKey:@"playerlevel"]];
NSString *_game_sign= [NSString stringWithFormat:@"%@",[dic valueForKey:@"game_sign"]];
NSString *_subject= [NSString stringWithFormat:@"%@",[dic valueForKey:@"subject"]];
NSString *_game_area= [NSString stringWithFormat:@"%@",[dic valueForKey:@"area"]];
NSString *_game_role_id= [NSString stringWithFormat:@"%@",[dic valueForKey:@"roleId"]];
NSString *_game_role_name= [NSString stringWithFormat:@"%@",[dic valueForKey:@"roleName"]];
NSString *_game_guid= [NSString stringWithFormat:@"%@",[dic valueForKey:@"guid"]];
SMPayInfo *payInfo = [[SMPayInfo alloc] init];
payInfo.game_orderid =[NSString stringWithFormat:@"%@",[dic valueForKey:@"orderid"]]; //游戏订单号 60个字符
payInfo.game_sign =_game_sign; //服务器返回的签名�������������
payInfo.game_price =_price; //价格单位:元
payInfo.subject =_subject; //道具简介
payInfo.game_area =_game_area; //角色所在区服
payInfo.game_level =_p_level; //角色等级
payInfo.game_role_id =_game_role_id; //角色ID
payInfo.game_role_name =_game_role_name; //角色名称
payInfo.notify_id = @"-1"; //回调通知ID 默认可以在后台填写
payInfo.extends_info_data = @""; //自定义扩展数据
payInfo.game_guid=_game_guid; //游戏登陆后服务器通过token解析拿到的guid
//調用支付接口
[SMSDK smPayWithNewPayInfo:payInfo];
}
// 把格式化的JSON格式的字符串转换成字典
// @param jsonString JSON格式的字符串
// @return 返回字典
- (NSDictionary *)dictionaryWithJsonString:(NSString *)jsonString {
if (jsonString == nil) {
return nil;
}
NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
NSError *err;
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:jsonData
options:NSJSONReadingMutableContainers
error:&err];
if(err) {
NSLog(@"json解析失败:%@",err);
return nil;
}
return dic;
}
@end
上面的代码看完了。还会有点小困惑,既然在Libraries/SDK/UnityIos.m文件下,OC是如何找到登陆方法的呢?UnityIos.m定义了SDk_Login(),这个方法在Classes/Native中能找到解释,我给大家看一个图啊.
这个图就是在Unity c#层定义好的同名方法,编译成了c++代码放到了Xcode工程路径下,成了沟通OC和c#的桥梁。游戏用户点击登陆按钮即可调用OC的登陆方法完成登陆的客户端流程。
至于回调为什么放在UnityAppController.mm里,有两个点,其一小7 sdk设计问题,要做个全局回调我不知道怎么弄,其二是游戏把初始化放在生命周期哪里处理的,不这样写我也没什么好办法,毕竟我不是一个真正的ios开发者。正常的接sdk方法和回调都可以写在UnityIos.m文件里面的。小7比较特殊啊所以这样写。到这里接入结束喽,回调和相关代码放在下面了。
#import <SMSDK/SMSDK.h>
#define SMSDKAppKey @"小7后台申请的appKey"
@implementation UnityAppController //展示部分核心需要部分
- (BOOL)application:(UIApplication*)app openURL:(NSURL*)url options:(NSDictionary<NSString*, id>*)options
{
id sourceApplication = options[UIApplicationOpenURLOptionsSourceApplicationKey], annotation = options[UIApplicationOpenURLOptionsAnnotationKey];
NSMutableDictionary<NSString*, id>* notifData = [NSMutableDictionary dictionaryWithCapacity: 3];
if (url) notifData[@"url"] = url;
if (sourceApplication) notifData[@"sourceApplication"] = sourceApplication;
if (annotation) notifData[@"annotation"] = annotation;
AppController_SendNotificationWithArg(kUnityOnOpenURL, notifData);
return [SMSDK handleApplication:app openURL:url
sourceApplication:[options valueForKey:@"UIApplicationOpenURLOptionsSourceApplicationKey"]
annotation:[options valueForKey:@"UIApplicationOpenURLOptionsAnnotationKey"]];
}
//生命周期启动时调用sdk初始化方法(ios的生命周期跟安卓概念差不多)
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
//省略了部分代码
//设置全局回调
//初始化�
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(SMSDKInitCallback:) name:SMSDKInitDidFinishNotification object:nil];
//登陆
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(SMSDKLoginCallback:) name:SMSDKLoginNotification object:nil];
//注销
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(SMSDKLogoutCallback:) name:SMSDKLogoutNotification object:nil];
//支付
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(SMSDKPayResultCallback:) name:SMSDKPayResultNotification object:nil];
//使用appKey初始化SDK
[SMSDK smInitWithAppKey:SMSDKAppKey];
return YES;
}
//初始化回调
- (void)SMSDKInitCallback:(NSNotification *)notify {
if (notify.object == kSMSDKSuccessResult) {
NSLog(@"初始化成功");
} else if (notify.object == kSMSDKFailedResult) {
NSLog(@"初始化失敗");
[SMSDK smInitWithAppKey:SMSDKAppKey]; //初始化失敗可以重新初始化
}
}
//登陆回调
- (void)SMSDKLoginCallback:(NSNotification *)notify {
NSLog(@"SMSDKLoginCallback");
if (notify.object == kSMSDKSuccessResult) {
NSLog(@"login callback success");
NSString *idfa = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];
NSString *deviceUUID = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
NSString *deviceModel = [[UIDevice currentDevice] model];
NSString *token = notify.userInfo[kSMSDKLoginTokenKey];
NSLog(@"解析token========:%@",token);
//这一步是和服务器协定需要的参数,这里不做真实展示每个游戏逻辑都不一样
NSDictionary *resultDict=@{@"":@1,@"":@"",@"":"",@"token":token,@"deviceUdid":deviceUUID,@"deviceId":deviceModel,@"idFa":idfa
};
NSData *data = [NSJSONSerialization dataWithJSONObject:resultDict options:NSJSONWritingPrettyPrinted error:nil];
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"json解析:%@",string);
//向 Unity发送数据
// 参数1绑定的场景 参数2 为对象上的脚本的一个成员方法名称(脚本名称不限制)
//参数3传递的json数据。这个跟安卓的一样。
UnitySendMessage("GameLaunch","OnSdkLoginSuc",[string cStringUsingEncoding:NSASCIIStringEncoding]);
} else {
NSLog(@"login callback fail");
}
}
//切换账号回调
- (void)SMSDKLogoutCallback:(NSNotification *)notify {
NSLog(@"LogoutCallback");
}
//支付结果回调
- (void)SMSDKPayResultCallback:(NSNotification *)notify {
//支付結果
if (notify.object == kSMSDKSuccessResult) {
//支付成功,刷新用戶數據
NSLog(@"支付成功");
} else if (notify.object == kSMSDKUserCancelResult) {
NSLog(@"支付取消");
} else if (notify.object == kSMSDKFailedResult) {
//支付錯誤,刷新用戶數據,保障不漏單
NSString *errMsg = notify.userInfo[kSMSDKErrorShowKey];
errMsg = errMsg && [errMsg isEqualToString:@""] ? errMsg : @"支付失敗";
NSLog(@"支付错误");
}
}
为了方便只展示结果,展示初始化成功,意味着 sdk初始化接入是没问题的
感悟
应标题那句话接入ios SDK没有你想的那么难,这不是噱头这是我真实的感受。我是一个搞安卓SDK的程序员,ios里面的很多思想都是和安卓Java 相通的文中我也做过解释。只要你会用安卓接sdk,那么ios也不是那么难。大家要是有兴趣可以看下Android sdk接入Unity 与 Android交互通信 之OPPO篇。
刚开始弄的时候,我承认我非常无助,找了oc语法大全。看了一会就看不下了,即使看了一会也就忘了。等真正去操作的时候(实践才是正道,光看知识点一会就忘),发现真没那么难。思想上跟安卓接sdk一样的,就是语法略微不同。稍微查一下就知道怎么搞了。如果文章对你有帮助留下一个赞呗,你的支持是我继续写下去的动力。
收尾
写博文不易,希望大家多多支持,如有不对大家多多指正。写出来就是记录、学习和成长的过程。