Building a iOS Unity + UIView / UIViewController / Interface Builder App

Edit: 02/27/13

Here’s a demo project. Please read my very poorly written TERMS OF USE.txt before use. I also apologize for the unkept look of my blog; I haven’t had time to play with the formatting lately.

You must run the project on the iPhone iOS 5.0 simulator for it to work properly.

http://bit.ly/XcewEy

End Edit

Edit: 02/26/13

I’ve received a few questions about this post, so I’ll attempt to answer the most frequent ones here:

  1. How do I switch between Unity and the rest of my application?

    Both your application’s root view controller (UINavigationController for many) and Unity’s AppController should share the same window. Your view controller should be positioned above the AppController.

    To “swap” between your view controllers, you have 2 options. You can hide your view controller, exposing Unity’s beneath it; or, you can disable touch events on your view controller (you can set self.view.userInteractionEnabled = NO) and allow your events to propagate to Unity’s view controller.

    NOTE: you’ll need to set your view controllers backgrounds to a transparent color to be able to see Unity beneath it if you opt to keep your view controllers visible.

  2. How do I use the unityPause method?

    The purpose of this method is to pause Unity and stop it temporarily as to conserve system resources. You’d want to use this, for instance, when you have Unity hidden behind your iOS interfaces.

    The purpose of this method is not to transition between Unity and your view controllers. Please refer to 1. to see options on for to handle transitioning.

  3. Can you provide an example application?

    I certainly can and will. Please give me a few more days to do this, and I’ll update this post with a link.

    Here is a paste of one of my AppDelegate implementations. Ignore the super calls, or use them yourself to extend an existing implementations. Notice that didFinishLaunchingWithOptions is where most of the magic happens:

    //
    //  AppDelegate.m
    //
    //  Created by Alexander Wong on 4/16/12.
    //  Copyright (c) 2012 Ender's Fund Inc. All rights reserved.
    //
    
    #import "AppDelegate.h"
    #import "YourViewController.h"
    
    @implementation AppDelegate
    
    @synthesize unityController = _unityController; // unity
    
    // Unity
    
    - (void)playUnity
    {
        [_unityController unityPause:NO];
    }
    
    - (void)pauseUnity
    {
        [_unityController unityPause:YES];
    }
    
    #pragma mark - UIApplicationDelegate
    
    - (void)applicationDidReceiveMemoryWarning:(UIApplication *)application
    {
        [super applicationDidReceiveMemoryWarning:application];
        
        // unity
        [_unityController applicationDidReceiveMemoryWarning:application];
    }
    
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
        // unity
        BOOL returnBOOL;
        if (_unityController == nil) {
            _unityController = [[AppController alloc] init];
        }
        returnBOOL = [_unityController application:application didFinishLaunchingWithOptions:launchOptions];
        
        // unity, set window
        self.window = [_unityController window];
        
        // navigation controller
        YourViewController *yourViewController = (YourViewController *)[[[NSBundle mainBundle] loadNibNamed:@"YourViewController" owner:nil options:nil] objectAtIndex:0];
        self.navigationController = [[UINavigationController alloc] initWithRootViewController:yourViewController];
        [self.window addSubview:self.navigationController.view];
        
        // unity, return
        return returnBOOL;
    }
    
    - (void)applicationWillResignActive:(UIApplication *)application
    {
        [super applicationWillResignActive:application];
        
        // unity
        [_unityController applicationWillResignActive:application];
    }
    
    - (void)applicationDidEnterBackground:(UIApplication *)application
    {
        [super applicationDidEnterBackground:application];
        
        // unity
        [_unityController applicationDidEnterBackground:application];
    }
    
    - (void)applicationWillEnterForeground:(UIApplication *)application
    {
        [super applicationWillEnterForeground:application];
        
        // unity
        [_unityController applicationWillEnterForeground:application];
    }
    
    - (void)applicationDidBecomeActive:(UIApplication *)application
    {
        [super applicationDidBecomeActive:application];
        
        // unity
        [_unityController applicationDidBecomeActive:application];
    }
    
    - (void)applicationWillTerminate:(UIApplication *)application
    {
        [super applicationWillTerminate:application];
        
        // unity
        [_unityController applicationWillTerminate:application];
    }
    
    // push notifications
    - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
    {
        [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
        
        // unity
        [_unityController application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
    }
    
    - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
    {
        [super application:application didFailToRegisterForRemoteNotificationsWithError:error];
        
        // unity
        [_unityController application:application didFailToRegisterForRemoteNotificationsWithError:error];
    }
    
    - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
    {
        [super application:application didReceiveRemoteNotification:userInfo];
        
        // unity
        [_unityController application:application didReceiveRemoteNotification:userInfo];
    }
    
    - (void)application:(UIApplication*)application didReceiveLocalNotification:(UILocalNotification*)notification
    {
        [super application:application didReceiveLocalNotification:notification];
        
        // unity
        [_unityController application:application didReceiveLocalNotification:notification];
    }
    
    @end
    
    

End Edit

Edit: 02/26/13

After working with my implementation more, I’ve found that it is sometimes necessary to also replace the Unity Libraries directory along with the Data directory when updating your app with a new build of your Unity game.

End Edit

Original post below:

When you build your Unity 3D project for the iPhone/iPad, Unity will generate a new Xcode project for you from scratch. It will even overwrite most files on subsequent builds. The design pattern assumes you’ll only want to run Unity in the app and nothing else (no custom UIViews or UIViewControllers, etc).

However, there are a number of cases where you might want to integrate Unity into your app and be able to show custom UIViews. Perhaps, you want to use Unity only for a portion of your application flow. Configuring your Xcode project to allow you to do this is possible, albeit quite tricky.

Before we get started, you should note:

  1. Unity cannot be completely stopped and then started on demand (as of the writing of this post). The best you’ll be able to do is pause Unity, change to a blank scene to free up some of Unity’s memory use, and then load a new scene and resume. (In AppController.mm might notice the UnityCleanup method and wrongly assume you can call it and call applicationDidFinishLaunchingWithOptions to stop and start Unity, but you can’t. If you try, your app will crash.)
  2. Building everything in Unity makes it simple to build your application across platforms (iPhone + Android). Conversely, integrating Unity with your Xcode project means you’ll have to port over your Objective-C.
  3. There is no official support (currently) for writing your application this way, and so it will rest solely on your shoulders to ensure your application continues to function in the future.

Here’s the game plan for how to integrate Unity:

  1. Use your own AppDelegate (from now on referred to as YourAppDelegate) and forward all UIApplicationDelegate methods to AppController, which you instantiate in applicationDidFinishLaunchingWithOptions in your YourAppDelegate.
  2. Set YourAppDelegate's window property to keep track of the window returned by AppController. Instantiate your UIViewControllers or UIViews and add them as a subview to the aforementioned window. Your views are basically overlaid on top of Unity’s view.
  3. Expose methods to play and pause Unity.

Let’s get started:

0. Import, link, and copy all files and libraries needed by Unity into your app.

Start by copying all files in the folders named “Classes” and ”Libraries” from Unity into your app. Under “Classes”, move main.mm out of your project so you can reference it later. Also copy ”Data” but do not subversion this folder as it is generated each time your build your Unity application.

Now, under Targets > YourAppTarget > Build Phases, click “Add Build Phase” and select “Run Script”. Paste the contents of the Run Script from the Xcode project generated by Unity here. Mine looks like:

rm -rf "$TARGET_BUILD_DIR/$PRODUCT_NAME.app/Data"
cp -Rf "$PROJECT_DIR/Data" "$TARGET_BUILD_DIR/$PRODUCT_NAME.app/Data"

Under “Compile Sources”, check that the Unity Library files are linked. If you are using ARC in your project, add:

-fno-objc-arc

to the “Compiler Flags” next to any of Unity’s .m or .mm files.

Under “Link Binary With Libraries”, add any missing libraries from the Unity Xcode app and set the required/optional field accordingly.

1. Merge Unity’s .pch and main.mm file with your own.

For the .pch file, you can just about copy and paste its contents and overwrite your own. If you have custom code in your own file, you’ll have to merge by hand.

To merge the main.mm files, copy and paste Unity’s code into your file, but replace their UIApplicationMain call with your own:

    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([YourAppDelegate class]));
    }

You’ll likely have to change the extension on your main.m file to .mm. Again, if you have any custom code, merge it by hand.

2. Configure your application delegate files.

In YourAppDelegate create the following properties as applicable:

@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) UINavigationController *navigationController;
@property (strong, nonatomic) AppController *unityController; // unity

In AppController.h and AppController.mm create a getting method to return the window created by Unity.

- (UIWindow *)window
{
    return _window;
}

In application didFinishedLaunchingWithOptions instantiate AppController and store the window, also keep track of any UINavigation controllers you need.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // unity
    BOOL returnBOOL;
    if (_unityController == nil) {
        _unityController = [[AppController alloc] init];
    }
    returnBOOL = [_unityController application:application didFinishLaunchingWithOptions:launchOptions];

    //... your code here...

    // unity, set window
    _window = [_unityController window];
    
    // navigation controller
    _navigationController = [[UINavigationController alloc] initWithRootViewController:...];
    [_window addSubview:_navigationController.view];

    //... your code here...

    // unity, return
    return returnBOOL;

Lastly, forward all remaining UIApplicationDelegate methods like so…

- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application
{
    ...
    // unity
    [_unityController applicationDidReceiveMemoryWarning:application];
}

- (void)applicationWillResignActive:(UIApplication *)application
{
    ...
    // unity
    [_unityController applicationWillResignActive:application];
}

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    ...
    // unity
    [_unityController applicationDidEnterBackground:application];
}

- (void)applicationWillEnterForeground:(UIApplication *)application
{
    ...
    // unity
    [_unityController applicationWillEnterForeground:application];
}

- (void)applicationDidBecomeActive:(UIApplication *)application
{
    ...
    // unity
    [_unityController applicationDidBecomeActive:application];
}

- (void)applicationWillTerminate:(UIApplication *)application
{
    ...
    // unity
    [_unityController applicationWillTerminate:application];
}

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
    ...
    // unity
    [_unityController application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}

- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
    ...
    // unity
    [_unityController application:application didFailToRegisterForRemoteNotificationsWithError:error];
}

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
    ...
    // unity
    [_unityController application:application didReceiveRemoteNotification:userInfo];
}

- (void)application:(UIApplication*)application didReceiveLocalNotification:(UILocalNotification*)notification
{
    ...
    // unity
    [_unityController application:application didReceiveLocalNotification:notification];
}

Now Unity will be launched when your application launches but your application flow should be unchanged. Since your UINavigationController is added to the window after Unity, it will cover Unity. To handle switching back and forth between Unity and your views a little better, let’s add the ability to pause and resume Unity.

3. Pause and resume Unity.

Again in AppController.h and AppController.mm, create the following pause method:

- (void)unityPause:(BOOL)pause
{
    UnityPause(pause);
}

In YourAppDelegate, create the corresponding methods:

// Unity

- (void)playUnity
{
    [_unityController unityPause:NO];
}

- (void)pauseUnity
{
    [_unityController unityPause:YES];
}

4. Swap between Unity and your UIViews.

As previously mentioned, Unity will always be running in the background. To display your own views, simply add them to the window above Unity’s view and be sure to pause / resume and clear the scene as appropriate to help alleviate the overhead of running Unity in the background.

5. Configure “Build Settings” under targets before your build.

Unity is particularly finicky about your build settings and target. In fact, you’ll need to rebuild your Unity project to switch between an iOS device target and a simulator target. The target’s “Build Settings” will also have to be configured according to your intended device. The best way to set up “Build Settings” is to simply copy and merge settings line by line from the “Build Settings” generated by Unity’s Xcode project.

Create two targets in your project, one for simulator builds and one for device builds so you don’t have to modify settings each time. Then in Unity, build the Xcode project for the simulator and then your actual device. In between builds, copy over your build settings to your Xcode project’s respective targets.

Some things to note here: Be sure to set the TARGET_IPHONE_SIMULATOR=1 preprocessor macro on your simulator build. And, if you have any -all_load linker flags, you’ll have to remove them.

6. Build your Unity + UIView app.

First build your Unity project. Then in the generate Xcode files, copy the contents of the “Data” folder into your project’s “Data” folder. You may also need to replace the contents of your “Unity Libraries” folder as well. Ensure that your build settings in Unity match the iOS version and device type you intend to build your Xcode project for. If you build your Unity project for your iPhone and try to run your Xcode project in the simulator it will not work, and vice-versa.

If all goes well, you might see a few warnings but no errors, and your app should start right up.

If you need more control, you can communicate with your Unity scripts from your Objective-C by following the directions in this post:http://alexanderwong.me/post/29861010648/call-objective-c-from-unity-call-unity-from