Unity接入ios SDK(小7手游)没有你想的那么难

前言

大约一个月前收到领导新布置的任务,要用Unity直接接入发行方ios sdk。当时我一下子就懵了,ios的Object-c没接触过啊,Unity和ios该怎么交互呀,完全什么都不懂。接到消息的那一刻整个人状态都不好了,查阅了很多资料完全没有头绪也看不进去任何有关OC的基础知识。还好有我们部门大杨哥耐心的讲了一遍怎么弄。经过大杨锅的讲解还有Google理解出来的一些知识,现已完整的对接完好几个IOS 的SDK的接入工作。其实ios sdk接入并没有你想的那么难,接下来我会举例说明,跟大家分享一下我学到的东西,让新手同学不要跟我一样上来就懵。

妹子.jpg


文章目录

  • 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
    {
        #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 两种。使用""引入的是本地工程的文件,而使用<>引入的是系统库的文件。

@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工程目录结构如下图:

Xcode_Project.png

要想了解原理就要找到程序的入口,熟悉OC或者C的朋友一定知道main方法,这是整个程序的入口。我们先看下MainApp/main.mm,这个文件做了什么呢。

main.mm..png
UnityFramework.png

从代码中大概读懂的意思将/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)obj :注册监听器对象,用于接收 UnityFramework 生命周期相关事件的回调。

  • -(void)unregisterFrameworkListener:(id)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 以覆盖默认进程终止

main
main-1

然后再看下Classes/main.mm,这个文件做了什么,根据代码得知(UIApplicaitonMain方法),程序需要创建UnityAppController对象,也就是说UnityAppController.mm才是真正的程序入口。

  • 到UnityAppController.mm里先调用- (BOOL)application:(UIApplication)application didFinishLaunchingWithOptions:(NSDictionary)launchOptions生命周期方法,进行Unity界面初始化
image.png
  • 然后则调用- (void)applicationDidBecomeActive:(UIApplication*)application方法,方法中设置了UnityPause(0);表示Unity为启动状态,在方法最后,执行[self performSelector:@selector(startUnity:) withObject:application afterDelay:0];.
image.png
  • 最后调用到-(void)startUnity:(UIApplication*)application方法,展示Unity游戏界面,完成了原生OC和Unity的交互。
image.png


ios 小7手游sdk接入演示

正常情况下要接入小7苹果sdk,公司商务或者运营会提供相应的参数和接入文档。本文演示不提供文档和参数。

进入正题把小7 ios对应的库文件加入进来,首先工程Libraries 创建一个文件夹(例如SDK文件夹),把小7依赖所有库放到创建的SDK文件夹下,然后Libraries右键add files to Unity-iPhone ...把文件添加进来(下图是添加后的图),xcode 会自动把小7的文件添加到对应的库和引用文件上

addLib.png
addFile.png

按小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 
#import 
#import 
#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的登陆方法完成登陆的客户端流程。

image.png

至于回调为什么放在UnityAppController.mm里,有两个点,其一小7 sdk设计问题,要做个全局回调我不知道怎么弄,其二是游戏把初始化放在生命周期哪里处理的,不这样写我也没什么好办法,毕竟我不是一个真正的ios开发者。正常的接sdk方法和回调都可以写在UnityIos.m文件里面的。小7比较特殊啊所以这样写。到这里接入结束喽,回调和相关代码放在下面了。

#import 

#define SMSDKAppKey @"小7后台申请的appKey"

@implementation UnityAppController    //展示部分核心需要部分

- (BOOL)application:(UIApplication*)app openURL:(NSURL*)url options:(NSDictionary*)options
{
    id sourceApplication = options[UIApplicationOpenURLOptionsSourceApplicationKey], annotation = options[UIApplicationOpenURLOptionsAnnotationKey];

    NSMutableDictionary* 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初始化接入是没问题的

FD23B039AA570EAADC2BA75A85FFBB56.png

感悟

应标题那句话接入ios SDK没有你想的那么难,这不是噱头这是我真实的感受。我是一个搞安卓SDK的程序员,ios里面的很多思想都是和安卓Java 相通的文中我也做过解释。只要你会用安卓接sdk,那么ios也不是那么难。大家要是有兴趣可以看下Android sdk接入Unity 与 Android交互通信 之OPPO篇。

刚开始弄的时候,我承认我非常无助,找了oc语法大全。看了一会就看不下了,即使看了一会也就忘了。等真正去操作的时候(实践才是正道,光看知识点一会就忘),发现真没那么难。思想上跟安卓接sdk一样的,就是语法略微不同。稍微查一下就知道怎么搞了。如果文章对你有帮助留下一个赞呗,你的支持是我继续写下去的动力。

小表情.png


收尾

写博文不易,希望大家多多支持,如有不对大家多多指正。写出来就是记录、学习和成长的过程。

你可能感兴趣的:(Unity接入ios SDK(小7手游)没有你想的那么难)