Chapter 15

Cocos2d with UIKit Views

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.

What Is Cocoa Touch?

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

Using Cocoa Touch and cocos2d Together

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.

Why Mix Cocoa Touch with cocos2d?

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.

Limitations of Mixing Cocoa Touch with cocos2d

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.

How Is Cocoa Touch Different from cocos2d?

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.

The Model-View-Controller Pattern

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.

Cocos2d’s View Uses OpenGL ES

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.

Alert: Your First UIKit View in cocos2d

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 image New image 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*)alertViewimage
    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,image
        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.

images

Figure 15–1. A UIAlertView is displayed over the cocos2d view.

Embedding UIKit Views in a cocos2d App

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.

Adding Views in Front of the cocos2d View

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:image
        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:image
    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.

Skinning the UITextField with a UIImage

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:image
        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.

images

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 UIViews 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.

Adding Views Behind the cocos2d View

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.

Improving the View Hierarchy

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 UIViews before and after the cocos2d view. Moreover, this gives you the ability to change the view order at runtime.

images

Figure 15–3. An illustration of the change made to the view hierarchy by introducing the dummy UIView

Moving the UITextFields to the Background

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?

Making the cocos2d View Transparent

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:

// make the cocos2d view transparent
glClearColor(0.0, 0.0, 0.0, 0.0);
glView.opaque = NO;

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!

Properly Propagating Touch Events via Hit Tests

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.

Sandwiching the cocos2d View

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:image
    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!

images

Figure 15–4. UIKit views on top and behind the cocos2d view, with input enabled for all of them

Adding Views Designed with Interface Builder

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 image New image 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.

images

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.

images

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 image Utilities image 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 image Utilities image 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.

images

Figure 15–7. The resulting project shows the MyView.xib file designed with Interface Builder in the bottom half.

Orientation Course on Autorotation

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:image
    (UIInterfaceOrientation)interfaceOrientation
{
#if GAME_AUTOROTATION == kGameAutorotationNone

    return (interfaceOrientation == UIInterfaceOrientationPortrait);

#elif GAME_AUTOROTATION == kGameAutorotationCCDirector

    if (interfaceOrientation == UIInterfaceOrientationLandscapeLeft)
    {
        [[CCDirector sharedDirector]image
            setDeviceOrientation:kCCDeviceOrientationLandscapeRight];
    }
    else if (interfaceOrientation == UIInterfaceOrientationLandscapeRight)
    {
        [[CCDirector sharedDirector]image
            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]image
        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) ||image
    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 ||image
        toInterfaceOrientation == UIInterfaceOrientationPortraitUpsideDown)
    {
        rect = screenRect;
    }
    else if (toInterfaceOrientation == UIInterfaceOrientationLandscapeLeft ||image
        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.

Embedding the cocos2d View in Cocoa Touch Apps

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.

Creating a View-Based Application Project with cocos2d

Fire up your Xcode 4. Create a new project via File image New image 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.

images

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.

images

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 image New image 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 image Navigators image 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.

images

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.

images

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:

  • AudioToolbox.framework
  • AVFoundation.framework
  • libz.dylib
  • OpenAL.framework
  • OpenGLES.framework
  • QuartzCore.framework

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!

Designing the User Interface of the Hybrid App

Select the ViewBasedAppWithCocos2DViewController.xib file in the Project Navigator to see the Interface Builder view. Using the Object Library (View image Utilities image 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:

  • View
  • Switch
  • Segmented Control

Now select the newly added View object and switch to the Identity Inspector (View image Utilities image 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.

images

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 image Utilities image 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 image Editor image Assistant. You can customize the layout of the Assistant Editor with one of the selections available under View image 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.

Start Your cocos2d Engine

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 image 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 image Add Files To … command. Otherwise, the framerate counter will not be displayed.

Stop the cocos2d Engine and Restart It

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.

Changing Scenes

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.

images

Figure 15–13. A cocos2d view in a view-based application

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.37.196