in General

“Almost” bulletproof cocos2d modal alerts and common layers

I’ve been working on a cocos2d game for some time now, its not an arcade game; its more of a solitaire Tycoon trading game as I felt this would be good enough to get me started at least.

Working with cocos2d has been very problematic for me. Things that should be simple take bucket loads of code just to get working.

One of the main things frustrating me is the node system will only allow you add children once (depending of course on context).

In my game I have a HUD (heads-up display) which I want to share across multiple scenes.

Here is a diagram;

What’s happening here is that I have two scenes and I want to share my HUD across both of them.

The HUD also has a menu in it (for example a Settings button) that will launch a modal pop-up dialog.

The way I’ve done it up to now is to use a BaseScene concept;


@implementation BaseScene

- (id)init
{
self = [super init];
if (self) {
NSLog(@"HUD Scene");
[self addChild:[BaseLayer node] z:0 tag:1];
}
return self;
}
@end

@implementation BaseLayer
@synthesize currentNode, thisLayer;
@synthesize hud;

-(id) init
{
if ((self =[super init]))
{
self.hud = [HeaderHUDLayer node];

if (self.currentNode == nil)
{
self.currentNode = [GamePlayLayer node];
[self changeNodeTo:self.currentNode];
}
[self addChild:self.hud z:TopZLayer tag:kHUDTag];

[[NSNotificationCenter defaultCenter] addObserver:self selector: @selector(changeHUDSceneObserver:) name: @"changeHUDScene" object: nil];
} // end if
return self;
}

- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super dealloc];
}

-(void) cleanup
{
[super cleanup];

// deregister as observer
[[NSNotificationCenter defaultCenter] removeObserver:self];
}

-(void) changeNodeTo:(CCNode *)thisNode
{
NSLog(@"changeNodeTo...");
[self removeChild:self.currentNode cleanup:YES];
[self addChild:thisNode z:0];
self.currentNode = thisNode;
}

-(void) changeHUDSceneObserver:(NSNotification *)notification
{
NSLog(@"changeHUDSceneObserver = %@", notification.name);

if ([notification.name isEqualToString:@"changeHUDScene"]==YES) {
if ([notification object]!=nil) {
[self changeNodeTo:[notification object]];
}
}
}

You can see here that I have:

  1. BaseScene
  2. A HUD included as a child of BaseScene
  3. All other subsequent layers are changed via NSNotification

Whilst this “worked”, it caused a lot of unforeseen problems.

Unforeseen problems

  1. I had NSNotifications everywhere and the whole project was becoming very unmanageable and messy
  2. I had a problem where Modal alert pop-ups were appearing underneath the HUD, and the modal alert did not disable touches on the HUD layer
  3. Modal alerts did not stop interactions on CCScrollLayers, or you could launch multiple modal alerts with lots of clicks

The solution I’ve come up with is to use class forwarding instances and use brute-force to stop all the interactions.

Class forwarding instances

I don’t know if its actually called this, but it sounds about right.

In the open-source game CastleHassle, the author sends “instances” back to the receiving layer and can pass the instance around; not only this he can call methods within the receiving class.

In summary, it means doing this:


@implementation MainMenuScene
static MainMenuScene *instance = nil;

+(MainMenuScene *) instance {
if(instance == nil) {
instance = [[MainMenuScene alloc] init];
}

return instance;
}

+(void) resetInstance {
[instance release];
instance = nil;
}

// This removes nodes I send to the instance and adds
// new things.
+(void) changeNodeTo:(CCNode *)node
{
MainMenuScene *main = [MainMenuScene instance];
[main removeAllChildrenWithCleanup:YES];
//[main removeChild:self cleanup:YES];
[main addChild:node];
}

This returns an instance and then you can use it later on.

In the HUD he has a controller, HUDActionController.


// Hud.mm
-(id) init {
if( (self=[super init]) ) {

[[HUDActionController instance] setHud:self];

[self drawHeaderLayer];
//[self drawFooterLayer];

} // end if
return self;
}

// Doesn't actually make it visible, just here for example
-(void) toggleHUDLayerVisible:(BOOL)yn
{
NSLog(@"toggleVisible: %d", yn);
[self scheduleUpdate];
}

-(void) update:(ccTime)delta
{
// To be safe
[self unscheduleAllSelectors];
}

The HUDActionController acts like a MVC (not strict MVC) deciding what to launch.


// HUDActionController.mm
@implementation HUDActionController
@synthesize hud;

static HUDActionController* instance = nil;

+(HUDActionController *) instance {
if(instance == nil) {
instance = [HUDActionController alloc];
[instance init];
}

return instance;
}

-(id) init {
if( (self=[super init]) ) {
NSLog(@"HUDActionController");
}
return self;
}

-(void) goToMainMenu
{
// MainMenu needs to be included though
[MainMenuScene resetInstance];
[[CCDirector sharedDirector] replaceScene: [MainMenuScene instance]];
}

-(void) toggleHUDLayerVisible:(BOOL)yn {
NSLog(@"toggleVisibility of Hud = %d", yn);
[self.hud toggleHUDLayerVisible:yn];
}

So, I could do:


[[HUDActionController instance] toggleHUDLayerVisible:YES];

Yes, it probably isn’t proper design; like observers or factory methods but its more of a hack really then a proper scientific solution.

So, using this concept, I can create a HUD on multiple layers and launch things on referencing classes using forwarders.

Book solution

In the book “Learn cocos2d Game Development with iOS 5” by Apress at around Chapter 5, the author describes a similar solution using “MultiLayerScene”.

Quote,

Simply put, the multiLayerSceneInstance is a static global variable that will hold the current MultiLayerScene object during its lifetime. The static keyword denotes that the multiLayerSceneInstance variable is accessible only within the implementation file it is defined in. At the same time, it is not an instance variable; it lives outside the scope of any class. That’s why it is defined outside any method, and it can be accessed in class methods like sharedLayer.

The reason for this semi-singleton is that you’ll be using several layers, each with its own child nodes, but you still need to somehow access the main layer. It’s a very comfortable way to give access to the main layer to other layers and nodes of the current scene.

I’ve not used the MultiLayerScene solution, but it seems just as viable as my solution.

I will post code below at the bottom to give you an example.

Now onto the next problem — ModalAlerts.

Modal alerts

One of the biggest stumbling blocks I had were modal dialog pop-ups or “modal alerts” as I will call them.

I’ve been able to work with RombosBlog’s modal alert;

http://rombosblog.wordpress.com/2012/02/28/modal-alerts-for-cocos2d/

It uses blocks to handle callbacks, and is very easy to use.

I can have a layer call up a modal alert, get a callback and handle the rest my own way.

However, it came with its own issues I found very frustrating.

Sometimes, rarely I might add, the modal alert did not stop interaction underneath the CoverLayer (a slightly dimmed CCLayerColor which holds the modal alert).

This was very evident on CCScrollLayers. You could click on, move and interact with CCScrollLayers when the modal alert was being displayed.

The other problem I had with the modal alert if you could click on a CCMenuItem (or a button) very fast you can spawn multiple occurrences of the ModalAlert.

I found this very frustrating.

At first I used this:


// Disabled/Enable layers
-(void) MenuStatus:(BOOL)_enable Node:(id)_node
{
for (id result in ((CCNode *)_node).children)
{
if ([result isKindOfClass:[CCMenu class]]) {
for (id result1 in ((CCMenu *)result).children) {
if ([result1 isKindOfClass:[CCMenuItem class]]) {
((CCMenuItem *)result1).isEnabled = _enable;
}
} // next
}
} // next
}

However, it did not stop interactions on CCScrollLayers; worse still I had to put it on every layer I wanted to handle this.

The way around this was to use a BaseLayer (CCLayer) and make all my game layers inherit from this:


@interface PlayerSetupLayer : BaseLayer // BaseLayer is a CCLayer

Whilst this worked; it only worked it didn’t stop interactions with CCScrollLayers.

To resolve this problem, I ended up using a mess of NSNotifications up the chain of command (CCNodes) to change or lock layers and to display models.

Not only was this bad, its very hard to maintain. It was a mess!

So what’s the solution?

Well so far, I’ve got a “working” solution that is “almost” bulletproof. It is not perfect, though.

The way I’ve done it is to combine the sharing of HUDs across multiple scenes and use the HUDActionController to decide what to do.

For the modal alert, I used a rather hacky solution; just go through every node and disable/enable it so long as its not the CoverLayer class.

ie,


/**
* This requires the following in your cocos2d project
* CCScrollLayer extension
* @url: https://github.com/cocos2d/cocos2d-iphone-extensions/
* and
* ModalAlert - Customizable popup dialogs/alerts for Cocos2D
* @url: http://rombosblog.wordpress.com/2012/02/28/modal-alerts-for-cocos2d/
*/

// Disabled/Enable layers
-(void) MenuStatus:(BOOL)_enable Node:(id)_node
{
LOG_METHOD;
if (_enable == YES) NSLog(@"MenuStatus.Enable = YES");
if (_enable == NO) NSLog(@"MenuStatus.Enable = NO");

for (id result in ((CCNode *)_node).children)
{
NSLog(@"Node result = %@", [result class]);

if ([result isKindOfClass:[CoverLayer class]])
{
// Do nothing

} else {

// Scrolllayer
if ([result isKindOfClass:[CCScrollLayer class]]) {
NSLog(@"Found CCScrollLayer...");
((CCScrollLayer *)result).isTouchEnabled = _enable;
for (id result1 in ((CCScrollLayer *)result).children) {
NSLog(@" result1 class = %@", [result1 class]);
if ([result1 isKindOfClass:[CCLayer class]]) {
((CCLayer *)result1).isTouchEnabled = _enable;
}
for (id result2 in ((CCLayer *)result1).children) {
NSLog(@" child found: %@", [result2 class]);
if ([result2 isKindOfClass:[CCMenu class]]) {
((CCMenu *)result2).isTouchEnabled = _enable;
} // end if
}
} // next
} // end if

// Layers
if ([result isKindOfClass:[CCLayer class]]) {
NSLog(@"Found CCLayer -- %@", [CCLayer class]);

// Disable CCLayer and any children?
((CCLayer *)result).isTouchEnabled = _enable;
for (id result2 in ((CCLayer *)result).children) {
NSLog(@"child found: %@", [result2 class]);
if ([result2 isKindOfClass:[CCMenu class]]) {
((CCMenu *)result2).isTouchEnabled = _enable;
} // end if
} // next

} // end if

} // end if

} // next
}

Summary

In summary, I:

  • I have a base scene with multiple layer children under it (MainMenuScene -> Player Setup Layer, etc)
  • I use instances and class forwarding to change the child of MainMenuScene
  • I include a HUD, which is a CCLayer, onto the layers I need them on
  • The HUD uses an HUDActionController to decide where to go, and it also locks/unlocks things, including CCScrollLayers
  • The HUD will launch a modal pop-up dialog fine now
  • Other pop-up dialogs can appear higher than the HUD modal so long as the Z-Index is higher without any issue

In numerical format, it sorta looks like this

  1. Main Menu Scene has a Main Menu Layer.
    1. Player Setup Layer replaces child of Main Menu Layer
    2. Difficulty Setup Layer replaces child of Main Menu Layer
    3. Game confirmation Setup Layer replaces child of Main Menu Layer
  2. Game Scene has a Game Layer
    1. Game Layer has a HUD child node in it
      1. HUD uses HUDActionController to handle routing and disable/enable touches
      2. If I launch a modal it will appear on top of HUD if z-index is higher, it will also disable touches under it
      3. If I launch a modal on the HUD it will disable everything except the modal itself and her children

It isn’t perfect, but I’ve found it works for me. So far….