Archive for the ‘iPhone’ Category

A Cocoa-Based Frontend For Unity iPhone Applications

Friday, January 23rd, 2009

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!