A Cocoa-Based Frontend For Unity iPhone Applications

I spent about 3 months at the end of 2008 knee-deep in Unity iPhone — first testing the beta and then working with the release version. I spent tons of time just playing with it, learning its capabilities and how to optimize for it. That’s a whole other post though, which I’ll get to sometime in the future. For now I want to talk about the Cocoa frontend that I developed for all our Unity iPhone games.

Why use a Cocoa frontend?

We wanted a way to allow players to login using their Blurst user id in our iPhone games, but Unity iPhone doesn’t yet support the iPhone keyboard. We could have simply used the device id to let users pair their account via the webpage, but I wanted a more elegant solution. Furthermore, after working on iSplume (which we coded entirely using Objective-C), I found that I could make menus much faster in Apple’s Interface Builder than in UnityGUI. Adam and I planned a fair number of menus in Rebolt, so I wanted a way to make them in Cocoa/Interface Builder.

So I set a goal: Make an easily extensible Cocoa frontend for Unity iPhone that supports Blurst logins and supports any menus we might want. It should work for any project we add it to, so we don’t have to do tons of custom code for every game. Further, it should require changing as little of ReJ’s existing Objective-C AppController code as possible, in the event that it changed in a later build. Finally, I wanted an easy way to add my additional files to the XCode project once I created a build. This is particularly important because, to maintain rapid iteration times, there must be a minimal amount we have to do in XCode between creating a build and installing that build on the phone.

Basic Architecture

The basic idea is that we write our own UIApplication delegate to replace AppController, and then forward events like applicationWillTerminate: to the existing AppController once we’ve started the Unity content. We’ll also keep a loop running in the background once the Unity content is created that checks the PlayerPrefs file. We’ll use this to get back to the menus from within the Unity content. Finally, we’ll organize the Cocoa content in such a way that we can easily add it to the XCode project that Unity iPhone spits out. We’ll use a PostprocessBuildPlayer Perl script to accomplish this.

Writing our own UIApplication delegate

Our UIApplication delegate needs to do a few different things. First, we want to handle the usual application event callbacks — applicationDidFinishLaunching:, applicationWillResignActive:, etc. Unless we’re particularly interested in some event, or we’re doing something more complex than just menus, we will just forward most of these messages to ReJ’s AppController. The notable exception is applicationDidFinishLaunching:, which we will use to launch our frontend and add a scheduled timer to the app’s run loop that will listen for menu return requests.

We’ll also want a few functions for switching between Unity and Cocoa content. We’ll create launchFrontend, cleanupFrontend, and launchUnity methods to handle switching content. We’ll also create a checkForReturnToMenu: method that our scheduled timer will call regularly. This will read the PlayerPrefs file and, if it finds a specific key, hide the Unity content and re-launch our frontend.

Here’s a zipped copy of the default Flashbang UIApplication delegate files — download it and follow along as I describe the various sections.
flashbangfrontend.zip

First, we’ll take a look at the header file. Note that we explicitly adopt the UIApplicationDelegate protocol. We also keep references to the application window and the Unity AppController.

#import "FBGameSettings.h"
#import "FBScene.h"
#import "FBSceneManager.h"
#import "FBSceneSetup.h"
#import "AppController.h"

// Integer tag that we use to distinguish the Unity view
#define UNITY_VIEW_TAG 1

@interface FBFrontendController : NSObject
{
   // A local reference to the app’s window
   UIWindow *window;
   // ReJ’s original AppController that runs Unity
   AppController *unityController;
}

- (void)checkForReturnToMenu:(NSTimer *)timer;
- (void)launchFrontend;
- (void)launchUnity;
- (void)cleanupFrontend;

@end

The specific details of the headers I’m importing are mostly unimportant. FBGameSettings.h is just some game-specific #defines (version number, etc). FBScene is a subclass of UIViewController and represents a generic menu scene. Specific scenes needed by an individual game are subclasses of this. FBSceneManager keeps a hash table of all scenes and handles transition animations between them. We’ll take a closer look at FBSceneSetup.h later.

Starting the Application

Now let’s take a look at the implementation of FBFrontendController. We’ll look at the applicationDidFinishLaunching: method first.

- (void)applicationDidFinishLaunching:(UIApplication *)application
{
   [application setStatusBarHidden:YES animated:NO];

   // Clear keys that signal transitions back and forth from
   // Unity, just in case
   [FBPlayerPrefs deleteKey:@“_start_cocoa”];
   [FBPlayerPrefs deleteKey:@“_start_unity”];

   // reset blurst logged in status
   [FBPlayerPrefs deleteKey:@“blurst_online”];

   // Start listening for signal to return to menus
   [NSTimer scheduledTimerWithTimeInterval:1.0 target:self
      selector:@selector(checkForReturnToMenu:)
      userInfo:nil repeats:YES];
   [self launchFrontend];
}

Here, we first delete the two PlayerPrefs keys we’ll use to communicate that we want to swap between Cocoa and Unity — _start_cocoa and _start_unity. This ensures that we know their initial states. Note: Unity iPhone stores PlayerPrefs using NSUserDefaults. FBPlayerPrefs is just a wrapper for NSUserDefaults that behaves like the PlayerPrefs class in Unity. We then begin a timer that runs our checkForReturnToMenu: method once per second. A shorter delay here means faster responsiveness for opening the frontend from within Unity, while a longer delay will give better performance. Finally, we run our launchFrontend method. Here’s the checkForReturnToMenu: method.

- (void)checkForReturnToMenu:(NSTimer *)timer
{
   if([FBPlayerPrefs getInt:@“_start_cocoa” orDefault:0] == 1)
   {
      [FBPlayerPrefs deleteKey:@“_start_cocoa”];
      [self launchFrontend];
   }
}

This simply checks for the proper key in PlayerPrefs and then launches the frontend if it finds it.

Launching Our Frontend

Here’s the launchFrontend method, which we call whenever we want to display our Interface Builder-constructed menu system:

- (void)launchFrontend
{
   // re-sync the PlayerPrefs file, in case we’ve been in Unity
   [FBPlayerPrefs readPrefsFile];

   // Create the window if we don’t already have one
   if([UIApplication sharedApplication].keyWindow == nil)
   {
      window = [[UIWindow alloc] initWithFrame:[[UIScreen
         mainScreen] bounds]];
      [window makeKeyAndVisible];
   }

   // Check to see if any views are exclusive/multitouch (ie find
   // the Unity EAGLView). Temporarily disable it, and tag it
   // so we can find it later
   for(UIView *v in window.subviews)
   {
      if(v.exclusiveTouch && v.multipleTouchEnabled)
      {
         v.exclusiveTouch = NO;
         v.multipleTouchEnabled = NO;
         v.tag = UNITY_VIEW_TAG;
         v.hidden = YES;
      }
   }

   // load menu scenes
   LoadScenesInWindow(window);

   // start the first scene
   [FBSceneManager startScene:FIRST_SCENE
      withTransition:FBSceneTransitionNone];
}

Here we create the application window if it doesn’t exist, we disable interaction with the Unity view if it’s present, then we load our custom views in the window. Finally, we start our first scene using the scene manager. LoadScenesInWindow is a function defined in FBSceneSetup.h — we’ll take a quick look at that.

#define FIRST_SCENE @"Title"  // The string key for our first scene

// load in game-specific scenes and define function to load them
#import "SceneCredits.h"
#import "SceneOptions.h"
#import "SceneTitle.h"
#import "SceneGame.h"

void LoadScenesInWindow(UIWindow* window)
{
   [window addSubview:[[[SceneCredits alloc] init] view]];
   [window addSubview:[[[SceneOptions alloc] init] view]];
   [window addSubview:[[[SceneTitle alloc] init] view]];
   [window addSubview:[[[SceneGame alloc] init] view]];
}

Recall that each SceneXXXX is a subclass of FBScene, which is a subclass of UIViewController. The init method of each SceneXXXX first calls initWithNibName: with the name of each scene’s Interface Builder xib, then adds the scene to the scene manager with an appropriate string-based key. The LoadScenesInWindow function thus initializes each view controller and adds its view to the window. The idea behind this architecture is that for any given project, we only have to edit FBSceneSetup.h, and then create the appropriate SceneXXXX subclasses to manage each of our scene xibs. That is, FBSceneController will be the same for every project.

Launching Unity

Now we’ll take a look at how we launch the Unity content.

- (void)launchUnity
{
   [self cleanupFrontend];
   // Tell Unity loading scene to stop holding
   [FBPlayerPrefs setInt:1 withKey:@“_start_unity”];
   [FBPlayerPrefs saveAndUnload];

   // If we’ve not yet started the Unity content, run its startup
   if(unityController == nil)
   {
      unityController = [[AppController alloc] init];
      [unityController applicationDidFinishLaunching:
         [UIApplication sharedApplication]];

      // Set our window to the one created by ReJ’s AppController,
      // for any future use
      window = unityController->_window;
   }

   // If we’ve already got Unity content running, show it and
   // return its Exclusive/multitouch status
   else
   {
      for(UIView *v in window.subviews)
      {
         if(v.tag == UNITY_VIEW_TAG)
         {
            v.exclusiveTouch = YES;
            v.multipleTouchEnabled = YES;
            v.hidden = NO;
            [window bringSubviewToFront:v];
         }
      }
   }
}

We start out by cleaning up the frontend (which just tells the scene manager to remove its views from the window and releases them). We then set the _start_unity PlayerPrefs key, so that our loaded Unity content will know we want it to start executing. After that, there are two possible codepaths. The first time we call the method, it will call AppController’s applicationDidFinishLaunching: method, and then point our local window reference to the one created by AppController. Once the Unity content is initialized (if we’ve come back to the menu and then want to return to Unity), we find our Unity view by the tag we set in launchFrontend, return it to the front, and re-enable interaction with it.

We’ll typically want to run this method in response to a button press in some view. Here’s an example use:

- (IBAction)playButtonPressed:(id)sender
{
   [(FBFrontendController *)[UIApplication
      sharedApplication].delegate launchUnity];
}

Using the Frontend From Unity

To get back to the frontend from Unity, we just need to set the _start_cocoa PlayerPrefs key. Since the Unity content will continue running in the background, you’ll also want to pause your game and have a loop continue to check for the _start_unity PlayerPrefs key.

Putting It All Together — Project Organization and PostprocessBuildPlayer

So this frontend stuff is all well and good, but it would be a royal pain in the ass if we had to copy the files into the project manually and edit ReJ’s files every time we made a build. But, as usual, it’s Perl to the rescue!

We’ll first create a directory inside our project that will contain all of our frontend files. We’ll set it up so that we can also replace the application icon and splash screen in the same pass. First, create an “XCode” directory within the Unity project. Any files that you want overwritten in the default XCode project should be in the same locations with the same names. So for instance, we’ll add Icon.png and Default.png to the root of the XCode directory. Put anything that you’re adding to the project (all the frontend files) into a separate “Frontend” sub-directory. So your tree should end up something like this:

XCode Directory Tree

Now we’ll take a look at the PostprocessBuildPlayer script. If you aren’t familiar with it, check the Build Player Pipeline section of the Unity Manual. Here’s my script, written in Perl:

#!/usr/bin/perl

#################################################################
# Build Player postprocessor for Unity iPhone projects. Injects #
# FBS Frontend into generated XCode project                     #
#################################################################

# Path for assets that will get added to the XCode project.
# Relative to the project root directory.
$iPhoneAssetPath = “./Assets/XCode/”;
$toPath = $ARGV[0];

##########################################################
# Copy iPhone assets from Unity project to XCode project #
##########################################################

opendir(XCODEDIR, $iPhoneAssetPath) || die(“Cannot open directory $iPhoneAssetPath”);
@files = readdir(XCODEDIR);
closedir(XCODEDIR);

# copy files from Unity iPhoneAssetPath to the generated XCode project
foreach $file (@files)
{
   # kind of a lazy hack
   unless(($file eq “.”) || ($file eq “..”))
   {
      #`echo $file > log.txt`;
      $fromPath = $iPhoneAssetPath.$file;
      `cp -R \‘$fromPath\’ \’$toPath\’`;
   }
}

################################################################
# Change default UIApplicationDelegate to FBFrontendController #
################################################################

$omPath = $toPath."/Classes/main.mm";
$nmPath = $toPath."/Classes/main.mm.tmp";

open OLDMAIN, "<", $omPath or die("Cannot open main.mm");
open NEWMAIN, ">", $nmPath or die("Cannot create new main.mm");

while(<OLDMAIN>)
{
   $_ =~ s/\”AppController\”/\”FBFrontendController\”/;
   print NEWMAIN $_;
}

close OLDMAIN;
close NEWMAIN;

`mv "$nmPath" "$omPath\’`;

#################################################
# Make _window variable in AppController public #
#################################################

$oacPath = $toPath."/Classes/AppController.h";
$nacPath = $toPath."/Classes/AppController.h.tmp";

open OLDAC, "<", $oacPath or die("Cannot open AppController.h");
open NEWAC, ">", $nacPath or die("Cannot create new AppController.h");

while(<OLDAC>)
{
   if($_ =~ m/UIWindow.*window/)
   {
      print NEWAC "\t\@public\n";
   }
   print NEWAC $_;
}

close OLDAC;
close NEWAC;

`mv "$nacPath" "$oacPath"`;

As you can see, this script does three things — copy all the files from our XCode directory to the built XCode project, change the UIApplicationDelegate in main.mm to FBFrontendController, and make the _window variable of AppController public. So we’ve managed to implement our own frontend by changing only two lines of existing code!

Building the Project

I know of no way to add files to an XCode project via the command line, so you will still have to manually add the files to the project after building. However, because of the way that we’ve organized the project, this will be relatively simple. It does mean, however, that “Build and Run” will no longer work as a one-click solution.

Build the project in Unity and open the generated project in XCode. Select Project -> Add To Project… (Cmd + Option + A), and select “Frontend” directory. In the next dialog, choose to “Recursively create groups for added folders:

Add files to project dialog

That’s it! You’ll now have the references needed to run your custom frontend before the Unity content — simply build and run in XCode!

This entry was posted in Tutorials, iPhone. Bookmark the permalink.

17 Responses to A Cocoa-Based Frontend For Unity iPhone Applications

  1. great explanations on how to achieve such a thing and the hurdles that one has to overcome.

    Very generous to share this with us and investing the time to explain the things, thank you very much.

  2. Oniatariio says:

    Does this work in the indie version of Unity iPhone publisher?

  3. @Oniatariio I’m not certain, as I’ve only ever had the full advanced featureset to work with. However, I believe the basic version still outputs an XCode project, and supports the PostprocessorBuildPlayer script, so I think it ought to work.

  4. Spocko says:

    This is great stuff! Thanks very much for the lesson.
    I’m still a n00b with mac/iphone/unity and was hoping I could get a look at the scene and scenemanager files. Could you please post these, too.

    Thanks again for sharing!

  5. @Spocko Yes — I have a simple Unity project I’m putting together with all the necessities to demonstrate the whole setup and build process. I should have it up within the next week — it will just be an amendment to this post.

  6. Spocko says:

    Very cool. I’ll be looking forward to it.
    I was able to use your code above to switch back and forth between cocoa and unity. Awesome.

    Live long and prosper! ;o)

  7. Chris Aiken says:

    Matt,

    Great work! This should be a great help to extend Unity functionality. I, like Spocko, would love to play with the implementation of scene and scenemanager as well. Look forward to the example project.

    Again, thanks for taking the time and effort to share. Hope I can do the same someday.

    Regards

  8. Andy says:

    Hey – This is a great technique. I’m just getting into iPhone dev and this was just what i needed. When i try to use it, however, i can’t get subviews to be added above my unity content (nor is the unity subview ever found on the window). I have to create a new window above the content instead. Would you happen to know of any reason this might be the case?

  9. @Andy: Despite being able to technically create multiple windows in code, the iPhone really only lets you use one at a time. I ran into this problem myself, which is why the launchUnity: method has the line:
    window = unityController->_window;
    to reassign the local window pointer to the window that Unity creates.

    Once a second window has been created, I couldn’t find any way to get the original window to return to the front, so it’s best to just treat it as though it’s gone forever.

    As for getting created views to add above the Unity content, by default subviews will always be added to the top of their superview/window. However, I’m not sure how well these play with the OpenGL EAGLView. That’s why I set the Unity EAGLView to hidden in the launchFrontend: method.

  10. Andy says:

    @Matt – Thanks for the info. It turns out that the window i was getting back from unityController->_window was nil – which is why my content wasn’t being added above the unity content. It seems there is a global var in the AppController called _window, which is where the EAGLView is added. The local _window was never being instantiated. I deleted that reference altogether, and just added a function to return the global _window, and everything worked perfectly. I’m not sure if i have a wrong version or something – i’m not sure what has changed from when you wrote the code to when i tried to implement :)

    Thanks for the great work – it has helped a lot.

  11. bill says:

    I just have a question ?How do I pass value from frontend to unity ?
    When I type something to the form,how can I pass the string to unity and actually show the string on the unity view?Hope to get any of answers

  12. Matt says:

    @bill
    Right now the only way to pass a value from Cocoa to Unity is through PlayerPrefs/NSUserDefaults. FBPlayerPrefs file is a wrapper for NSUserDefaults to make it behave like PlayerPrefs in Unity. So you can do like:

    [FBPlayerPrefs setInt:15 withKey:@“number_of_bunnies”]

    Then within Unity, you would do like:

    var numberOfBunnies:int = PlayerPrefs.GetInt(“number_of_bunnies”, 0);

  13. Fra says:

    Thanks for sharing. The approach is working smoothly, the only problem is that when I’m back to cocoa from unity and I move the device orientation an animation that tries to rotate the device in landscape view is showing up.

    My application is configured in portrait in info.plist and View Controllers are all returning NO to shouldAutorotateToInterfaceOrientation.

    Did you experience a similar issue or have any idea if the EAGLView could be the cause of the problem ?

  14. Donna says:

    Fra, I am having the exact same issue. Except that I have my application configured as a landscape, and when i come back my menus are portrait. I suspect the Unity EAGLView, but I have no clue how to overcome this issue. Any help would be greatly appreciated from anyone who’s been successful with their menus in XCode

  15. ecurtz says:

    Late reply here, I just started working on this stuff and have been using the uprise78 posted on the forum.

    I was able to switch back and forth between UIWindows using [window makeKeyAndVisible] without any issues. I originally tried this as a hack to fix autorotate on my UIKit views.

  16. Yann says:

    Hi,

    First of all : Thanks a lot ! All this is very interesting and will be very useful…
    once I get it to work properly :-/

    Actually I managed to build a project using the provided sources.
    It starts nicely on cocoa and switch to unity without any problem. But when I’m trying to get back to cocoa something isn’t working.

    If fact the FBFrontendController receive the signal and launchFrontend is called but it looks like it can’t find the Unity EAGLview. The widow subview count return 0 and the for loop is never played. So I can’t hide the Unity view.

    NSLog(@”%d”,window.subviews);

    for(UIView * v in window.subviews)
    {
    NSLog(@”for”);

    After the loop I try to set up a UIView :

    SceneTitle * newSceneTitle= [[SceneTitle alloc] init];
    [window addSubview:[newSceneTitle view]];
    [window bringSubviewToFront:[newSceneTitle view]];

    But nothing shoes up :-(
    Any ideas ?

    I’m using Unity 1.7 Basic and compile against iOS 3.2.

    Cheers !

  17. Rebecca says:

    Thank you very much for this tutorial, it was very helpful. I am still trying to add a subview to Unity’s UIWindow in landscape mode (ie. a bottom navigation bar created using Interface Builder) I know this is possible because the Rolls Royce Phantom application has both components working together in landscape mode. If anyone has any suggestions I would greatly appreciate it.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>