Modal Alerts for Cocos2D

Cocos2D is insanely great for game development, and a lot of otherwise complex or tedious gaming tasks are almost no-brainers here. The one thing, however, that I found less straightforward, is bringing up small alerts to ask for some user confirmation, while at the same time blocking the rest of the application.
There are actually tons of bits and pieces around that solve the one or the other part of it, but no one-shot solution as I wanted it to be. What I was looking for is:
- Small alert-box with custom design fitting to the overall game look-and-feel
- Alike support for iPad and iPhone, incl. retina
- Predefined “Ask”, “Confirm” and “Tell” type of alerts that can be easily called from within the code
- Modal operation, i.e. lock and dim the game scene and ideally have a no-hassle modal kind of control-flow within the calling code without the need for additional callbacks that make the code hard to read from there on
- Fire-and-forget operation without need for any further cleanup
- Nice animations
So what I finally came up with is actually a bit more powerful and complex than what I want to present here. But this stripped-down version is a great start to do your own further customizations, and it is fully functional and can be integrated right away. You just ask for the dialog to be brought up with your own message, and you’re done. Code that should be executed when pressing a button is written inline as a block, thus being kind of a modal control-flow schema. And if you want code to be executed right away, just put it directly after the statement itself. Asking the user for a simple confirmation is as easy as this:
[ModalAlert Ask: @"Do you want to cancel the game?" onLayer:self // this is usually the current scene layer yesBlock: ^{ // code that should be executed on "yes" goes here ... } noBlock: ^{ // code that should be executed on "no" goes here ... }]; // control flow proceeds here right away (non-modal) ...
That’s all. Everything is encapsulated into simple class-functions and requires no allocations or anything on the caller side. There is a predefined call for a Yes/No question, an OK/Cancel confirmation and a Tell-function that is just to be confirmed with an OK-button.
In principle a semi-transparent black color layer is created that covers the whole current scene and swallows all touches, except those that should go to the dialog sprite and the buttons on it. The dialogbox itself and the button are images that fit for your purpose, and you can design them any way you like (color and size). For this sample, I provided three different sizes for iPhone retina and non-retina, and an extra one for the iPad. The non-retina size would be too small for the iPad, and HD resolution too large and would look clumsy. So from the HD retina resolution, the iPad is sized 70% of it, and the SD non-retina is as usual 50%. If you change the size you might also need to tweak some of the sizings, however they are relative to the dialog box size and should fit at least as long as you approximately retain the height/width relation.
Beside the fact that you now have a fully functional base to start from for the ultimate and universal alerting library, you can take away some interesting things from the code, if you are new to Cocos2D or iPhone programming in general, like animations and block programming (which BTW is only available starting with iOS 4).
Where could you go from here? Include localization for the predefined buttons, provide more formats, provide a function where you can also customize the button texts, center the message text vertically, draw all boxes dynamically using OpenGL (instead of having to provide images), include a nice image in the dialog box to make it more friendly, etc…there is tons you could elaborate on. But probably you won’t even need much more.
So here is the coding of the ModalAlert class. I leave out the *.h file here, because it is pretty unspectacular. All the code files and the required images for the sample can be downloaded as ZIP file to get you jump-started.
// ModalAlert.m // // Created by Hans-Juergen Richstein // Copyright 2012 ROMBOS. All rights reserved. #import "ModalAlert.h" #import "cocos2d.h" // constants #define kDialogTag 1234 #define kAnimationTime 0.4f #define kDialogImg @"dialogBox.png" #define kButtonImg @"dialogButton.png" #define kFontName @"MarkerFelt-Thin" // class that implements a black colored layer that will cover the whole screen // and eats all touches except within the dialog box child @interface CoverLayer : CCLayerColor { } @end @implementation CoverLayer - (id)init { self = [super init]; if (self) { [self initWithColor:ccc4(0,0,0,0) width:[CCDirector sharedDirector].winSize.width height:[CCDirector sharedDirector].winSize.height]; self.isTouchEnabled = YES; } return self; } - (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event { CGPoint touchLocation = [self convertTouchToNodeSpace: touch]; CCNode *dialogBox = [self getChildByTag: kDialogTag]; // eat all touches outside of dialog box return !CGRectContainsPoint(dialogBox.boundingBox, touchLocation); } - (void) registerWithTouchDispatcher { [[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:INT_MIN+1 swallowsTouches:YES]; } @end // the actual ModalAlert implemenation class @implementation ModalAlert // this is called to gracefully close the alert, cleanup everything // and call the according block belonging to the pressed button + (void) CloseAlert: (CCSprite*) alertDialog onCoverLayer: (CCLayer*) coverLayer executingBlock: (void(^)())block { // shrink dialog box [alertDialog runAction:[CCScaleTo actionWithDuration:kAnimationTime scale:0]]; // in parallel, fadeout and remove cover layer and execute block // (note: you can't use CCFadeOut since we don't start at opacity 1!) [coverLayer runAction:[CCSequence actions: [CCFadeTo actionWithDuration:kAnimationTime opacity:0], [CCCallBlock actionWithBlock:^{ [coverLayer removeFromParentAndCleanup:YES]; if (block) block(); }], nil]]; } + (void) ShowAlert: (NSString*) message onLayer: (CCLayer *) layer withOpt1: (NSString*) opt1 withOpt1Block: (void(^)())opt1Block andOpt2: (NSString*) opt2 withOpt2Block: (void(^)())opt2Block { // create the cover layer that "hides" the current application CCLayerColor *coverLayer = [CoverLayer new]; [layer addChild:coverLayer z:INT_MAX]; // put to the very top to block application touches [coverLayer runAction:[CCFadeTo actionWithDuration:kAnimationTime opacity:80]]; // smooth fade-in to dim with semi-transparency // open the dialog CCSprite *dialog = [CCSprite spriteWithFile:kDialogImg]; dialog.tag = kDialogTag; dialog.position = ccp(coverLayer.contentSize.width/2, coverLayer.contentSize.height/2); dialog.opacity = 220; // make it a bit transparent for a cooler look // add the alert text CGSize msgSize = CGSizeMake(dialog.contentSize.width * 0.9, dialog.contentSize.height * 0.55); float fontSize = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)?42:30; CCLabelTTF *dialogMsg = [CCLabelTTF labelWithString:message dimensions: msgSize alignment:UITextAlignmentCenter fontName:kFontName fontSize:fontSize]; dialogMsg.position = ccp(dialog.contentSize.width/2, dialog.contentSize.height * 0.6); dialogMsg.color = ccBLACK; [dialog addChild: dialogMsg]; // add one or two buttons, as needed CCMenuItemSprite *opt1Button = [CCMenuItemSprite itemFromNormalSprite:[CCSprite spriteWithFile: kButtonImg] selectedSprite:[CCSprite spriteWithFile: kButtonImg] block:^(id sender){ // close alert and call opt1block when first button is pressed [self CloseAlert:dialog onCoverLayer: coverLayer executingBlock:opt1Block]; } ]; opt1Button.position = ccp(dialog.textureRect.size.width * (opt2 ? 0.27f:0.5f), opt1Button.contentSize.height * 0.8f); CCLabelTTF *opt1Label = [CCLabelTTF labelWithString:opt1 dimensions:opt1Button.contentSize alignment:UITextAlignmentCenter fontName:kFontName fontSize:fontSize]; opt1Label.anchorPoint = ccp(0, 0.1); opt1Label.color = ccBLACK; [opt1Button addChild: opt1Label]; // create second button, if requested CCMenuItemSprite *opt2Button = nil; if (opt2) { opt2Button = [CCMenuItemSprite itemFromNormalSprite:[CCSprite spriteWithFile: kButtonImg] selectedSprite:[CCSprite spriteWithFile: kButtonImg] block:^(id sender){ // close alert and call opt2block when second button is pressed [self CloseAlert:dialog onCoverLayer: coverLayer executingBlock:opt2Block]; } ]; opt2Button.position = ccp(dialog.textureRect.size.width * 0.73f, opt1Button.contentSize.height * 0.8f); CCLabelTTF *opt2Label = [CCLabelTTF labelWithString:opt2 dimensions:opt2Button.contentSize alignment:UITextAlignmentCenter fontName:kFontName fontSize:fontSize]; opt2Label.anchorPoint = ccp(0, 0.1); opt2Label.color = ccBLACK; [opt2Button addChild: opt2Label]; } // add the menu to the dialog CCMenu *menu = [CCMenu menuWithItems:opt1Button, opt2Button, nil]; menu.position = CGPointZero; [dialog addChild:menu]; [coverLayer addChild:dialog]; // open the dialog with a nice popup-effect dialog.scale = 0; [dialog runAction:[CCEaseBackOut actionWithAction:[CCScaleTo actionWithDuration:kAnimationTime scale:1.0]]]; } + (void) Ask: (NSString *) question onLayer: (CCLayer *) layer yesBlock: (void(^)())yesBlock noBlock: (void(^)())noBlock { [self ShowAlert:question onLayer:layer withOpt1:@"Yes" withOpt1Block:yesBlock andOpt2:@"No" withOpt2Block:noBlock]; } + (void) Confirm: (NSString *) question onLayer: (CCLayer *) layer okBlock: (void(^)())okBlock cancelBlock: (void(^)())cancelBlock { [self ShowAlert:question onLayer:layer withOpt1:@"Ok" withOpt1Block: okBlock andOpt2:@"Cancel" withOpt2Block:cancelBlock]; } + (void) Tell: (NSString *) statement onLayer: (CCLayer *) layer okBlock: (void(^)())okBlock { [self ShowAlert:statement onLayer:layer withOpt1:@"Ok" withOpt1Block: okBlock andOpt2:nil withOpt2Block:nil]; } @end
How to call Modal Alert from HelloWordLayer class ???
Enter this code in the HelloWorld layer implementation in HelloWorld.m:
- (void)onEnterTransitionDidFinish {
[ModalAlert Tell:@"TEST" onLayer:self okBlock:nil];
}
It makes sure that the alert actions are only started once the layer is ready and shown.
If you use the Cocos2D HelloWorld project, you might have to manually add the source code explicitly to the HelloWorldSample Compile Sources build phase, as well as the graphics under Copy Bundle Resources.
Note that in the above sample code there is no block to execute when the user presses OK (for simplicity of this sample).
I’m still learning from you, while I’m trying to achieve my goals. I definitely love reading everything that is posted on your blog.Keep the stories coming. I loved it!
ZIP file is now at https://rombos.s3.amazonaws.com/cocos2dAlerts.zip — I guess Amazon changed it’s URL scheme.
I think this is also the link I am using anyway (except https, but http works also)
Thanks for posting this. It works. The Ask Confirm and Tell features are a nice added bonus. I appreciate your kindness with the free code…… It is well written.
ANDY
Hail to Rombos. You are on my Life Savier list 🙂
I have a little problem. I have one regular class which is subclass of NSObject. When I call ModalAlert like this, [ModalAlert Tell=@”Test” OnLayer:????? okBlock:nil];
I can not use self because this class is not a subclass of cclayer. How can I call ModalAlert from that class. Is there any way I can send CCLayer Object.
I have a little problem. I have one regular class which is subclass of NSObject. When I call ModalAlert like this, [ModalAlert Tell=@”Test” OnLaye:????? okBlock:nil];
I can not use self because this class is not a subclass of cclayer. How can I call ModalAlert from that class. Is there any way I can send CCLayer Object.
You have to provide a layer class object of the current scene on which the blocking layer and the dialog should be instantiated. Otherwise it can’t be displayed by the Cocos framework. It doesn’t matter whether you call it from any other class, but you need to provide an instance of the current scene layer from somewhere. If you don’t have direct access to it from your current object, you might try to look it up under the child-nodes of [CCDirector sharedDirector].runningScene, which is the current scene and the parent of all subnodes. Your current scene layer should be one of it, so you could use the tag property to mark it and then look it up here with getChildByTag.
Best, rombos
Wow, why I did not think that way at the first place? I was trying to send scene instead of Layer. Now it works perfect thank you very much.
Actually I was trying to implement it on inApp purchse. When user tap buy then it takes some time to give me an alert. If I change the scene in that period then it does not show me the alert. Is there any way to display alert regardless of the layer (display alert whatever layer is displaying not the layer I am sending using getChildByTag)
Thanks
How do I use a clipping node in combination with your modal alert? If I use a clipping node via the visit() method; the visit() method never gets visited.
Is there a way to get clipping node to work with your modal alert?
Thanks
Right now the alert is placed on an own color layer on top of everything else, so that it dims and blocks the scene below. A clipping node just works for its child nodes (not sure whether this maybe layers as well at all), so you should try to organize your nodes in such a way that everything, including all the modal alert nodes, are children of your clipping node.
I use the glsissors technique which is used on the visit method but the visit method never gets visited (as it were)
As said, you need to make sure that you have a proper nodetree structure. Visit will be called for sure if the layer is below the calling node. There is no magic involved in the used objects here, it’s all plain Cocos2D node stuff.
Please post an example of modal alert with clipping node because as I cannot get it to work. I’ve tried making the layer inherit from ClippingNode, tried init’d CN on the modal alert, but it doesn’t seem to do anything. I am not sure what I am doing wrong. Thanks
I’ve fixed it now! I used Viewport on github to solve the problem. Thanks. Link: http://pastebin.com/tWsEbxvJ
The modal alert (coverlayer) does not stop touches. I can press the menu buttons multiple times to launch a modal — how did you resolve this? thanks
Unless you changed something, I have no explanation for that. The cover layer has a touch dispatcher priority of “-INT_MIN+1”, a z-order of “INT_MAX”, covers the whole screen and “eats” all touches outside the actual dialog box. It is almost impossible that any other element below it gets further touches (unless you would have changed it’s priority even lower — which is then counter-intuitively higher for the touch dispatcher — to “-INT_MIN”. I left that option for very rare occasions where I really would need that, but normal menus have a priority of -128).
So: no clue. You need to explore that on your own. The code normally works as intended and blocks everything, but maybe you did something special…
One thing maybe: before the initialization of the whole thing is completed, you might still be able to touch the buttons below. This is something beyond the modal alert’s control, of course, so you need to make sure that you somehow “survive” the time until the cover layer is up and initialized.
I’ve reverted my adjustments back to your original version but I can still launch multiple modal alerts.
CCMenu *menu = [CCMenu menuWithItems:nil];
[menu alignItemsHorizontallyWithPadding:10.0f];
[menu setPosition:CGPointMake(50, 150)];
CCLabelTTF *txtNewGame = [CCLabelTTF labelWithString:@"New Game" fontName:@"Arial" fontSize:18];
[txtNewGame setAnchorPoint:CGPointMake(0, 0.5)];
CCLabelTTF *txtContinueGame = [CCLabelTTF labelWithString:@"Continue" fontName:@"Arial" fontSize:18];
[txtContinueGame setAnchorPoint:CGPointMake(0, 0.5)];
CCMenuItemLabel *lblNewGame = [CCMenuItemLabel itemWithLabel:txtNewGame target:self selector:@selector(menuButtonTapped:)];
CCMenuItemLabel *lblContinue = [CCMenuItemLabel itemWithLabel:txtContinueGame target:self selector:@selector(menuButtonTapped:)];
[menu addChild:lblNewGame];
[menu addChild:lblContinue];
[menu alignItemsVerticallyWithPadding:25.0];
[menu setPosition:CGPointMake(150, 200)];
[self addChild:menu];
- (void)menuButtonTapped:(id)sender
{
int tag = [sender tag];
NSLog(@"Menu Button Tapped -- %d", tag);
[ModalAlert Ask:@"Hello?" onLayer:self yesBlock:nil noBlock:nil];
}
The modal alert code in your zip file is unchanged, but I can still click the menu items.
You mean through the completely initialized open cover layer that dims the screen?
Sorry, this can not be. If that is the case, there is something badly wrong with your setup, but I can’t do your work here. This is really no rocket science and WILL WORK in any thinkable normal usage of this technique, since it uses no tricks or something. It is simple and straightforward usage of Cocos2d concepts.
If there is a project with a sample example this would be helpful. Perhaps I am doing this wrong?
I want to show this alert when my application running count is multiple of 2 , i have one condition:
if(player_count ==2){
} in this condition i want to show alert i m using in Appdelegate method can you please tell me how can i do this.
Thanks;
The alerts operate on top of a displayed scene, so you should only open them once at least your very first scene is in place and fired the “onEnterTransitionDidFinish” event. If you need to show an alert out of appDelegate already, use regular system alerts.
I solved it! I used a MenuStatus trick and code to iterate through all child nodes to disable the things I want, including CCScrollView!
I’m going to post the code very soon!
Great if this works for you!
In general, however, this does not prevent touches to get through and be processed by the lower layers. Not everything is done with menus or based on enabled/disabled nodes…
This is great code. The only thing is that it doesn’t eat the touches of buttons directly underneath the alert box.
I like your custom alert view code but I have been struggling with something, when I create the alert view it passes in the layer that is going to display the alert view right? Then afterwards I can no longer release the layer that has presented the alert view. Just wondering why this would be happening.
Hm…this would mean that the retain count is somehow incremented on the layer parameter, but I can’t confirm that this happens for me. Can you provide a bit more context, e.g. the piece of code how you call it?
I am calling it like so:
[ModalAlert Ask:@”Do you want to quit this game and return to the main menu?”
onLayer:self
yesBlock: ^ {
[[CCDirector sharedDirector] replaceScene:[MainMenu scene]];
}
noBlock:^{
}];
If this is really all, it should work w/o issues (I do it the same way, and the current scene is deallocated then properly). Only if you would have referred to self within the block you would suffer an over-retaining and would have to remedy this with a blockself local helper. But if this is not the case, I really have no clue from the information you provide.
Thanks, that is what I was. I was calling self.isTouchesEnabled in the no block. Could you explain to me why this happens inside of a block?
A block assures that references to self remain valid until the end of the block scope by additionally retaining self. If you discard the class within the block (e.g.implicitly by switching to another scene), self remains overretained and the scene won’t die.
Put this in front of calling ModalAlert:
__block typeof(self) blockself = self;
and then call within the block:
blockself.isTouchesEnabled = false;
This should do the job.
Thanks for the help!!!!!!!!
Hello Rombos,
Thank you for the Modal Alert code! I am having the same problem as Stephen. I am calling self inside the blocks of an Ask Modal Alert call and it is retaining the layer. I tried the blockself work around but it still is retaining the layer!
__block typeof(self) blockself = self;
[ModalAlert Ask: @”Great Job! Move on to the next lesson?”
onLayer:self
yesBlock:
^{
[blockself resetValues];
//[self resetValues];
}
noBlock:
^{
modal_display = false;
}];
I suppose “modal_display” is a member variable, so you need to declare that also as a property and access it via “blockself” only…
Great! I didn’t realize it was any references to the class… and not just ‘self’. Thank you Rombos! I love your class!
Hello.. Thanks for this.. I have a small problem.. In my scene, I have a UITableView in the center of the screen.. Whenever a row is selected, instead of using the AlertView, I tried using your modal alerts.. However it goes below the UITableView.. Is there a way I can fix this issue? Cheers..
I am afraid this is not possible. Cocos2D graphics is rendered into one dedicated OpenGL UIView. Whenever you want to mix that with iOS UIViews, they are opened in the context of the application root view (that hosts the OpenGL subview) and as such are always displayed on top of all OpenGL stuff. You can’t now put another OpenGL graphics (i.e. the modal alert) again on top of such a native UIView.
thank you
Okay, after trying several solutions, I worked out that using the touch priority you can set up a dialog to swallow all touches.
The original approach set the dialog to the most prioritised touch handler, but then used a bounding rect to let touches through to the dialog buttons.
The problem is if there are any other controls in the middle of the dialog, they also can get those touches.
The solution is to set the layer priority above normal menu items (but doesn’t have to be INT_MIN) and then swallow *all* touches. Then, for the buttons on the actual dialog, set them as even more important priority.
Relevant code (not complete):
// Ensure dialog background, which swallows all touches, is prioritised before normal menus (-128)
// Menus displayed on the dialog, then need to be prioritised before that.
#define kDialogSwallowTouchPriority -1024
#define kDialogMenuPriority -1032
– (void)addCloseMenu
{
CCSprite *close = [CCSprite spriteWithSpriteFrameName:@”closebutton.png”];
CCMenuItem *closeMenuItem = [CCMenuItemSprite itemWithNormalSprite:close selectedSprite:nil target:self selector:@selector(closeTapped:)];
closeMenuItem.anchorPoint = ccp( 1, 1 );
closeMenuItem.position = ccp( self.dialog.contentSize.width – 10, self.dialog.contentSize.height – 10 );
self.closeMenu = [CCMenu menuWithItems:closeMenuItem, nil];
self.closeMenu.anchorPoint = ccp ( 1, 1 );
self.closeMenu.position = CGPointZero;
self.closeMenu.touchPriority = kDialogMenuPriority;
[self.dialog addChild:self.closeMenu];
}
– (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
// Swallow all touches
return YES;
}
– (void) registerWithTouchDispatcher {
[[[CCDirector sharedDirector] touchDispatcher] addTargetedDelegate:self priority:kDialogSwallowTouchPriority swallowsTouches:YES];
}
The problem with not eating the touches lies with this line:
return !CGRectContainsPoint(dialogBox.boundingBox, touchLocation);
It eats all the touches except those within the bounding box of the entire dialog. So if you have buttons below the dialog’s text for instance – this won’t eat those touches – and is what is causing the problems.
One option is to create two layers one that eats everything and another on top of that with a higher priority that allows touches for the Yes/No buttons. Another ideas could be to restrict the touches to only the buttons on the dialog box and not the entire dialog itself.
If I wanted to animated some of the CCMenuItems when the modal alert closes. Like fade and scale them out separately before the dialog, and coverLayer fade and scale. Where would I do it.
I’ve tried placing the animations inside the block on the CCMenuItem, but it doesn’t like those because I think it is autoreleasing them before I can animate them… which is weird because we are animating the alertDialog and and coverLayer in closeAlert method.
As you can see in the CloseAlert method, your callback block is only called after a removeFromParentAndCleanup call on the alert, so all the cleanup is either already done or in progress when it’s your turn. That’s by intent since the block is meant to be your action after the decision has been met and the alert is gone.
If you want that differently, just switch these two lines and call your block before the cleanup call. But make sure that your block runs the animations quasi-synchroneously, i.e. does not return before your animations are completed. You might need an artificial delay-wait at the end, since animations usually run asynchroneously.
Thanks you again Rombos! Your little comment about the artificial delay-wait was very helpful. That made my phantom leak go away!
I had the fadeTo on coverLayer set to a duration shorter than my total CCSequence animations duration on the alertDialog. I put in that artificial delay just to make sure that I wasn’t removing from parent prematurely.
Hey, thank you. I did get warnings in 2.0 because of deprecated functions. I adjusted accordingly..
/*
* ModalAlert – Customizable popup dialogs/alerts for Cocos2D
* For details, visit the Rombos blog:
* https://rombosblog.wordpress.com/2012/02/28/modal-alerts-for-cocos2d/
*
* Copyright (c) 2012 Hans-Juergen Richstein, Rombos
* http://www.rombos.de
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the “Software”), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
#import “ModalAlert.h”
#import “cocos2d.h”
#define kDialogTag 1234
#define kAnimationTime 0.4f
#define kDialogImg @”dialogBox.png”
#define kButtonImg @”dialogButton.png”
#define kFontName @”MarkerFelt-Thin”
// class that implements a black colored layer that will cover the whole screen
// and eats all touches except within the dialog box child
@interface CoverLayer : CCLayerColor {
}
@end
@implementation CoverLayer
– (id)init {
self = [super initWithColor:ccc4(0,0,0,0)
width:[CCDirector sharedDirector].winSize.width
height:[CCDirector sharedDirector].winSize.height];
if (self) {
[self setTouchEnabled:YES];
}
return self;
}
– (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint touchLocation = [self convertTouchToNodeSpace: touch];
CCNode *dialogBox = [self getChildByTag: kDialogTag];
// eat all touches outside of dialog box
return !CGRectContainsPoint(dialogBox.boundingBox, touchLocation);
}
– (void) registerWithTouchDispatcher {
[[[CCDirector sharedDirector] touchDispatcher] addTargetedDelegate:self priority:INT_MIN+1 swallowsTouches:YES];
}
@end
@implementation ModalAlert
+ (void) CloseAlert: (CCSprite*) alertDialog onCoverLayer: (CCLayer*) coverLayer executingBlock: (void(^)())block {
// shrink dialog box
[alertDialog runAction:[CCScaleTo actionWithDuration:kAnimationTime scale:0]];
// in parallel, fadeout and remove cover layer and execute block
// (note: you can’t use CCFadeOut since we don’t start at opacity 1!)
[coverLayer runAction:[CCSequence actions:
[CCFadeTo actionWithDuration:kAnimationTime opacity:0],
[CCCallBlock actionWithBlock:^{
[coverLayer removeFromParentAndCleanup:YES];
if (block) block();
}],
nil]];
}
+ (void) ShowAlert: (NSString*) message onLayer: (CCLayer *) layer
withOpt1: (NSString*) opt1 withOpt1Block: (void(^)())opt1Block
andOpt2: (NSString*) opt2 withOpt2Block: (void(^)())opt2Block {
// create the cover layer that “hides” the current application
CCLayerColor *coverLayer = [CoverLayer new];
[layer addChild:coverLayer z:INT_MAX]; // put to the very top to block application touches
[coverLayer runAction:[CCFadeTo actionWithDuration:kAnimationTime opacity:80]]; // smooth fade-in to dim with semi-transparency
// open the dialog
CCSprite *dialog = [CCSprite spriteWithFile:kDialogImg];
dialog.tag = kDialogTag;
dialog.position = ccp(coverLayer.contentSize.width/2, coverLayer.contentSize.height/2);
dialog.opacity = 220; // make it a bit transparent for a cooler look
// add the alert text
CGSize msgSize = CGSizeMake(dialog.contentSize.width * 0.9, dialog.contentSize.height * 0.55);
float fontSize = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)?42:30;
CCLabelTTF *dialogMsg = [CCLabelTTF labelWithString:message
fontName:kFontName
fontSize:fontSize
dimensions:msgSize
hAlignment:kCCTextAlignmentCenter ];
//dialogMsg.anchorPoint = ccp(0, 0);
dialogMsg.position = ccp(dialog.contentSize.width/2, dialog.contentSize.height * 0.6);
dialogMsg.color = ccBLACK;
[dialog addChild: dialogMsg];
// add one or two buttons, as needed
CCMenuItemSprite *opt1Button = [CCMenuItemSprite itemWithNormalSprite:[CCSprite spriteWithFile:kButtonImg]
selectedSprite:[CCSprite spriteWithFile: kButtonImg]
block:^(id sender) {
[self CloseAlert:dialog onCoverLayer: coverLayer executingBlock:opt1Block];
}];
opt1Button.position = ccp(dialog.textureRect.size.width * (opt1 ? 0.27f:0.5f), opt1Button.contentSize.height * 0.8f);
CCLabelTTF *opt1Label = [CCLabelTTF labelWithString:opt1 fontName:kFontName fontSize:fontSize dimensions:opt1Button.contentSize hAlignment:kCCTextAlignmentCenter ];
opt1Label.anchorPoint = ccp(0, 0.1);
opt1Label.color = ccBLACK;
[opt1Button addChild: opt1Label];
// create second button, if requested
CCMenuItemSprite *opt2Button = nil;
if (opt2) {
opt2Button = [CCMenuItemSprite itemWithNormalSprite:[CCSprite spriteWithFile:kButtonImg]
selectedSprite:[CCSprite spriteWithFile: kButtonImg]
block:^(id sender) {
[self CloseAlert:dialog onCoverLayer: coverLayer executingBlock:opt2Block];
}];
opt2Button.position = ccp(dialog.textureRect.size.width * 0.73f, opt2Button.contentSize.height * 0.8f);
CCLabelTTF *opt2Label = [CCLabelTTF labelWithString:opt2 fontName:kFontName fontSize:fontSize dimensions:opt2Button.contentSize hAlignment:kCCTextAlignmentCenter ];
opt2Label.anchorPoint = ccp(0, 0.1);
opt2Label.color = ccBLACK;
[opt2Button addChild: opt2Label];
}
CCMenu *menu = [CCMenu menuWithItems:opt1Button, opt2Button, nil];
menu.position = CGPointZero;
[dialog addChild:menu];
[coverLayer addChild:dialog];
// open the dialog with a nice popup-effect
dialog.scale = 0;
[dialog runAction:[CCEaseBackOut actionWithAction:[CCScaleTo actionWithDuration:kAnimationTime scale:1.0]]];
}
+ (void) Ask: (NSString *) question onLayer: (CCLayer *) layer yesBlock: (void(^)())yesBlock noBlock: (void(^)())noBlock {
[self ShowAlert:question onLayer:layer withOpt1:@”Yes” withOpt1Block:yesBlock andOpt2:@”No” withOpt2Block:noBlock];
}
+ (void) Confirm: (NSString *) question onLayer: (CCLayer *) layer okBlock: (void(^)())okBlock cancelBlock: (void(^)())cancelBlock {
[self ShowAlert:question onLayer:layer withOpt1:@”Ok” withOpt1Block: okBlock andOpt2:@”Cancel” withOpt2Block:cancelBlock];
}
+ (void) Tell: (NSString *) statement onLayer: (CCLayer *) layer okBlock: (void(^)())okBlock {
[self ShowAlert:statement onLayer:layer withOpt1:@”Ok” withOpt1Block: okBlock andOpt2:nil withOpt2Block:nil];
}
@end
Hi I’m getting a leak and I’m stumped on where it’s coming from and how to fix it. It’s leaking from the ShowAlert method on the CoverLayer and the CCMenu and the sprites I create them with. It’s the same code but I added 2 buttons for 4 buttons total. I have a feeling it has to do with the blocks because of the libsystem_blocks.dylib _Block_copy_internal… but I’m not sure because I’m new to blocks.
I’m calling Modal Alert like this:
__block typeof(self) blockself = self;
[ModalAlert Pause:@””
onLayer:self
resumeBlock:^{
[blockself resetValues];
[blockself playerTurn];
blockself.modal_display = false;
}
listenBlock:^{
[blockself resetValues];
[blockself handlePlayButton];
blockself.modal_display = false;
}
optionsBlock:^{
[blockself handleOptions];
blockself.modal_display = false;
}
backBlock:^{
[blockself handleLessonSelect];
blockself.modal_display = false;
}];
Here is a list of the leaked objects:
Leaked Object # Address Size Responsible Library Responsible Frame
CCLabelTTF 1 0x1cd55770 480 Bytes beets-game +[CCLabelTTF labelWithString:dimensions:alignment:fontName:fontSize:]
CCMenu 1 0x1d8c7610 256 Bytes beets-game +[CCMenu menuWithItems:]
CCMenuItemSprite 1 0x1d8a0fe0 272 Bytes beets-game +[CCMenuItemSprite itemFromNormalSprite:selectedSprite:disabledSprite:block:]
CCMenuItemSprite 1 0x1d8d91f0 272 Bytes beets-game +[CCMenuItemSprite itemFromNormalSprite:selectedSprite:disabledSprite:block:]
CCMenuItemSprite 1 0x1d8be3e0 272 Bytes beets-game +[CCMenuItemSprite itemFromNormalSprite:selectedSprite:disabledSprite:block:]
CCMenuItemSprite 1 0x1d8d8c10 272 Bytes beets-game +[CCMenuItemSprite itemFromNormalSprite:selectedSprite:disabledSprite:block:]
CCSprite 1 0x1d8d8e70 448 Bytes beets-game +[CCSprite spriteWithFile:]
CCSprite 1 0x1d8bd950 448 Bytes beets-game +[CCSprite spriteWithFile:]
CCSprite 1 0x1cd5e810 448 Bytes beets-game +[CCSprite spriteWithFile:]
CCSprite 1 0x1d8d24b0 448 Bytes beets-game +[CCSprite spriteWithFile:]
CCSprite 1 0x1d8d9030 448 Bytes beets-game +[CCSprite spriteWithFile:]
CCSprite 1 0x1d8d16a0 448 Bytes beets-game +[CCSprite spriteWithFile:]
CCSprite 1 0x1d8c5be0 448 Bytes beets-game +[CCSprite spriteWithFile:]
CCSprite 1 0x1d8d4440 448 Bytes beets-game +[CCSprite spriteWithFile:]
CCSprite 1 0x1d8d94d0 448 Bytes beets-game +[CCSprite spriteWithFile:]
CoverLayer 1 0x1cd4fdf0 304 Bytes beets-game +[ModalAlert ShowAlert:onLayer:withOpt1:withOpt1Block:andOpt2:withOpt2Block:andOpt3:withOpt3Block:andOpt4:withOpt4Block:]
CCTexture2D 1 0x1cd5fe00 48 Bytes beets-game -[CCLabelTTF setString:]
Malloc 48 Bytes 1 0x1d8be660 48 Bytes beets-game -[CCMenuItem initWithTarget:selector:]
Malloc 48 Bytes 1 0x1d89f950 48 Bytes beets-game -[CCMenuItem initWithTarget:selector:]
Malloc 336 Bytes 1 0x1d8c74c0 336 Bytes beets-game -[CCMenuItem initWithTarget:selector:]
Malloc 48 Bytes 1 0x1d8d7fd0 48 Bytes beets-game -[CCMenuItem initWithTarget:selector:]
Malloc 48 Bytes 1 0x1d87dcb0 48 Bytes beets-game -[CCMenuItem initWithTarget:selector:]
Malloc 336 Bytes 1 0x1d8d4810 336 Bytes beets-game -[CCMenuItem initWithTarget:selector:]
Malloc 336 Bytes 1 0x1d8d8d20 336 Bytes beets-game -[CCMenuItem initWithTarget:selector:]
Malloc 336 Bytes 1 0x1d8d18c0 336 Bytes beets-game -[CCMenuItem initWithTarget:selector:]
CCArray 1 0x1d89e760 16 Bytes beets-game -[CCNode childrenAlloc]
CCArray 1 0x1d87f030 16 Bytes beets-game -[CCNode childrenAlloc]
CCArray 1 0x1d8a1120 16 Bytes beets-game -[CCNode childrenAlloc]
CCArray 1 0x1d8d4990 16 Bytes beets-game -[CCNode childrenAlloc]
CCArray 1 0x1d8d9470 16 Bytes beets-game -[CCNode childrenAlloc]
CCArray 1 0x1d8d47c0 16 Bytes beets-game -[CCNode childrenAlloc]
CCArray 1 0x1d8c7e90 16 Bytes beets-game -[CCNode childrenAlloc]
Malloc 48 Bytes 1 0x1d8a10f0 48 Bytes libsystem_blocks.dylib _Block_copy_internal
Malloc 48 Bytes 1 0x1d8d2670 48 Bytes libsystem_blocks.dylib _Block_copy_internal
Malloc 32 Bytes 1 0x1d8d26a0 32 Bytes libsystem_blocks.dylib _Block_copy_internal
Malloc 32 Bytes 1 0x1d8d4600 32 Bytes libsystem_blocks.dylib _Block_copy_internal
Malloc 32 Bytes 1 0x1d8d1860 32 Bytes libsystem_blocks.dylib _Block_copy_internal
Malloc 32 Bytes 1 0x1d87f010 32 Bytes libsystem_blocks.dylib _Block_copy_internal
Malloc 48 Bytes 1 0x1d89e730 48 Bytes libsystem_blocks.dylib _Block_copy_internal
Malloc 48 Bytes 1 0x1d8c7e60 48 Bytes libsystem_blocks.dylib _Block_copy_internal
Malloc 32 Bytes 1 0x1d8bed30 32 Bytes libsystem_blocks.dylib _Block_object_assign
Malloc 16 Bytes 1 0x1d8be620 16 Bytes beets-game ccArrayNew
Malloc 16 Bytes 1 0x1d8d2490 16 Bytes beets-game ccArrayNew
Malloc 16 Bytes 1 0x1d8be690 16 Bytes beets-game ccArrayNew
Malloc 16 Bytes 1 0x1d89f980 16 Bytes beets-game ccArrayNew
Malloc 16 Bytes 1 0x1d8d81d0 16 Bytes beets-game ccArrayNew
Malloc 16 Bytes 1 0x1d8cfb30 16 Bytes beets-game ccArrayNew
Malloc 16 Bytes 1 0x1d8ce330 16 Bytes beets-game ccArrayNew
Malloc 16 Bytes 1 0x1d87db40 16 Bytes beets-game ccArrayNew
Malloc 16 Bytes 1 0x1d8d8000 16 Bytes beets-game ccArrayNew
Malloc 16 Bytes 1 0x1d8d82f0 16 Bytes beets-game ccArrayNew
Malloc 16 Bytes 1 0x1d89d2a0 16 Bytes beets-game ccArrayNew
Malloc 16 Bytes 1 0x1d8cddf0 16 Bytes beets-game ccArrayNew
Malloc 16 Bytes 1 0x1d8a0ac0 16 Bytes beets-game ccArrayNew
Malloc 16 Bytes 1 0x1d8d2c70 16 Bytes beets-game ccArrayNew
Ahhhg okay after a whole day of fiddling with it… I just had to add:
[alertDialog removeAllChildrenWithCleanup:YES];
before the call to:
[coverLayer removeFromParentAndCleanup:YES];
inside the CloseAlert method to clear up my memory leak…
For some reason I have to explicitly remove those children. The removeFromParentAndCleanup on coverLayer doesn’t work like I expected. I guess because it has children nested so deep…
Feel free to delete my comment. Thanks Rombos, love the blog and sorry for the spamming.