For most iOS developers there’s a clear dividing line: if you want to program “regular” apps with no or little multimedia content, you’ll be using Cocoa Touch and its UIKit framework to create the iPhone’s and iPad’s native user interfaces.
On the other hand, if you want to develop iOS games and multimedia applications, you want to use cocos2d and have little incentive to use anything but CCSprite
and CCMenu
to create your game’s scenes and user interfaces.
A great number of developers are experienced only in either environment, and they’ll often find it confusing to cross the border from Cocoa Touch to cocos2d, and vice versa. In almost all these cases, the programmers want to combine the best of both worlds, leveraging their existing knowledge of either Cocoa Touch or cocos2d to create hybrid applications.
Since Cocoa Touch and cocos2d work fundamentally differently and require a different mind-set, it’s usually not as straightforward to create such hybrids. This chapter will help you transition in both directions. You’ll learn how to add Cocoa Touch views and features to a cocos2d application; at the same time, you’ll also learn how you can plug in cocos2d to an existing Cocoa Touch application.
Cocoa Touch is the name of the application programming interface (API) used to create iOS applications. It is of course inspired by Cocoa, the API for programming Mac OS X applications.
Cocoa Touch is comprised of several frameworks such as Core Animation, Core Data, Map Kit, Store Kit, and Web Kit, just to name a few. But strictly speaking, even cocos2d is a Cocoa Touch library because the OpenGL ES framework, as well as Core Audio, OpenAL, and AV Foundation (AV stands for Audio/Video) frameworks that cocos2d is built on, are part of Cocoa Touch.
It is no wonder then that most programmers refer specifically to UIKit when they are asking about how to integrate Cocoa Touch views into cocos2d. UIKit is the framework that provides programmers with the native iOS controls and views that are used to build the graphical user interfaces (GUIs) of iOS applications. At the same time, other frameworks such as iAd, Web Kit, Game Kit, and Map Kit add specialized views, and they are mostly built with the GUI elements provided by UIKit.
So, technically, even if programmers discuss integration issues of Game Center with cocos2d, they will often refer to the views as being part of UIKit, even though the actual view is provided by Game Kit or Web Kit, for example. For reference, here are the cocos2d forum topics tagged with UIKit and Cocoa Touch, respectively:
http://www.cocos2d-iphone.org/forum/tags/uikit
http://www.cocos2d-iphone.org/forum/tags/cocoa-touch
Before we get to work with the code in this chapter, I want to step back for a moment and discuss why one would want to mix cocos2d with Cocoa Touch (UIKit views), what limitations there are, and what the differences between Cocoa Touch and cocos2d are.
There are many good reasons to mix Cocoa Touch and cocos2d. Essentially, they all boil down to a better user experience or faster development.
For one, if you’re a cocos2d programmer, you’ll be adding some Cocoa Touch views to your application sooner or later, most commonly to generate some revenue with iAd or if you’re writing a Game Center–enabled game. But you also might want to provide the users with a native-looking user interface, which can be designed efficiently with Interface Builder and later skinned with textures that maintain the game’s look and feel so that your user interface doesn’t look like the Settings app. A great example of such a skinned app is Carcassone; you’ll have to look twice to see that its user interface is actually entirely made with UIKit views.
While you can make reasonably good user interfaces with cocos2d, there’s simply a much greater variety of already existing controls available from UIKit that cocos2d doesn’t provide. And the occasional reimplementation in cocos2d always lacks in feel and features. Sliders, on/off toggle buttons, navigation views, and tab bars can all be highly useful in designing your game’s user interface, especially in those games or parts of the game where performance is not of the utmost importance.
If you are a Cocoa Touch programmer and you need some multimedia content in your game, it’s much easier to rely on cocos2d to do that job and do it with high performance rather than programming it directly with OpenGL ES. After all, cocos2d shields you from OpenGL ES and provides an interface that’s much easier to use.
Cocoa Touch does provide powerful graphical frameworks like Core Graphics and Core Animation. But they suffer from a major disadvantage: they are often not fast enough for real-time games. They were designed to display and animate user interface elements, not games.
When designing your app or game that mixes Cocoa Touch views with the cocos2d view, you should be aware of some limitations. Most obviously, UIKit views are not designed for high performance, so you may notice a drop in performance, especially if you use UIKit views in fast-paced games and during game play.
For example, it is more favorable for performance to rely on CCLabelBMFont
to display the score during game play than using a UITextField for the same purpose. And likewise, you should prefer to use CCMenu
for the in-game pause menu button rather than using a UIButton. In menu screens, however, those performance considerations are usually not a problem, and you can see improved productivity from being able to use Interface Builder to create your menu screens.
Mixing UIKit views in a cocos2d app while supporting autorotation will also affect performance, quite severely on first- and second-generation devices. Just allowing all views to be possibly rotated can have your framerate drop to below 60 fps right away, without even rendering anything! That means you might want to leave autorotation support disabled specifically for the older devices, which is the default setting in the cocos2d project templates.
You should also be aware that any UIKit view can be either in front of the entire cocos2d view or entirely behind it. You can’t have a UIKit view that is in front of some of the cocos2d scene’s sprites, labels, effects, and so on, while at the same time being behind other cocos2d sprites, labels, effects, or other nodes. In other words, you cannot “sandwich” a UIKit view between two or more cocos2d nodes.
You can do the opposite, however, although with some limitations. You can “sandwich” the cocos2d view: UIKit views in the background, then a transparent cocos2d view, and then some more UIKit views in the foreground. This approach requires only a little more work setting up the view hierarchy and making the cocos2d view transparent. Imagine playing a full-motion video in the background, over which you draw cocos2d sprites, and the rest of the user interface is made up of UIKit views.
But touch input remains a problem: either the UIKit views and not thecocos2d view will receive input or those UIKit views added to the cocos2d view and the cocos2d view itself will recieve input but not the views in the background. This has to do with the fact that the cocos2d view receives all touches on the screen simply because it occupies the entire screen. So, you need to write additional code to process the touches on the cocos2d view and then decide whether the cocos2d view should forward the touches, for example if the user didn’t touch any of the cocos2d sprites currently displayed on screen.
Allowing all views to receive input is possible, and I’ll provide you with a basic solution later in this chapter. But it is up to you to improve and adapt it for your own needs. Depending on your needs, the necessary code changes may actually be substantial and challenging in order to fully support UIKit views both in front of the cocos2d view and behind it and have all views reacting properly to touch input.
Let’s take a look at the major differences of Cocoa Touch programming compared to working with cocos2d. One difference is the Model-View-Controller pattern common to Cocoa Touch applications but essentially missing from cocos2d. And then you also have to consider the differences caused by cocos2d’s OpenGL ES view since it behaves differently in some aspects than a regular UIView
.
Probably the first and biggest difference for programmers coming from a Cocoa Touch background is that cocos2d does not strictly adhere to the Model-View-Controller (MVC) pattern, which is commonplace in Cocoa and Cocoa Touch.
The MVC pattern divides the programming tasks into the three subsets: model, view, and controller. The model contains any algorithms that run behind the scenes and maintains the state of the world; in essence, the model represents knowledge. The view is the visual representation of the model and renders the current state of the world based on the model data. And the controller essentially provides a means for the user to interact with the world through user input, but it is also used to react to other external events such as receiving data over the network. The model, view, and controller are each separate classes to decouple the user interface from business (or game) logic.
In games, the MVC pattern can be applied, and many have attempted to do so with cocos2d. You’ll find a good number of articles on the subject if you search for cocos2d mvc, and my personal favorite treatment of the subject is this two-part article by Bartek Wilczyński:
http://xperienced.com.pl/blog/how-to-implement-mvc-pattern-in-cocos2d-game
http://xperienced.com.pl/blog/how-to-implement-mvc-pattern-in-cocos2d-gamepart-2
For Cocoa Touch programmers, the fact that cocos2d does not follow the MVC pattern may come as a culture shock. But it’s one you can work around. On the other hand, as a cocos2d programmer, you likely won’t even notice that you’re using MVC because the entire Cocoa Touch framework is designed for the MVC pattern. You’ll happily use the controllers and views provided to you, and you’ll find no problem adding the logic and algorithms (the model) into either controller or view, or both. That is also a valid pattern, albeit more tightly coupled and less maintainable in large projects.
Instead of relying on UIKit for displaying its graphics, cocos2d creates an OpenGL ES view. This means cocos2d has more direct access to graphics resources and can render its view much faster. On the other hand, it does lose some of the automatic features of UIKit applications, like autorotation.
Of course, behind the scenes, all UIKit views are also rendered by OpenGL ES; there’s just a lot more stuff going behind the scenes that is needed for graphical user interfaces but is essentially a waste of performance if you want to make games. You may remember the very early games that were written entirely with UIKit, Core Graphics, and Core Animation? If not, good for you. They were often slow and unresponsive.
One immediately noticeable difference between Cocoa Touch and cocos2d is how autorotation is handled. Later in this chapter you’ll learn how cocos2d implements autorotation to rotate both OpenGL ES content as well as UIKit views. Fortunately for us, this issue was solved eventually by the cocos2d engine so you can rotate both UIKit and cocos2d views, but there are some performance drawbacks. And in some cases, you still need to consider the differences in coordinate systems used by UIKit and OpenGL ES, especially if views are being rotated.
And since cocos2d is programmed to interact directly with the graphics hardware, it uses its own hierarchy of displaying graphical elements. In cocos2d that’s the CCNode
hierarchy where you can add any CCNode
-based class to any other CCNode
, with a CCScene
as the very first element in that hierarchy. The UIKit framework, on the other hand, operates with a view hierarchy where you add UIView
-based classes to another, often with a UIWindow
as the topmost element. Both view hierarchies are incompatible, so you can’t add a UIView
to a CCNode
, and vice versa. This is noticeable when you change from one CCScene
to another using a CCTransitionScene
. While the cocos2d nodes all move aside, the UIKit views will remain fixed in place unless you also move them separately and in sync with the cocos2d animation. It’s actually a good idea to avoid this kind of situation in the first place.
The simplest and most straightforward example for using a UIKit view with cocos2d is found in the example project CocosWithCocoa01. It displays a UIAlertView
on top of the cocos2d scene created from the default cocos2d project template. To re-create the project from scratch, open Xcode and go to File New New Project to bring up the New Project dialog. In that dialog, select cocos2d
under the iOS list and create the cocos2d
project.
Let’s modify the HelloWorldLayer
class to display a UIAlertView
. The interface in HelloWorldLayer.hneeds only one small addition; namely, the HelloWorldLayer
class needs to support the UIAlertViewDelegate
protocol:
@interface HelloWorldLayer : CCLayer <UIAlertViewDelegate>
{
}
All other changes are made to the HelloWorldLayer.m implementation file. At the top, you first need to declare the PrivateMethods
interface to avoid the “may not respond to selector” compiler warning where the addSomeCocoaTouch
method is called before the actual implementation of the method:
#import "HelloWorldLayer.h"
@interface HelloWorldLayer (PrivateMethods)
-(void) addSomeCocoaTouch;
@end
The init
method of the “Hello World” sample is modified to use a uniformly colored background, just so you see the visual effect of the UIAlertView
, and to call the addSomeCocoaTouch
method. It still retains the Hello World CCLabelTTF
, but I moved it from its center position:
-(id) init
{
if ((self=[super init]))
{
// color background to make the UIAlertView "darkening" effect noticeable
glClearColor(0.1f, 0.3f, 0.7f, 1.0f);
CCLabelTTF* label = [CCLabelTTF labelWithString:@"Hello Cocos2D!"
fontName:@"Marker Felt"
fontSize:54];
CGSize size = [[CCDirector sharedDirector] winSize];
label.position = CGPointMake(size.width / 2, size.height / 6);
[self addChild:label];
[self showHelloWorldAlertView];
}
return self;
}
The addSomeCocoaTouch
allocates a UIAlertView
with a title, two buttons, and the message text “Hello Cocoa Touch!” For a delegate, you’ll be using self
now that you’ve added the UIAlertViewDelegate
protocol to the HelloWorldLayer
class.
Finally, you can show the alert view. Since showing the alert view retains the alert view behind the scenes, you can immediately send the release message to the alertView
without risking a crash. Listing 15–1 shows the resulting code.
Listing 15–1. A UIAlertView Is Created and Added to cocos2d’s openGLView (EAGLView Class)
-(void) showHelloWorldAlertView
{
UIAlertView* alertView = [[UIAlertView alloc] initWithTitle:@"UIAlertView Example"
message:@"Hello Cocoa Touch!"
delegate:self
cancelButtonTitle:@"Well"
otherButtonTitles:@"Done", nil];
[alertView show];
[alertView release];
}
TIP: It is not necessary to add a UIAlertView
to another view. This makes it very straightforward to create UIAlertView
messages. The only drawback is that UIAlertView
will always be drawn above everything else, and it will swallow all touches as long as it is displayed. No amount of sending views to back or reordering the view hierarchy will change that. If you need a simple solution for a pause menu, UIAlertView
is your cheap and dirty friend, especially during development. But keep in mind that while touches are disabled, you’ll still be receiving acceleration events, which you’ll have to turn off or ignore while the UIAlertView
is shown.
The HelloWorldLayer
class will receive all events from the UIAlertView
and can respond to them by simply implementing one or more of the UIAlertViewDelegate
methods. For this example, I decided to respond to the didDismissWithButtonIndex
message (see Listing 15–2), which is sent whenever the user taps a button, which always dismisses the UIAlertView
regardless of which button was tapped. Another CCLabelTTF
, with a string and color that depend on the buttonIndex
, is added to the cocos2d scene at a random position every time the alert view is dismissed.
Listing 15–2. Responding to the UIAlertView didDismissWithButtonIndex Message
-(void) alertView:(UIAlertView*)alertView
didDismissWithButtonIndex:(NSInteger)buttonIndex
{
NSString* message = @"Well";
ccColor3B labelColor = ccYELLOW;
if (buttonIndex == 1)
{
message = @"Done";
labelColor = ccGREEN;
}
CCLabelTTF* label = [CCLabelTTF labelWithString:message
fontName:@"Arial"
fontSize:32];
CGSize size = [[CCDirector sharedDirector] winSize];
label.position = CGPointMake(CCRANDOM_0_1() * size.width,
CCRANDOM_0_1() * size.height);
label.color = labelColor;
[self addChild:label];
// keep the alert view alive by bringing it up again
[self showHelloWorldAlertView];
}
The addSomeCocoaTouch
is called again whenever the alert view has been dismissed, so the alert view will keep showing up again, allowing you to add another label to the cocos2d view. You can see the result in Figure 15–1.
Figure 15–1. A UIAlertView is displayed over the cocos2d view.
Next you’ll be embedding more commonly used UIKit views in cocos2d. One of the simplest and most common is the UITextField
, which you’ll add on top of cocos2d, as you’ve done before. It gets more complicated when you move it to the background of cocos2d, which requires making the cocos2d view transparent.
Finally, I’ll show you how you can add your Interface Builder views into a cocos2d app, instead of creating the views programmatically, and I’ll also go into detail on how the RootViewController
class and autorotation works.
In the CocosWithCocoa03 project, I’ve added UITextField
views on top of the cocos2d view. The UITextField
is a simple text entry box that automatically brings up the iPhone keyboard when you tap it. I removed the UIAlertView
from the addSomeCocoaTouch
method in the HelloWorldLayer
class implementation and added the UITextField
:
-(void) addSomeCocoaTouch
{
// regular text field with rounded corners
UITextField* textField = [[UITextField alloc] initWithFrame:
CGRectMake(40, 20, 200, 24)];
textField.text = @"Regular UITextField";
textField.borderStyle = UITextBorderStyleRoundedRect;
// get the cocos2d view (it's the EAGLView class which inherits from UIView)
UIView* glView = [CCDirector sharedDirector].openGLView;
// add the text field view to the cocos2d EAGLView
[glView addSubview:textField];
// after that it’s safe to release the textField
[textField release];
}
It’s important to note that the process of programmatically creating UIView
classes is quite similar to how you create the UITextField
. You pick the desired class derived from UIView
and then call alloc
and initWithFrame
. Most UIView
controls can be created by just providing a frame rectangle. However, you will usually have to set some properties afterward to configure the control; in this example, I’ve set the textField
to use the rounded style as well as setting the initial text.
CAUTION: The frame rectangle is where many programmers first notice the different coordinate systems of cocos2d nodes and UIView
classes. Whereas in cocos2d the origin (0, 0) is at the lower-left corner of the screen, the origin for UIView
classes is at the upper-left corner of the screen. This means that the UITextField
is actually 20 pixels below the top border of the screen and not 20 pixels above the bottom border. You will have to keep this in mind when working with UIView
classes.
Since the UITextField
, like most other UIView
classes, does not have a show method, you need some other way to attach it to the view hierarchy. Since the cocos2d view is the EAGLView
class, which in turn inherits from UIView
, you can simply add the UITextField
to the cocos2d glView
as a subview. The CCDirector
has a openGLView
property, which allows you to access the cocos2d view and then call the addSubview
method on it to add the textField
. By default this adds the view on top of the cocos2d view.
If you try this now, you’ll see a text field on your scene, and when you tap the text field, the iPhone keyboard comes up, and you can start editing text. No extra code needed. Except, the keyboard won’t go away anymore.
This is by design because the Return key might be a valid key to start a new line rather than to stop editing. So, you need some way to dismiss the keyboard. To do so, open the HelloWorldLayer
header file and replace the UIAlertViewDelegate
protocol with the UITextFieldDelegate
protocol like so:
@interface HelloWorldLayer : CCLayer <UITextFieldDelegate>
{
}
Doing so allows the HelloWorldLayer
class to respond to UITextFieldDelegate
methods like textFieldShouldReturn
. For this to work, you must assign the HelloWorldLayer
class instance to the UITextField
by assigning self
to the delegate
property. Add the highlighted line at the end of the initialization block of the UITextField
:
// regular text field with rounded corners
UITextField* textField = [[UITextField alloc] initWithFrame:
CGRectMake(40, 20, 200, 24)];
textField.text = @" Regular UITextField";
textField.borderStyle = UITextBorderStyleRoundedRect;
textField.delegate = self;
Most UIKit views have this delegate method and an accompanying delegate protocol. So, if you ever wonder how you can respond to events of a certain UIView
, it’s done by implementing the class’s delegate protocol and responding to the appropriate message. Of course, one very common and repeated mistake you’ll make (I know I do) is to forget to actually assign the delegate or assign the correct one. So, whenever a delegate method isn’t being called, you should check whether you actually set the (right) class instance as the view’s delegate.
In our case, the textFieldShouldReturn
message of the UITextFieldDelegate
protocol is sent whenever the user taps the Return key on the iPhone keyboard:
-(BOOL) textFieldShouldReturn:(UITextField *)textField
{
// dismiss the keyboard
[textField resignFirstResponder];
// if the text is empty, remove the text field
if ([textField.text length] == 0)
{
[textField removeFromSuperview];
}
return YES;
}
By sending the resignFirstResponder
message to the textField
, the keyboard will be dismissed. Simply as an exercise on how to remove a UIView
from the cocos2d view, I’ve added a condition that sends the removeFromSuperview
message to the textField
if the textField
is empty when the user presses Return. Notice how this entire method does not care which UITextField
is sending the message, nor does it care where in the view hierarchy the textField
was added. You’ll take advantage of that next by adding another UITextField
.
If you try what you have so far, you’ll notice that the keyboard is dismissed when you press Return, and if you have deleted all characters from the text field, the entire text field will vanish.
TIP: Keep in mind that if it is possible that your scene changes while the user is editing text in a UITextField
, you would have to manually send the resignFirstResponder
message to all text fields in order to dismiss the keyboard. Otherwise, the keyboard may remain visible during and after the scene change, and the user won’t be able to dismiss it anymore. To avoid this situation, it is preferable to also respond to the textFieldDidBeginEditing
message and use that to temporarily disable any buttons or events that could change the current scene. Then reenable the buttons or events when you receive the textFieldShouldReturn
message.
No, I’m not going to peel off the text field’s skin! If you haven’t heard the term skinning before, it basically means adding (or changing) a texture to a user interface control or view. Essentially you’ll change the native look of the control or view and replace it with your own.
In Listing 15–3 you’ll be adding some more code at the bottom of the addSomeCocoaTouch
method in order to create a second UITextField
that uses a texture as background.
Listing 15–3. Skinning a UITextField View
-(void) addSomeCocoaTouch
{
…
// text field that uses an image as background (aka "skinning")
UITextField* textFieldSkinned = [[UITextField alloc] initWithFrame:
CGRectMake(40, 60, 200, 24)];
textFieldSkinned.text = @"With background image";
textFieldSkinned.delegate = self;
// load and assign the UIImage as background of the text field
NSString* file = [CCFileUtils fullPathFromRelativePath:@"background-frame.png"];
UIImage* image = [[UIImage alloc] initWithContentsOfFile:file];
textFieldSkinned.background = image;
[glView addSubview:textFieldSkinned];
[textFieldSkinned release];
[image release];
}
Creating the UITextField
should be familiar, and you also add self
as a delegate of the text field. The code that dismisses the keyboard and removes the text field when it is empty (see Listing 15–2) now works for this new UITextField
as well.
The next part is where cocos2d users with little or no Cocoa Touch programming experience may have problems. You can’t just add a CCSprite
or the sprite’s texture to a UIView
. You do need a UIImage
class for skinning Cocoa Touch views, which you can then comfortably create via initWithContentsOfFile
. Or not? Well, the returned UIImage
might be nil
.
It turns out that cocos2d allows you to use file names without specifying a path because internally it adds the path to the application’s bundle file for you. This full path to a bundle file looks something like this on an iOS device, and the path will be different when running the app in the simulator or on another device:
/var/mobile/Applications/…lots of letters…/CocosWithCocoa.app/background-frame.png
Since UIImage
and most other Cocoa Touch classes dealing with files expect the full path to the file, you will have to use the CCFileUtils
class method fullPathFromRelativePath
in order to create an NSString
, which contains the full path to the file in the app bundle. Then you get a valid UIImage
, and you can assign it to the background
property. You can see what this looks like in Figure 15–2.
Figure 15–2. Two UITextField views with the iPhone keyboard raised
TIP: The background image of a UIView
will always be scaled and stretched to fit the UIView
’s frame. This will often blur or otherwise distort the texture. To avoid that, you should design background images of UIView
s to the exact dimensions of the UIView
. Alternatively, design the texture for the largest possible size of the UIView
so that even if it is scaled, it is scaled down and doesn’t lose as much image quality compared to upscaling the texture.
What if you wanted to add a UIView
behind the cocos2d view? For example, say you wanted to show the video camera feed in the background for an augmented reality app. You’ll learn how to do so in Chapter 16, where I’m using the solution conveniently provided by cocos3d to create an augmented reality demo app.
There are a few things that you need to change to allow UIKit views in the background. You’ll find these code changes in the CocosWithCocoa04 project.
First, the view hierarchy needs to be changed so that the UIKit views are not a subview of the cocos2d view, respectively; the UIKit views can’t have the cocos2d view as their superview. That would always render them in front of the cocos2d view. The other required change is to make the cocos2d view transparent.
Let’s start by setting up the view hierarchy so that you have some way to add views to the hierarchy so that they’re behind the cocos2d view. For that change, you’ll have to open the AppDelegate.m file and look for the following lines in the applicationDidFinishLaunching
method:
// make the OpenGLView a child of the view controller
[viewController setView:glView];
// make the View Controller a child of the main window
[window addSubview: viewController.view];
[window makeKeyAndVisible];
As you can see, the cocos2d glView
is added directly to the UIWindow
. Well, it’s not quite directly because it is first set to be the viewController
view. The viewController
is an instance of the RootViewController
class provided by cocos2d to handle autorotation (more on that later). Then the viewController.view
, which is the same as the glView
now, is added as subview to the window
.
This setup means that any view that should be rendered before the cocos2d view will be a subview of the UIWindow
. There’s only one problem with that: you lose the autorotation feature provided by the RootViewController
class. All views added to the UIWindow
will display in portrait orientation by default, and they won’t rotate if you change the device orientation.
To overcome this problem, we need to introduce another view in between the UIWindow
and the cocos2d view. A simple UIView
does the job, and its only purpose is to be a dummy view that will handle the autorotation through the RootViewController
class and will contain all the subviews, both the cocos2d view and any UIKit views:
// add a dummy UIView to the view controller
UIView* dummyView = [[UIView alloc] initWithFrame:[window bounds]];
[viewController setView:dummyView];
[dummyView addSubview:glView];
[dummyView release];
// make the View Controller's view a child of the main window
[window addSubview:viewController.view];
[window makeKeyAndVisible];
I think a picture explains best how this changes the view hierarchy. Take a look at the before and after diagram in Figure 15–3 and notice that previously you’ve been adding UIKit views as subviews of the cocos2d view. Now with the introduction of the dummy view, all views are at the same level so that you can have UIView
s before and after the cocos2d view. Moreover, this gives you the ability to change the view order at runtime.
Figure 15–3. An illustration of the change made to the view hierarchy by introducing the dummy UIView
Adding our UITextField
views to the dummy view is straightforward. For this example, I skip over the UITextField
initialization code in the addSomeCocoaTouch
method because it doesn’t change. The only change is in adding the UITextField
views as subviews of the dummyView
:
-(void) addSomeCocoaTouch
{
// get the cocos2d view (it's the EAGLView class which inherits from UIView)
UIView* glView = [CCDirector sharedDirector].openGLView;
// The dummy UIView is the superview of the glView
UIView* dummyView = glView.superview;
// UITextField initialization code omitted
…
// add the text fields to the dummy view
[dummyView addSubview:textField];
[dummyView addSubview:textFieldSkinned];
// UITextField release code omitted
…
}
You can simply access the newly introduced dummy view because you’ve already added the cocos2d glView
to it, so that makes it the glView.superview
. The superview is the Cocoa term for what you would call the parent node in the cocos2d node hierarchy. You can then add the text fields to the dummyView
instead of the glView
.
However, you won’t notice a difference if you run the project now. Since you’ve added the text fields after the cocos2d view, they’re automatically rendered after the cocos2d view by default. This is the same behavior as in the cocos2d node hierarchy. To actually move the text fields to the back, we can either send the sendSubviewToBack
message to all of them or, more easily, send the bringSubviewToFront
message to the glView
, like so:
// send the cocos2d view to the front so it is in front of the other views
[dummyView bringSubviewToFront:glView];
Note that the sendSubviewToBack
and bringSubviewToFront
messages are sent to the view that contains the view that should be sent to the back or front. In this case, that’s the dummyView
. If you run the project now, you will see a difference. But you won’t be seeing the text fields anymore. What’s the problem?
By default, the cocos2d view is completely opaque. Anything behind the glView
will be obstructed because the cocos2d EAGLView
is filled each frame with an opaque clear color. It also has its opaque property set to YES
. This is easily remedied by adding the following lines to the addSomeCocoaTouch
method:
The opaque
flag is set to NO
, and the glClearColor
is all zero. The latter is not strictly necessary; it is sufficient to reduce the alpha channel (fourth parameter) so that the clear color is at least somewhat transparent. But for this example and in most cases, you don’t want the background to be tinted or just partially opaque. You may also wonder why setting the view’s opaque
property to NO
isn’t enough to make the view transparent. The answer is simple: OpenGL ES doesn’t respect that property and draws its clear color anyway.
This is only one half of the story. What’s easy to forget and something you just have to know is that cocos2d’s EAGLView
has to be set up with a pixelFormat
that actually has an alpha channel. Without the alpha channel, you can’t make the cocos2d view transparent.
By default, cocos2d initialized the EAGLView
with the kEAGLColorFormatRGB565
pixel format. This pixel format uses 16 bits per pixel and has no alpha channel. The only other pixelFormat
currently supported is kEAGLColorFormatRGBA8
, which has 8 bits per color channel plus an 8-bit alpha channel, which results in 32 bits per pixel. Obviously, this has an impact on performance and memory usage because the framebuffer memory usage doubles. That’s the reason why the kEAGLColorFormatRGB565
pixel format is the default, but there’s really no other choice than to use kEAGLColorFormatRGBA8
if you want to make the cocos2d view transparent.
Open the AppDelegate.m file, and in the applicationDidFinishLaunching
method look for the line that initialized the EAGLView
. Then change that to use the kEAGLColorFormatRGBA8
pixel format:
EAGLView *glView = [EAGLView viewWithFrame:[window bounds]
pixelFormat:kEAGLColorFormatRGBA8
depthFormat:0];
Now you can run the app again, and you’ll see the “Hello Cocos2D!” labels being drawn over the text fields. There’s only one issue remaining: the text fields won’t respond to your touches!
The easiest way to have the views behind the cocos2d view respond to touch events is to completely disable touch input on the cocos2d view. You won’t be receiving any messages from the CCTouchDispatcher
anymore if you add this line:
// This will disable all touch events on the cocos2d view
glView.userInteractionEnabled = NO;
Now the text fields behind the cocos2d view act normally, but touch input for the cocos2d view is disabled. UIKit views, which are in front of the cocos2d view, should also work normally and respond to touches, unless you’ve added them to the cocos2d glView
directly instead of the dummy view.
You may be wondering why disabling touch input on the cocos2d view is the best, or at least the easiest, option. For that, you have to understand that the cocos2d view is a UIView
that spans the entire screen area. Although you can see through it now that you’ve set it up to be transparent, it still responds positively to the UIViewhitTest
event. After all, any touch is somewhere on the screen, and since the cocos2d view is as big as the screen and doesn’t take into account what’s actually displayed inside its view, it responds positively to the hit test. So, any touch that reaches the cocos2d view will be processed by it or, respectively, the CCTouchDispatcher
class. Anything underneath the cocos2d view is cut off from receiving touch events.
Unfortunately, cocos2d does not have a built-in system to forward the hitTest
event to its nodes in order for them to decide whether they actually need to respond to the touch. I’ll present you with a solution that uses the node’s bounding boxes but requires some changes to the cocos2d code.
CAUTION: Only add the following hit test code to the EAGLView
class if you absolutely need it in your project. It will have a negative effect on performance whenever a touch event is fired, which is basically the whole time the user has at least one finger on the touchscreen. The more nodes there are in your scene, the larger the performance penalty will be.
Your first order of business, if you want to perform custom hit tests, is to make sure that userInteractionEnabled
is set to YES
. Please do that right away; otherwise, you might forget about the userInteractionEnabled
property and instead start looking for a bug in the hit detection code.
Now open the EAGLView.m file, which you can find in the /libs/cocos2d/Platforms/iOS
group in the Xcode project. Add the following #import
statements at the beginning of the file, because the EAGLView
class needs to know about these cocos2d classes:
#import "CCArray.h"
#import "CCScene.h"
#import "CCLayer.h"
Now somewhere in the implementation section of the EAGLView
class, override the hitTest
method shown in Listing 15–4. This method is part of the UIView
class and gets called when the UIKit framework is trying to determine which view needs to respond to a touch event. The method either returns a UIView
instance, which should receive the touch input, or returns nil
to signal that the hit test was unsuccessful, in which case the UIKit framework keeps looking for other views that might want to process the touch event.
Listing 15–4. Preparing to Hit Test All cocos2d Scene Children
-(UIView*) hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
UIView* hitView = [super hitTest:point withEvent:event];
if (hitView == self)
{
CCScene* runningScene = [CCDirector sharedDirector].runningScene;
CCArray* sceneChildren = [runningScene children];
CGPoint glPoint = [[CCDirector sharedDirector] convertToGL:point];
bool hit = [self hitTestNodeChildren:sceneChildren point:glPoint];
return (hit ? self : nil);
}
return hitView;
}
In this case, we first call the super implementation to receive the view the hitTest
would normally return. In almost all cases, this will be the EAGLView
itself, but since you can add subviews to the EAGLView
, it might return a subview, and in this case you want to allow the subview to handle the touch.
Otherwise, the runningScene
is obtained from the CCDirector
, which gives you access to the cocos2d node hierarchy via the children
array. Since the hitTest
point is in Cocoa Touch coordinates, you also have to convert it to GL coordinates before passing both the sceneChildren
and the glPoint
to the hitTestNodeChildren
method. If that method returns a hit, the hitTest
responds by returning self
. Otherwise, it lets the hitTest
fail by returning nil
, allowing all views behind the cocos2d view to take their turn and proceed with the hit testing.
The hitTestNodeChildren
method in Listing 15–5 is more complicated and harder to understand because it uses recursion to traverse the cocos2d node hierarchy. In other words, the function can call itself to go even deeper into the cocos2d node hierarchy. Add the hitTestNodeChildren
method just above the hitTest
method.
Listing 15–5. Recursively Testing All Nodes to Test If Their boundingBox Contains a Given Point
-(BOOL) hitTestNodeChildren:(CCArray*)children point:(CGPoint)point
{
bool hit = NO;
if ([children count] > 0)
{
Class sceneClass = [CCScene class];
Class layerClass = [CCLayer class];
CCNode* node = nil;
CCARRAY_FOREACH(children, node)
{
// check the node's children first
hit = [self hitTestNodeChildren:[node children] point:point];
// abort search on first hit
if (hit)
{
break;
}
// scenes/layers are always full screen, so do not hitTest them
if ([node isKindOfClass:sceneClass] || [node isKindOfClass:layerClass])
{
continue;
}
// check the node itself
hit = CGRectContainsPoint([node boundingBox], point);
// abort search on first hit
if (hit)
{
break;
}
}
}
return hit;
}
The first half of the CCARRAY_FOREACH
loop simply traverses deeper into the cocos2d node hierarchy by calling the function recursively with the current node’s children. If any of the recursive calls have found a hit, the loop is aborted right there.
In the second half, the actual node being iterated is checked. This performs the actual hit test by first making sure we’re not testing a CCScene
or CCLayer
class node. The reason for this is that they both have their boundingBox
set to the entire screen area. If you would test any of these classes, you would always “hit” them, and that is exactly what you’re trying to avoid.
Now that we’re sure the test is on a node with a reasonable bounding box, the actual check is as simple as testing for whether the point is inside the boundingBox
:
hit = CGRectContainsPoint([node boundingBox], point);
Again, if there was a hit, the loop aborts, and the method returns. This is an optimization because we only ever need to find any node that responds positively to the hit test.
Obviously, this solution has some drawbacks. For one, it assumes that a node should get a touch event if the touch is inside its boundingBox
. What it doesn’t know is whether there’s some kind of game state that would prevent the node from processing the touch, for example if the node is a CCMenuItem
that is currently disabled. Or, if the touch is on a sprite that actually performs a pixel-perfect collision check, in that case the bounding box check is too broad. Moreover, the boundingBox
is excessively large when the node is rotated because it is an axis-aligned bounding box that changes in size as the node rotates.
What you can do to alleviate this situation is to add a hitTest
method to the CCNode
class, which performs just the bounding box check by default but can be overridden by subclasses to perform more accurate or conditional checks. You’ll find this minor code change in the CocosWithCocoa04 project along with additional debug logging and touch detection code to help you understand the hit testing process.
Just for completing this test, I’d like to add another text field but in front of the cocos2d view so that we truly have a sandwiched cocos2d view with UIKit views in the back and in the front and all of them will be able to respond to touches.
The change is rather simple; just add this code at the end of the addSomeCocoaTouch
method, and make sure you add the textFieldFront
as subview of the dummyView
and not the glView
:
UITextField* textFieldFront = [[UITextField alloc] initWithFrame:
CGRectMake(280, 40, 200, 24)];
textFieldFront.text = @" On top of Cocos2D";
textFieldFront.borderStyle = UITextBorderStyleRoundedRect;
textFieldFront.delegate = self;
[dummyView addSubview:textFieldFront];
[textFieldFront release];
Actually, you could also add the textFieldFront
to the glView
as a subview without any immediately noticeable change. But adding the text field to the dummyView
allows you to reorder it in the view hierarchy at any time; for example, you could move it behind the cocos2d view using the sendSubviewToBack
method of the dummyView
. You wouldn’t be able to do that if you add the view directly to cocos2d’s glView
.
Check out Figure 15–4 with the result. You’ll have UIKit views on top of the cocos2d view and behind it. The text view at the back can still be edited and manipulated as the Cut, Copy, Paste, Replace
button shows. More importantly, despite the accompanying text field being behind the cocos2d view, the Cut, Copy, Paste, Replace
pop-over button is automatically on top of the cocos2d view. Just how it ought to be!
Figure 15–4. UIKit views on top and behind the cocos2d view, with input enabled for all of them
At this point, you may be wondering how you could add a view that was designed with Apple’s Interface Builder. Let’s tackle this now. Code-wise, it’s surprisingly simple, and you can look it up in the CocosWithCocoa05 project if you want.
The first order of business is to create an Interface Builder resource file. In Xcode 4 you create them comfortably from within the project using the File New New File command from the menu, or right-click a group and select New File.
You’ll be prompted to choose a template from the file template dialog. As you can see in Figure 15–5, you should create the Interface Builder file using the UIViewController
subclass template. This will also create the Interface Builder nib file for you and connect it with your view controller, which is essential for the view to work.
Figure 15–5. Create an Interface Builder view by creating a UIViewController subclass.
Make sure that the check box WithXIB for user interface in Figure 15–6 is checked, and make sure the Subclass of text is the UIViewController
. I decided to save this template using the file name MyView.m. You should end up with three new files in your project: MyView.h, MyView.m, and MyView.xib.
NOTE: The developer documentation and even the Cocoa Touch API refers to Interface Builder files as nib files even though they use the extension .xib. They used to have the .nib extension, and it simply stuck as a tradition even though the file extension was changed years ago. So, nib and xib are used interchangeably and refer to the same thing.
Figure 15–6. Make sure “With XIB for user interface” is checked.
If you click the MyView.xib file, you’ll be presented with the Interface Builder, which is no longer a separate application but integrated into Xcode 4. You’ll see an iPhone screen’s view onto which you can drag and drop views from the Object Library, accessible via View Utilities Object Library in case it’s not currently visible.
With Interface Builder, you can easily create your UIKit user interface visually. Since it’s beyond the scope of the book to explain the Interface Builder workflow, I’ll refer you to Apple’s Xcode 4 User Guide and specifically the section on Designing User Interfaces:
http://developer.apple.com/library/mac/#documentation/ToolsLanguages/Conceptual
/Xcode4UserGuide/InterfaceBuilder/InterfaceBuilder.html
While I’m at it, if you need a refresher or introduction to Views and Windows, take a look at Apple’s View Programming Guide for iOS:
http://developer.apple.com/library/ios/#documentation/WindowsViews/Conceptual/V
iewPG_iPhoneOS/Introduction/Introduction.html
NOTE: Unfortunately, you cannot use Interface Builder to design your cocos2d view. For that you will have to use a separate editor like CocoShop, CocoaBuilder, LevelHelper, or any other editing tool with cocos2d support that fits your need. Please refer to Chapter 17 for a list of cocos2d editing tools.
For now, it is sufficient to just add any views to the Interface Builder view, like sliders, buttons, labels, and whatnot. But ideally you should at least do the following: select the main view and bring up the Attributes Inspector via View Utilities Attributes Inspector. The first attribute under Simulated Metrics is called Orientation, and you should change that to Landscape since the application is currently only capable of running in Landscape mode. If you don’t do that, your views will be rotated by 90 degrees when you run the application.
The MyView
class does not need to be modified; the default implementation works just fine. You can directly load the MyView.xib file by adding the following code at the end of the addSomeCocoaTouch
method:
// add an Interface Builder view
MyView* myViewController = [[MyView alloc] initWithNibName:@"MyView" bundle:nil];
[dummyView addSubview:myViewController.view];
[dummyView sendSubviewToBack:myViewController.view]; // optional
[myViewController release];
Notice that the initWithNibName
takes the name of the xib file as a parameter but without the .xib extension. If you add the extension, you’ll receive an error message that the xib could not be loaded. The bundle parameter is nil
, which means the app should look for the file in the main bundle.
Since the MyView
class inherits from UIViewController
, you can access the actual view with the myViewController.view
property. You’ll add that to the dummyView
, and if you want, you can also issue an sendSubviewToBack
message to put the view in the background. Lastly, and as always, you’ll release the myViewController
when you’re done with it, since the addSubview
method retains the view.
You can now create and add views designed with Interface Builder to a cocos2d app. Your result might look something like the one in Figure 15–7.
Figure 15–7. The resulting project shows the MyView.xib file designed with Interface Builder in the bottom half.
It’s about time that we discuss autorotation. It’s one of the things that need special considerations when you move from a purely cocos2d-based app to one that also uses UIKit controls. This is most obvious if you run any of the previous CocosWithCocoaexample projects on a first- or second-generation device. In that case, you’ll see all UIKit controls oriented to the portrait mode. What’s wrong?
The culprit is the code in the file GameConfig.h that the cocos2d project template creates. Here’s the essential code of the file making use of compiler flags to change the default autorotation behavior depending on the device type and platform:
#define kGameAutorotationNone 0
#define kGameAutorotationCCDirector 1
#define kGameAutorotationUIViewController 2
// 3rd generation and newer devices: Rotate using UIViewController.
#if defined(__ARM_NEON__) || TARGET_IPHONE_SIMULATOR
#define GAME_AUTOROTATION kGameAutorotationUIViewController
// ARMv6 (1st and 2nd generation devices): Don't rotate. It is very expensive.
#elif __arm__
#define GAME_AUTOROTATION kGameAutorotationNone
// Ignore this value on Mac
#elif defined(__MAC_OS_X_VERSION_MAX_ALLOWED)
#else
#error(unknown architecture)
#endif
Cocos2d defines three distinct types of autorotation support:
kGameAutorotationNone
kGameAutorotationCCDirector
kGameAutorotationUIViewController
Not supporting autorotation will lock the app into the orientation it was designed for. It is also the fastest mode, especially if you consider the impact on performance on ARMv6 (first- and second-generation) devices; it is considered so severe that it is disabled by default for those devices. So, why would you even support autorotation if it has an impact on performance?
The answer lies in Apple’s iOS Human Interface Guidelines (HIG). Specifically, see the Handling Orientation Changes section here:
developer.apple.com/library/ios/#documentation/UserExperience/Conceptual/Mobile
HIG/UEBestPractices/UEBestPractices.html
The minimum recommendation to take away from this is that your app should at least support both variants of an orientation. Locking the app to just a single orientation could cause a rejection of your app during the approval process.
TIP: Shortly before the iPad was first released, Apple’s iOS Human Interface Guidelines specified explicitly that all iPad apps must support rotation to all orientations. After much protest from developers, specifically from game developers whose games can’t easily support all orientations, Apple has changed the wording from a “must have” to: “On iPad, strive to satisfy users’ expectations by being able to run in all orientations.”
This leaves the other two autorotation modes. The difference between kGameAutorotationCCDirector
and kGameAutorotationUIViewController
is that the CCDirector
-based autorotation rotates only the cocos2d view but ignores any UIKit views. Only if you have UIKit views in your app should you need to use the UIViewController
-based autorotation mode. Mainly that’s because it’s the slowest mode and would simply waste performance if you did not use UIKit views.
The initial orientation is set in the AppDelegate
class. In the applicationDidFinishLaunching
method, you’ll see a few lines of code that check the GAME_AUTOROTATION
setting to set the initial orientation. Since the UIViewController
expects to be started in the portrait mode regardless of which orientations your app supports, it is initially set to portrait orientation. Otherwise, the default landscape orientation is used:
viewController = [[RootViewController alloc] initWithNibName:nil bundle:nil];
viewController.wantsFullScreenLayout = YES;
…
#if GAME_AUTOROTATION == kGameAutorotationUIViewController
[director setDeviceOrientation:kCCDeviceOrientationPortrait];
#else
[director setDeviceOrientation:kCCDeviceOrientationLandscapeLeft];
#endif
The actual handling of orientation changes is the responsibility of the RootViewController
class, which cocos2d also initializes in the applicationDidFinishLaunching
method. Let’s take a look at the RootViewController
class implementation and specifically add the shouldAutorotateToInterfaceOrientation
method, which is called by the UIKit framework in order to query which interface orientations are supported by this particular UIViewController
. The method returns YES
only for those orientations it supports, and it returns NO
if it does not support rotation to the given interfaceOrientation
.
-(BOOL) shouldAutorotateToInterfaceOrientation:
(UIInterfaceOrientation)interfaceOrientation
{
#if GAME_AUTOROTATION == kGameAutorotationNone
return (interfaceOrientation == UIInterfaceOrientationPortrait);
#elif GAME_AUTOROTATION == kGameAutorotationCCDirector
if (interfaceOrientation == UIInterfaceOrientationLandscapeLeft)
{
[[CCDirector sharedDirector]
setDeviceOrientation:kCCDeviceOrientationLandscapeRight];
}
else if (interfaceOrientation == UIInterfaceOrientationLandscapeRight)
{
[[CCDirector sharedDirector]
setDeviceOrientation:kCCDeviceOrientationLandscapeLeft];
}
return (interfaceOrientation == UIInterfaceOrientationPortrait);
#elif GAME_AUTOROTATION == kGameAutorotationUIViewController
return (UIInterfaceOrientationIsLandscape(interfaceOrientation));
#endif // GAME_AUTOROTATION
return NO;
}
The previous code is a cleaned-up version of the code you’ll find in the RootViewController
class. I removed all comments and made the code more concise and readable, because in the RootViewController
class it looks more daunting than it actually is.
Once again, this code uses the current GAME_AUTOROTATION
setting set in GameConfig.h to pick one of three possible code paths. If autorotation support is set to kGameAutorotationNone
, the method returns YES
only if the orientation is the portrait mode. This may seem a bit strange if your app is actually using the landscape orientation. In fact, you could just as well return NO
here or call the super
implementation and return that value. It doesn’t matter because if kGameAutorotationNone
is used, no autorotation takes place. Of course, you can still call the CCDirectorsetDeviceOrientation
method manually at any time to change the orientation.
If the kGameAutorotationCCDirector
mode is in effect, the director’s setDeviceOrientation
method is called to change the device orientation to one of the supported interfaceOrientation
modes. If you wanted to support all four orientations, for example, or both portrait orientations instead of landscape orientations, you would have to extend the code accordingly to call setDeviceOrientation
with the corresponding and supported device orientation. Here’s the code you would use if your app were designed for portrait orientations instead of landscape orientations:
if (interfaceOrientation == UIInterfaceOrientationPortrait)
{
[[CCDirector sharedDirector] setDeviceOrientation:kCCDeviceOrientationPortrait];
}
else if (interfaceOrientation == UIInterfaceOrientationPortraitUpsideDown)
{
[[CCDirector sharedDirector]
setDeviceOrientation:kCCDeviceOrientationPortraitUpsideDown];
}
NOTE: You may have noticed that if the interfaceOrientation
is UIInterfaceOrientationLandscapeLeft
, the device orientation is actually set to the seemingly opposing kCCDeviceOrientationLandscapeRight
mode. This is not a mistake but a difference in definition in the UIInterfaceOrientation
and UIDeviceOrientation
enums.
Setting the device orientation with the setDeviceOrientations
method has one drawback: it won’t rotate UIKit views. If you use any UIKit views in your app and you want them to autorotate, you will have to use the kGameAutorotationViewController
setting. By default only landscape orientations are supported:
// support all landscape orientations
return (UIInterfaceOrientationIsLandscape(interfaceOrientation));
You can easily change that to support only portrait orientations:
// support all portrait orientations
return (UIInterfaceOrientationIsPortrait(interfaceOrientation));
Or you can even signal that you support all orientations:
// support all four orientations
return (UIInterfaceOrientationIsLandscape(interfaceOrientation) ||
UIInterfaceOrientationIsPortrait(interfaceOrientation));
In the latter case, you could just return YES
. But how does the view controller perform the actual autorotation? The actual rotation happens in the willRotateToInterfaceOrientation
method in the RootViewController
class, shown in Listing 15–6.
Listing 15–6. Performing the Rotation of the cocos2d View
#if GAME_AUTOROTATION == kGameAutorotationUIViewController
-(void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
duration:(NSTimeInterval)duration
{
CGRect screenRect = [UIScreen mainScreen].bounds;
CGRect rect = CGRectZero;
if (toInterfaceOrientation == UIInterfaceOrientationPortrait ||
toInterfaceOrientation == UIInterfaceOrientationPortraitUpsideDown)
{
rect = screenRect;
}
else if (toInterfaceOrientation == UIInterfaceOrientationLandscapeLeft ||
toInterfaceOrientation == UIInterfaceOrientationLandscapeRight)
{
rect.size = CGSizeMake(screenRect.size.height, screenRect.size.width);
}
CCDirector* director = [CCDirector sharedDirector];
EAGLView* glView = director.openGLView;
float contentScaleFactor = [director contentScaleFactor];
if (contentScaleFactor != 1)
{
rect.size.width *= contentScaleFactor;
rect.size.height *= contentScaleFactor;
}
glView.frame = rect;
}
#endif // GAME_AUTOROTATION == kGameAutorotationUIViewController
In essence, the cocos2d EAGLView
obtained via director.openGLView
gets its frame property updated with the new screen size. And that is the new screen size returned from the [UIScreen mainScreen].bounds
property. The only thing that this code sorts out for you is that in landscape orientations the height is actually the new EAGLView
frame’s width, and vice versa. And of course, it takes the Retina display mode into account by multiplying rect.size.width
and rect.size.height
with the contentScaleFactor
. You don’t actually need to understand the code in full since you’ll hardly ever need to change it. The only thing you should know is that the code assumes that the cocos2d view is in full-screen.
The project named CocosWithCocoa06 has some changes regarding the RootViewController
and autorotation. For example, it defaults to support the UIViewController
even on first- and second-generation devices. If you have one of those, you’ll see the screen autorotate, and you might want to compare its performance with the previous CocosWithCocoa05project. Additionally, the RootViewController
class code is cleaned up and contains the example code to enable autorotation support for portrait or all four orientations.
Many developers don’t realize it is actually possible to embed a cocos2d view in a regular application using UIKit views as its main elements. The cocos2d view doesn’t even have to be full-screen!
The problem merely lies in setting up the project. I’d like to show you how it’s done.
Fire up your Xcode 4. Create a new project via File New New Project. Select the View-Based Application template, as shown in Figure 15–8, and name the project ViewBasedAppWithCocos2D. You’ll end up with a project that has a view controller class, the corresponding .xib file, a MainWindow.xib file, and an app delegate class. If you run it right now, you’ll see a blank iPhone view with just the status bar on top.
Figure 15–8. The starting point for embedding a cocos2d view is the View-based Application template.
TIP: You may encounter the message in Figure 15–9 if you try to run this project on a device, even if the device is properly provisioned. By default, the new project templates set the iOS Deployment Target setting to the latest iOS SDK, which may be iOS 4.3 or iOS 5.0. Those and newer iOS versions are not available for devices of the first and second generation. In such a case, click the project in the Project Navigator pane to see the list of projects and targets. Select the project again in the new view and switch to its Info
pane. You’ll see that the first setting iOS Deployment Target
is set to iOS 4.3
or newer. Change this to iOS 3.1.3
or lower and try running the app again. If Xcode still complains with the same message as in Figure 15–9, your device is not properly provisioned, and you’ll have to look into that. Possibly the provisioning profile has expired, and you may have to get a new one from http://developer.apple.com/ios
.
Figure 15–9. If you get this message when trying to run the app on a device, try changing the iOS Deployment Target setting in the project’s Info pane to iOS 3.1.3 or lower.
The next step is to get the cocos2d source code added to the project. There are several ways to do this; I prefer to rely on the cocos2d project templates because it makes it easier to copy exactly the necessary files—no more, no less. So, in Xcode, create another project via File New New Project, and this time select one of the cocos2d project templates. If you need physics in your cocos2d view, you should choose one of the templates using a physics engine; otherwise, just pick the cocos2d template. Save the project anywhere using any name; just remember where you saved it. In fact, you can immediately close the cocos2d Xcode project after you’ve created it.
With your ViewBasedAppWithCocos2D project still open in Xcode 4, navigate to the cocos2d project folder using the Finder app. Locate the subfolder named libs in the cocos2d project folder. Drag the libs folder onto the ViewBasedAppWithCocos2D project and drop it onto the Project Navigator pane where all the project’s files are listed. If necessary, open the Project Navigator first by selecting View Navigators Project from the menu. Make sure the Copy items into destination group’s folder (if needed) check box is checked and the other settings also correspond to the ones you see in Figure 15–10.
Figure 15–10. Make sure the Copy Items… check box is set when dropping the cocos2d libs folder into the view-based application project.
You can’t build the project just yet because the cocos2d engine requires additional frameworks and libraries, without which you’ll only receive linker errors. To add these dependencies to the project, select the project itself in the Project Navigator (first entry in the list with the blue icon). Select the ViewBasedAppWithCocos2D target, navigate to the Build Phases tab, and unfold the Link Binary With Libraries pane. Then click the + button to add additional libraries. If you have trouble finding this location, take a look at Figure 15–11.
Figure 15–11. Add the missing frameworks and libraries to the target’s Link Binary With Libraries Build Phase.
Once you click the + button, a list of frameworks and libraries opens. Here’s the list of frameworks and libraries you will have to add. They’re found in the list in the same alphabetical order as I list them here:
Note that you can select all libraries in one go by holding down the Command key while selecting the items in the list. The added frameworks and libraries will be added to the root of the project in the Project Navigator. You can safely move them to the Frameworks group where they belong, just to get them out of sight since you don’t need to work with them.
You can now build and run the app. It has the cocos2d source code built into it, but of course without a user interface, it’s the same dull and empty app as it was before. Let’s change that!
Select the ViewBasedAppWithCocos2DViewController.xib file in the Project Navigator to see the Interface Builder view. Using the Object Library (View Utilities Object Library), drag and drop the following objects onto the view. You can arrange these objects in the design area in any way you like:
Now select the newly added View object and switch to the Identity Inspector (View Utilities Identity Inspector). You’ll notice that the first item shows the view is derived from the UIView
class. Since this should become the cocos2d EAGLView
, use the drop-down button to select a custom class from the list. One of the first items should be the EAGLView
class. If not, scroll the list until you find the EAGLView
or simply type in the name of the class. The resulting user interface mockup should look something like Figure 15–12.
Figure 15–12. The Interface Builder view of the hybrid app’s user interface
Interface Builder will automatically instantiate the EAGLView
for you. You only need to attach the CCDirector with this particular view. The On/Off Switch should serve as the toggle button that turns the cocos2d view on and off.
First, some preparations. Select the EAGLView
view and switch to the Attributes Inspector (View Utilities Attributes Inspector). Check the Hidden
check box so that the view is initially hidden. With the Attributes Inspector still open, select the On/Off Switch and change its initial State
to Off
. You’ll hide and unhide the EAGLView
programmatically.
NOTE: The cocos2d CCDirector
class can manage only one EAGLView
at a time, mainly because the CCDirector
class is a singleton. An app using multiple cocos2d views at the same time is not possible without significant changes to cocos2d. It was simply never designed to work with multiple views at the same time.
Now we need to make the connection from the buttons on the view to the ViewBasedAppWithCocos2DViewController
class. The easiest way to do so is to open an Assistant Editor in Xcode 4 via View Editor Assistant. You can customize the layout of the Assistant Editor with one of the selections available under View Assistant Layout. The Assistant Editor will automatically display the ViewBasedWithCocos2DViewController.h file.
In the Interface Builder view, select the On/Off Switch and right-click it. It doesn’t matter if you select it from the list or by clicking its view. The context menu that opens shows a list of events that the control sends. Click the circle next to the Value Changed
event and drag it over to the Assistant Editor. You’ll notice that it will highlight a line with the label Insert Action
if you drag it somewhere below the class @interface
brackets and above the @end
statement. That’s where you should drop the arrow to make the connection. A pop-up view will show up and ask you for the name of the event. I decided to call mine switchChanged
. You can leave all the other settings at their default values and then make the connection by clicking the Connect
button in the pop-up view.
Interface Builder has automatically created the necessary code for you in order to receive the particular event that you just connected. There’s new code both in the interface and implementation sections of the ViewBasedAppWithCocos2DViewController
class. Before you review the code changes, you should also connect the same Value Changed
event of the Segmented Control and name it sceneChanged
.
This concludes the user interface design part of this project. Now let’s move on to hooking up cocos2d.
If you followed the user interface design part, you’ll find two empty methods called switchChanged
and sceneChanged
in the ViewBasedAppWithCocos2DViewController.m class implementation file, next to some other boilerplate code that was added by the View-based Application template.
The first step is to get cocos2d up and running. One thing that’s comfortable when working with cocos2d projects created from one of the cocos2d templates is that you rarely need to add a header file to any of your classes. This is because the cocos2d.h file is imported in the project’s prefix header. Since this is not the case in the View-based Application template, open the ViewBasedAppWithCocos2D-Prefix.pch file in the Supporting Files group and add the cocos2d header:
#import <Availability.h>
#ifndef __IPHONE_3_0
#warning "This project uses features only available in iPhone SDK 3.0 and later."
#endif
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import "cocos2d.h"
#endif
Next you need to add a cocos2d scene class to the project. Using the File Add Files to “ViewBasedAppWithCocos2D”… menu item, browse into the cocos2d project you created earlier and then locate and select both the header and implementation files of the HelloWorldLayer
class. Make sure the Copy items into destination group’s folder (if needed) check box is checked. Alternatively, you can also create a new cocos2d scene class from a cocos2d file template or manually. I’ll be using the HelloWorldLayer scene throughout this example.
Import the HelloWorldLayer
class in the ViewBasedAppWithCocos2DViewController.m file so that we can run it as our main cocos2d scene:
#import "HelloWorldLayer.h"
All that is left now is to actually start up cocos2d and connect it with the EAGLView
. The switchChanged
method in Listing 15–7 contains all the start-up code that is needed.
Listing 15–7. Setting Up the Director and Displaying the First cocos2d Scene
- (IBAction)switchChanged:(id)sender
{
CCDirector* director = [CCDirector sharedDirector];
if ([CCDirector setDirectorType:kCCDirectorTypeDisplayLink] == NO)
{
[CCDirector setDirectorType:kCCDirectorTypeDefault];
}
[director setAnimationInterval:1.0/60];
//[director setDisplayFPS:YES];
NSArray* subviews = self.view.subviews;
for (int i = 0; i < [subviews count]; i++)
{
UIView* subview = [subviews objectAtIndex:i];
if ([subview isKindOfClass:[EAGLView class]])
{
subview.hidden = NO;
[director setOpenGLView:(EAGLView*)subview];
[director runWithScene:[HelloWorldLayer scene]];
break;
}
}
}
That’s surprisingly little code to get cocos2d running. As usual, we pick the best possible director type, we set the animation interval, and the rest is just connecting the director with the EAGLView
. The latter part simply goes over the list of subviews in the view controller’s view to find one that is subclassed from EAGLView
. Once the right view is found, it’s made visible and assigned to the director via setOpenGLView
. Directly after that you can make the call runWithScene
, and that’s it.
I also break out of the loop once a scene was run as a precaution because if this loop would continue, it might find another EAGLView
, and that would result in a crash. In fact, you’ll notice if you run the app that you can change the switch button’s state only once. The second time, the app crashes.
You’ll notice that the setDisplayFPS
command is commented out in this example. To enable the fps display, you will also have to add the fps_images.png file from any cocos2d project to your project via the File Add Files To … command. Otherwise, the framerate counter will not be displayed.
You can easily start, stop and restart the cocos2d engine at any time. You just need to determine the current state of the engine. It might never have been started before, it might be running, or it might be stopped.
There’s no property in cocos2d’s CCDirector
class, but you can infer the status in other ways. I prepared this example project so that this status can be detected, and depending on your project’s requirements, you might have to use other mechanisms, such as keeping track of the cocos2d running status through global variables.
If the director’s openGLView
property is nil
, you know that it has never been started before. And in this particular project, the fact that the openGLView is hidden tells me that cocos2d has been suspended and could be restarted. In addition to that, the switch button’s state is also taken into account. The code for the switchChanged
method has changed as follows and is shown in Listing 15–8.
Listing 15–8. Starting, Suspending, and Stopping cocos2d
- (IBAction)switchChanged:(id)sender
{
UISwitch* switchButton = (UISwitch*)sender;
CCDirector* director = [CCDirector sharedDirector];
if (switchButton.on)
{
if (director.openGLView == nil)
{
if ([CCDirector setDirectorType:kCCDirectorTypeDisplayLink] == NO)
{
[CCDirector setDirectorType:kCCDirectorTypeDefault];
}
[director setAnimationInterval:1.0/60];
//[director setDisplayFPS:YES];
NSArray* subviews = self.view.subviews;
for (int i = 0; i < [subviews count]; i++)
{
UIView* subview = [subviews objectAtIndex:i];
if ([subview isKindOfClass:[EAGLView class]])
{
[director setOpenGLView:(EAGLView*)subview];
[director runWithScene:[HelloWorldLayer scene]];
break;
}
}
}
else
{
[director startAnimation];
}
director.openGLView.hidden = NO;
}
else
{
director.openGLView.hidden = YES;
[director stopAnimation];
}
}
Notice that the director methods startAnimation
and stopAnimation
are used to restart and stop cocos2d. Just for the very first time, you need to call runWithScene
. But if you wanted to run a different scene each time cocos2d is restarted, you should call replaceScene
directly after the call to startAnimation
. The runWithScene
method can be called only once during the lifetime of the application and must not be used again.
Technically, the stopAnimation
method only stops cocos2d from refreshing its view. Unless the view is hidden
or obstructed by another view, the last frame cocos2d has rendered will remain as a static image in the EAGLView
. That’s why hiding the EAGLView
is a good idea. Calling stopAnimation
is sometimes necessary to ensure that certain UIKit views are responsive and animate smoothly, in particular all views derived from UIScrollView
. It is good practice to call stopAnimation
whenever you display an (almost) full-screen UIKit view, to conserve performance for the foreground view as well as conserve battery power. Once the foreground view is dismissed, you call startAnimation
again, and the cocos2d view and director continue where they were.
TIP: If you want to see how this app behaves with autorotation, you have to make a small change to the ViewBasedAppWithCocos2DViewController
class. Simply return YES
from the shouldAutorotateToInterfaceOrientation
method to enable rotation to all orientations. Although your app doesn’t support it well (it is not designed for landscape orientation), it serves to show that the cocos2d view will be correctly autorotated even without its RootViewController
class.
The last step to complete this project is to use the Segmented Control’s buttons to change scenes in the cocos2d view. In Listing 15–9, taken from the ViewBasedAppWithCocos2DViewController
class, the code that was added to the sceneChanged
method is shown.
Listing 15–9. Changing Scenes Whenever You Press a UIKit Button
- (IBAction)sceneChanged:(id)sender
{
CCDirector* director = [CCDirector sharedDirector];
if (director.openGLView == nil || director.openGLView.hidden)
{
return;
}
UISegmentedControl* sceneChanger = (UISegmentedControl*)sender;
int selection = sceneChanger.selectedSegmentIndex;
CCScene* helloScene = [HelloWorldLayer scene];
CCScene* transScene = nil;
if (selection == 0)
{
transScene = [CCTransitionSlideInR transitionWithDuration:1 scene:helloScene];
}
else if (selection == 1)
{
transScene = [CCTransitionPageTurn transitionWithDuration:1 scene:helloScene];
}
else
{
transScene = [CCTransitionShrinkGrow transitionWithDuration:1 scene:helloScene];
}
[director replaceScene:transScene];
}
Since the user can press the Segmented Control buttons at any time, even before the cocos2d view is initialized, the first thing this method does is to check that the director.openGLView
exists and is not hidden. Otherwise, the remaining code could crash the app.
The sender
parameter is always the control that triggered the event. Here I assume that it’s a UISegmentedControl
. If you ever changed that, you would have to change the control’s class here as well. Via the selectedSegmentIndex
, you get the index of the currently selected button, which is then used to decide which transition to use for the new scene. I’m simply creating a new instance of the same HelloWorldLayer
class; of course, you can also use different scene classes for each button if you want. At last, the transScene
is used with the director
method replaceScene
to actually change the scene to the new one.
NOTE: The cocos2d transitions will actonly on the cocos2d view and its nodes. UIKit views will be unaffected by the cocos2d transitions. But you can use UIView
animations and transitions on the cocos2d view. You can learn more about UIView
animations here: http://developer.apple.com/library/ios/#documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/AnimatingViews/AnimatingViews.html
.
I spiced up my HelloWorldLayer
scene with some additional cocos2d labels in the background and labels for the buttons. You’ll find these code changes to the HelloWorldLayer
class in the ViewBasedAppWithCocos2D project. The result looks something like Figure 15–13.
Figure 15–13. A cocos2d view in a view-based application
This chapter provided you with everything you need to know to successfully and painlessly mix cocos2d with regular UIKit views. You now have the option to choose how much UIKit you want in your cocos2d app and when, where, and how you would like your cocos2d view in your UIKit app.
The trickiest aspects were making the cocos2d view transparent in order to allow UIKit views in the background as well as having a hit test method perform hit tests on cocos2d node in an attempt to allow all views to receive input, whether UIKit or cocos2d and regardless of where they are in the view hierarchy. And then there was autorotation, which cocos2d can do in two ways, but only one way using the RootViewController
allows you to rotate UIKit views correctly.
Adding cocos2d to a UIKit app also proved to be fairly simple, even if you need to turn the cocos2d view on and off only at specific times. You may have also taken away that the cocos2d view doesn’t need to be full-screen at all but can be any size, or even resized while the app is running.
But you also learned that mixing cocos2d and UIKit views is not without drawbacks, specifically performance-wise. Keep a watchful eye on your app’s performance by testing it regularly on a device, particularly on first- and second-generation devices.