Unity与iOS交互(2)——接入SDK

【前言】

接入Android和iOS SDK有很多相同的地方,建议先看下Android SDK如何接入。

 【UnityAppController详解】

 整个程序的入口在MainApp文件下的main.mm文件中,先加载了unityframework,然后调用runUIApplicationMain。源码如下:(这些源码在Xcode工程里都有)

#include 

UnityFramework* UnityFrameworkLoad()
{
    NSString* bundlePath = nil;
    bundlePath = [[NSBundle mainBundle] bundlePath];
    bundlePath = [bundlePath stringByAppendingString: @"/Frameworks/UnityFramework.framework"];//获取完整的UnityFramework的路径

    NSBundle* bundle = [NSBundle bundleWithPath: bundlePath];
    if ([bundle isLoaded] == false) [bundle load];//调用bunlde加载接口加载,bundle表示一个包含代码、资源和其他文件的目录或者是一个.framework文件

    //UnityFramework类的头文件是UnityFramework.h,实现文件在Classes文件夹中的main.mm
    UnityFramework* ufw = [bundle.principalClass getInstance];//获取bundle主类示例,主类是一个UnityFramework,bundle 只有一个主类,该类通常是应用程序的控制器类
    if (![ufw appController])  //调用appController方法获取,第一次获取的为空                  
    {
        // unity is not initialized
        [ufw setExecuteHeader: &_mh_execute_header];//设置头信息,初始化unity引擎
    }
    return ufw;
}

int main(int argc, char* argv[])//main是整个应用程序的入口,和什么语言没关系,一般都是这样
{
    @autoreleasepool //创建一个自动释放池
    {
        id ufw = UnityFrameworkLoad();//先加载了UnityFramework
        [ufw runUIApplicationMainWithArgc: argc argv: argv];//runUIApplicationMainWithArgc,这个方式是UnityFramework.h中的方法
        return 0;
    }
}

可以看看UnityFramework.h文件中定义的函数

#import 
#import 

#import "UnityAppController.h"

#include "UndefinePlatforms.h"
#include 
typedef struct
#ifdef __LP64__
    mach_header_64
#else
    mach_header
#endif
    MachHeader;
#include "RedefinePlatforms.h"


//! Project version number for UnityFramework.
FOUNDATION_EXPORT double UnityFrameworkVersionNumber;

//! Project version string for UnityFramework.
FOUNDATION_EXPORT const unsigned char UnityFrameworkVersionString[];

// In this header, you should import all the public headers of your framework using statements like #import 

#pragma once

// important app life-cycle events
__attribute__ ((visibility("default")))
@protocol UnityFrameworkListener
@optional
- (void)unityDidUnload:(NSNotification*)notification;
- (void)unityDidQuit:(NSNotification*)notification;
@end

__attribute__ ((visibility("default")))
@interface UnityFramework : NSObject
{
}

- (UnityAppController*)appController;

+ (UnityFramework*)getInstance;

- (void)setDataBundleId:(const char*)bundleId;

- (void)runUIApplicationMainWithArgc:(int)argc argv:(char*[])argv;
- (void)runEmbeddedWithArgc:(int)argc argv:(char*[])argv appLaunchOpts:(NSDictionary*)appLaunchOpts;

- (void)unloadApplication;
- (void)quitApplication:(int)exitCode;

- (void)registerFrameworkListener:(id)obj;
- (void)unregisterFrameworkListener:(id)obj;

- (void)showUnityWindow;
- (void)pause:(bool)pause;

- (void)setExecuteHeader:(const MachHeader*)header;
- (void)sendMessageToGOWithName:(const char*)goName functionName:(const char*)name message:(const char*)msg;

@end

函数实现在Classes文件夹下的main.mm文件中

#include "RegisterFeatures.h"
#include 
#include "UnityInterface.h"
#include "../UnityFramework/UnityFramework.h"

void UnityInitTrampoline();

// WARNING: this MUST be c decl (NSString ctor will be called after +load, so we cant really change its value)
const char* AppControllerClassName = "UnityAppController";

#if UNITY_USES_DYNAMIC_PLAYER_LIB
extern "C" void SetAllUnityFunctionsForDynamicPlayerLib();
#endif

extern "C" void UnitySetExecuteMachHeader(const MachHeader* header);

extern "C" __attribute__((visibility("default"))) NSString* const kUnityDidUnload;
extern "C" __attribute__((visibility("default"))) NSString* const kUnityDidQuit;

@implementation UnityFramework
{
    int runCount;
}

UnityFramework* _gUnityFramework = nil;
+ (UnityFramework*)getInstance 
{
    if (_gUnityFramework == nil)
    {
        _gUnityFramework = [[UnityFramework alloc] init];//获取单例时先调用alloc分配内存,再调用init初始化
    }
    return _gUnityFramework;
}

- (UnityAppController*)appController
{
    return GetAppController(); //调用UnityAppController.mm中的方法
}

- (void)setExecuteHeader:(const MachHeader*)header
{
    UnitySetExecuteMachHeader(header);//一个 Unity 引擎的函数,用于设置当前可执行文件的 Mach-O 头信息,header是一个指向可执行文件 Mach-O 头信息的指针
}//在 macOS 或 iOS 上,可执行文件的 Mach-O 头信息包含有关可执行文件的元数据,例如文件类型、CPU 架构、入口点和段信息。Unity 引擎使用该函数来设置当前可执行文件的 Mach-O 头信息,以确保 Unity 引擎可以正确加载并执行游戏逻辑。

- (void)sendMessageToGOWithName:(const char*)goName functionName:(const char*)name message:(const char*)msg
{
    UnitySendMessage(goName, name, msg);
}

- (void)registerFrameworkListener:(id)obj
{
#define REGISTER_SELECTOR(sel, notif_name)                  \
if([obj respondsToSelector:sel])                        \
[[NSNotificationCenter defaultCenter]   addObserver:obj selector:sel name:notif_name object:nil];

    REGISTER_SELECTOR(@selector(unityDidUnload:), kUnityDidUnload);
    REGISTER_SELECTOR(@selector(unityDidQuit:), kUnityDidQuit);

#undef REGISTER_SELECTOR
}

- (void)unregisterFrameworkListener:(id)obj
{
    [[NSNotificationCenter defaultCenter] removeObserver: obj name: kUnityDidUnload object: nil];
    [[NSNotificationCenter defaultCenter] removeObserver: obj name: kUnityDidQuit object: nil];
}

- (void)frameworkWarmup:(int)argc argv:(char*[])argv
{
#if UNITY_USES_DYNAMIC_PLAYER_LIB
    SetAllUnityFunctionsForDynamicPlayerLib();
#endif


    UnityInitTrampoline();
    UnityInitRuntime(argc, argv);

    RegisterFeatures();

    // iOS terminates open sockets when an application enters background mode.
    // The next write to any of such socket causes SIGPIPE signal being raised,
    // even if the request has been done from scripting side. This disables the
    // signal and allows Mono to throw a proper C# exception.
    std::signal(SIGPIPE, SIG_IGN);
}

- (void)setDataBundleId:(const char*)bundleId
{
    UnitySetDataBundleDirWithBundleId(bundleId);
}

- (void)runUIApplicationMainWithArgc:(int)argc argv:(char*[])argv
{
    self->runCount += 1;
    [self frameworkWarmup: argc argv: argv];
    UIApplicationMain(argc, argv, nil, [NSString stringWithUTF8String: AppControllerClassName]);
    //UIApplicationMain是iOS应用程序的入口点,一般在入口main函数中调用,unity在frameworkWarmup后调用,其用于启动应用程序并设置应用程序的主运行循环(main run loop)
    //该方法有4个参数,第三个参数是NSString类型的principalClassName,其是应用程序对象所属的类,该类必须继承自UIApplication类,如果所属类字符串的值为nil, UIKit就缺省使用UIApplication类
    //第四个参数是NSString类型的delegateClassName,其是应用程序类的代理类,该函数跟据delegateClassName创建一个delegate对象,并将UIApplication对象中的delegate属性设置为delegate对象,这里创建了UnityAppController,看其头文件是继承了UIApplicationDelegate
    //该函数创建UIApplication对象和AppDelegate对象,然后将控制权交给主运行循环,等待事件的发生。UIApplicationMain还会创建应用程序的主窗口,并将其显示在屏幕上,从而启动应用程序的UI界面。
}

- (void)runEmbeddedWithArgc:(int)argc argv:(char*[])argv appLaunchOpts:(NSDictionary*)appLaunchOpts
{
    if (self->runCount)
    {
        // initialize from partial unload ( sceneLessMode & onPause )
        UnityLoadApplicationFromSceneLessState();
        UnitySuppressPauseMessage();
        [self pause: false];
        [self showUnityWindow];

        // Send Unity start event
        UnitySendEmbeddedLaunchEvent(0);
    }
    else
    {
        // full initialization from ground up
        [self frameworkWarmup: argc argv: argv];

        id app = [UIApplication sharedApplication];

        id appCtrl = [[NSClassFromString([NSString stringWithUTF8String: AppControllerClassName]) alloc] init];
        [appCtrl application: app didFinishLaunchingWithOptions: appLaunchOpts];

        [appCtrl applicationWillEnterForeground: app];
        [appCtrl applicationDidBecomeActive: app];

        // Send Unity start (first time) event
        UnitySendEmbeddedLaunchEvent(1);
    }

    self->runCount += 1;
}

- (void)unloadApplication
{
    UnityUnloadApplication();
}

- (void)quitApplication:(int)exitCode
{
    UnityQuitApplication(exitCode);
}

- (void)showUnityWindow
{
    [[[self appController] window] makeKeyAndVisible];
}

- (void)pause:(bool)pause
{
    UnityPause(pause);
}

@end


#if TARGET_IPHONE_SIMULATOR && TARGET_TVOS_SIMULATOR
#include 

extern "C" int pthread_cond_init$UNIX2003(pthread_cond_t *cond, const pthread_condattr_t *attr)
{ return pthread_cond_init(cond, attr); }
extern "C" int pthread_cond_destroy$UNIX2003(pthread_cond_t *cond)
{ return pthread_cond_destroy(cond); }
extern "C" int pthread_cond_wait$UNIX2003(pthread_cond_t *cond, pthread_mutex_t *mutex)
{ return pthread_cond_wait(cond, mutex); }
extern "C" int pthread_cond_timedwait$UNIX2003(pthread_cond_t *cond, pthread_mutex_t *mutex,
    const struct timespec *abstime)
{ return pthread_cond_timedwait(cond, mutex, abstime); }

#endif // TARGET_IPHONE_SIMULATOR && TARGET_TVOS_SIMULATOR

调用UIApplicationMain创建了UnityAppController之后,接收iOS系统事件通知即可,接收通知的处理在 UnityAppController.mm文件中。下面是与生命周期相关的事件

先是willFinishLaunchingWithOptions,只发了个通知

- (BOOL)application:(UIApplication*)application willFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
    AppController_SendNotificationWithArg(kUnityWillFinishLaunchingWithOptions, launchOptions);
    return YES;
}

void AppController_SendNotificationWithArg(NSString* name, id arg) //Unity引擎用于在iOS平台上发送消息通知的方法
{
    [[NSNotificationCenter defaultCenter] postNotificationName: name object: GetAppController() userInfo: arg];
}

随后是didFinishLaunchingWithOptions

- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
    ::printf("-> applicationDidFinishLaunching()\n");

    // send notfications
#if !PLATFORM_TVOS

    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wdeprecated-declarations"

    if (UILocalNotification* notification = [launchOptions objectForKey: UIApplicationLaunchOptionsLocalNotificationKey])
        UnitySendLocalNotification(notification);

    if ([UIDevice currentDevice].generatesDeviceOrientationNotifications == NO)
        [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];

    #pragma clang diagnostic pop

#endif
    //UnityDataBundleDir()是一个用于获取Unity数据包目录的方法,它返回一个NSString对象,表示Unity数据包的路径
    //随后初始化Unity应用程序,但不会创建图形界面
    UnityInitApplicationNoGraphics(UnityDataBundleDir());

    [self selectRenderingAPI];//在iOS平台上,Unity引擎通常支持OpenGL ES和Metal两种渲染API,该方法会根据具体的设备和系统版本等因素来动态地选择使用哪种渲染API
    [UnityRenderingView InitializeForAPI: self.renderingAPI];//UnityRenderingView是用于呈现Unity引擎场景的iOS视图,InitializeForAPI是UnityRenderingView的初始化方法,self.renderingAPI表示Unity引擎所选的渲染API

#if (PLATFORM_IOS && defined(__IPHONE_13_0)) || (PLATFORM_TVOS && defined(__TVOS_13_0))
    if (@available(iOS 13, tvOS 13, *))
        _window = [[UIWindow alloc] initWithWindowScene: [self pickStartupWindowScene: application.connectedScenes]];
    else
#endif
//UIWindow是iOS中的一个视图对象,用于展示应用程序的用户界面.它是一个特殊的视图,通常作为应用程序中所有视图的容器。在iOS中,每个应用程序都有一个主窗口,即UIWindow对象,它是整个应用程序界面的根视图。所有其他的视图都是添加到UIWindow对象中的。
//UIWindow对象还可以响应用户的触摸事件和手势,同Android一样,Untiy会自己渲染视图,但需要使用操作系统提供的触摸事件
    _window = [[UIWindow alloc] initWithFrame: [UIScreen mainScreen].bounds];
//创建UnityRenderingView对象,而不是使用iOS系统的视图对象,如UIView或UIWindow,这个视图对象会放入UIWindow中
    _unityView = [self createUnityView];


    [DisplayManager Initialize];//unity的显示器管理类初始化,unity使用DisplayManager来管理显示器并在多个显示器上呈现Unity场景
    _mainDisplay = [DisplayManager Instance].mainDisplay;//设置主显示器
    [_mainDisplay createWithWindow: _window andView: _unityView];//将iOS的窗口UIWindow和Unity的RenderingView同主显示器关联起来

    [self createUI];//用于创建iOS应用程序界面中的常规视图对象,例如按钮、标签、文本框等
    [self preStartUnity];//预启动,在unity启动前可以注册插件

    // if you wont use keyboard you may comment it out at save some memory
    [KeyboardDelegate Initialize];

    return YES;
}

之后是applicationDidBecomeActive

- (void)applicationDidBecomeActive:(UIApplication*)application
{
    ::printf("-> applicationDidBecomeActive()\n");

    [self removeSnapshotViewController];//删除iOS应用程序窗口中的快照视图控制器
    //在iOS应用程序中,当应用程序进入后台时,系统会自动截取当前应用程序的截图,以便在应用程序再次进入前台时快速还原应用程序的状态。\
    //这个截图被称为快照(Snapshot),而用于管理快照的视图控制器被称为快照视图控制器(Snapshot View Controller)
    //unity自己渲染画面,不需要

    if (_unityAppReady)//从后台切换到前台时
    {
        if (UnityIsPaused() && _wasPausedExternal == false)
        {
            UnityWillResume();//会调用OnApplicaionFous
            UnityPause(0);
        }
        if (_wasPausedExternal)
        {
            if (UnityIsFullScreenPlaying())
                TryResumeFullScreenVideo();
        }
        // need to do this with delay because FMOD restarts audio in AVAudioSessionInterruptionNotification handler
        [self performSelector: @selector(updateUnityAudioOutput) withObject: nil afterDelay: 0.1];
        UnitySetPlayerFocus(1);
    }
    else if (!_startUnityScheduled)//刚打开游戏app走这里,调用startUnity
    {
        _startUnityScheduled = true;
        [self performSelector: @selector(startUnity:) withObject: application afterDelay: 0];
    }

    _didResignActive = false;
}

- (void)startUnity:(UIApplication*)application
{
    NSAssert(_unityAppReady == NO, @"[UnityAppController startUnity:] called after Unity has been initialized");

    UnityInitApplicationGraphics();//初始化Unity引擎的图形渲染

    // we make sure that first level gets correct display list and orientation
    [[DisplayManager Instance] updateDisplayListCacheInUnity];

    UnityLoadApplication();
    Profiler_InitProfiler();//初始化Profiler

    [self showGameUI];
    [self createDisplayLink];

    UnitySetPlayerFocus(1);

    AVAudioSession* audioSession = [AVAudioSession sharedInstance];
    [audioSession setCategory: AVAudioSessionCategoryAmbient error: nil];
    if (UnityIsAudioManagerAvailableAndEnabled())
    {
        if (UnityShouldPrepareForIOSRecording())
        {
            [audioSession setCategory: AVAudioSessionCategoryPlayAndRecord error: nil];
        }
        else if (UnityShouldMuteOtherAudioSources())
        {
            [audioSession setCategory: AVAudioSessionCategorySoloAmbient error: nil];
        }
    }

    [audioSession setActive: YES error: nil];
    [audioSession addObserver: self forKeyPath: @"outputVolume" options: 0 context: nil];
    UnityUpdateMuteState([audioSession outputVolume] < 0.01f ? 1 : 0);

#if UNITY_REPLAY_KIT_AVAILABLE
    void InitUnityReplayKit();  // Classes/Unity/UnityReplayKit.mm

    InitUnityReplayKit();
#endif
}

再后是进入后台进入前台

- (void)applicationDidEnterBackground:(UIApplication*)application
{
    ::printf("-> applicationDidEnterBackground()\n");
}

- (void)applicationWillEnterForeground:(UIApplication*)application
{
    ::printf("-> applicationWillEnterForeground()\n");

    // applicationWillEnterForeground: might sometimes arrive *before* actually initing unity (e.g. locking on startup)
    if (_unityAppReady)
    {
        // if we were showing video before going to background - the view size may be changed while we are in background
        [GetAppController().unityView recreateRenderingSurfaceIfNeeded];
    }
}

最后是applicationWillResignActive

- (void)applicationWillResignActive:(UIApplication*)application
{
    ::printf("-> applicationWillResignActive()\n");

    if (_unityAppReady)
    {
        UnitySetPlayerFocus(0);

        // signal unity that the frame rendering have ended
        // as we will not get the callback from the display link current frame
        UnityDisplayLinkCallback(0);

        _wasPausedExternal = UnityIsPaused();
        if (_wasPausedExternal == false)
        {
            // Pause Unity only if we don't need special background processing
            // otherwise batched player loop can be called to run user scripts.
            if (!UnityGetUseCustomAppBackgroundBehavior())
            {
#if UNITY_SNAPSHOT_VIEW_ON_APPLICATION_PAUSE
                // Force player to do one more frame, so scripts get a chance to render custom screen for minimized app in task manager.
                // NB: UnityWillPause will schedule OnApplicationPause message, which will be sent normally inside repaint (unity player loop)
                // NB: We will actually pause after the loop (when calling UnityPause).
                UnityWillPause();
                [self repaint];
                UnityWaitForFrame();

                [self addSnapshotViewController];
#endif
                UnityPause(1);
            }
        }
    }

    _didResignActive = true;
}

【接入SDK】

总体上和Android没什么区别,相比Andorid好点是不用做Jar,因为本质上是C#调用C,C调用OC,一些需要的编译在出包的时候都会编译好。

同样的看是否需要生命周期,不需要生命周期的自己写调用即可。如果需要生命周期,同样是需要继承的,在Android中继承的是Activiry,在iOS就是要继承UnityAppController。

在Plugins/iOS路径下创建CustomAppController.mm​文件。(文件名必须是 ___AppController,前缀可自选,但不能省略;否则在 Build 项目的时候,会被移动到错误的目录中去。)文件主要代码如下:



@interface CustomAppController : UnityAppController

@end

IMPL_APP_CONTROLLER_SUBCLASS (CustomAppController)
@implementation CustomAppController


//重写生命周期函数
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
    [super application:application didFinishLaunchingWithOptions:launchOptions];
    //自己的代码
    return YES;
}

- (void)applicationDidBecomeActive:(UIApplication *)application
{
    
    [super applicationDidBecomeActive:appliction]
    //自己的代码
}

//其他生命周期函数

@end

在Build iOS Project时,Unity会自动把该文件复制到Library文件里,原来的 UnityAppController在Classes文件夹里。

通过IMPL_APP_CONTROLLER_SUBCLASS知道要使用我们定制的 CustomAppController 而不是使用默认的 UnityAppController,其定义在UnityAppController.h文件中。可以看到其作用是在load的时候修改了AppControllerClassName。也即在加载UnityFramework时替换名字,这样在创建UIApplicationMain中创建的是我们新建的AppController。

// Put this into mm file with your subclass implementation
// pass subclass name to define

#define IMPL_APP_CONTROLLER_SUBCLASS(ClassName) \
@interface ClassName(OverrideAppDelegate)       \
{                                               \
}                                               \
+(void)load;                                    \
@end                                            \
@implementation ClassName(OverrideAppDelegate)  \
+(void)load                                     \
{                                               \
    extern const char* AppControllerClassName;  \
    AppControllerClassName = #ClassName;        \
}                                               \
@end   

【参考】

UnityAppController的定制以及Unity引擎的IL2CPP机 - 简书

你可能感兴趣的:(iOS,跨平台交互,AppController,接入iOS,SDK)