Examine cocos2d's nodes tree - ios

I am already finishing my game and I am trying to fix some memory issues.
My game has 36 levels and I noticed that when I run it in an iPod, after 20-25 levels my app crashes. I start getting memory warnings and it always crashes when transitioning between scenes.
I have already used instruments to fix every memory leak but this is still happening.
My guess is that cocos is still holding references to old objects.
I would like to find a way to look through the cocos' nodes hierarchy in certain points of my game to make sure everything is ok.
Any idea of how to do that?

I have modified CCTextureCache to log which textures are retained and which others are released, at critical steps in my games, where i force a 'removedUnusedTextures', notably on scene transitions. Whatever you see there should give a hint of where to look in your app. Also, i tend to tag everything with a unique tag, and remember the tags in every class that adds stuff to a CCNode. In the wash (cleanup), i rundown the array of tags, and force remove them.

I ended up doing this:
I added some logic in the CCDirector to "draw" the hierarchy:
-(void) printChildren:(CCNode *)node andLevel:(NSInteger)level {
NSString *tabs = #"";
for (int i=0; i <level; i++) {
tabs = [NSString stringWithFormat:#"%# ", tabs];
}
NSLog(#"%#NODE %#. Children count: %d", tabs, node, node.children.count);
if ( node.children.count == 0 ) {
return;
} else {
for (CCNode *child in node.children) {
[self printChildren:child andLevel:level+1];
}
}
}
-(void) nodeHierarchy
{
NSLog(#"Printing nodeHierarchy! with an stack of %d scenes", [scenesStack_ count]);
for (CCScene *scene in scenesStack_) {
NSLog(#"Scene in stack: %#", [scene class]);
[self printChildren:scene andLevel:0];
}
}
I call nodeHierarchy in the replaceScene method.
It would be great to have a more visual tool, but this worked for me.

Any reason you cannot migrate to SDK 5.0 that has ARC.

Related

Why this simple app with ARC leaks?

So I'm relative new to objC programming. But not to C. In a more complicated app I think I have a memory leaks. I've programmed this just for make some tests. The app is very simple: it store in a MutableArray a series of integer that rappresent timers scheduled. The app has one NSTimer in the current runloop that check every second if it is the right time to ring comparing a counter with the right element of the MutableArray. Everything works, but memory in debug panel grow up, grow up, grow up…
I've try some variants but something still missing for me about ARC. I simply don't understand, since ARC is NOT a garbage collector, why memory grow and what I do wrong.
Here is the code:
-(id)initWithLabel:(UILabel *)label {
self = [super init];
self.list = [[mtAllarmList alloc]init];
self.label = label;
return self;
}
My class init function. I pass a label reference (weak beacause it is own by viewcontroller) to my class. I also allocate and init the class mtAllarmList that contain the MutableArray and other information (in the original app, file to play, volumes, eccetera).
-(void)ClockRun {
NSMethodSignature * signature = [mtClockController instanceMethodSignatureForSelector:#selector(check)];
NSInvocation * selector = [NSInvocation invocationWithMethodSignature: signature];
[selector setTarget:self];
[selector setSelector:#selector(check)];
[[NSRunLoop currentRunLoop] addTimer: self.time = [NSTimer scheduledTimerWithTimeInterval:1
invocation:selector
repeats:YES]
forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runUntilDate:[[NSDate alloc]initWithTimeIntervalSinceNow: 30]];
}
ClockRun: is the method the app call to start everything. It simply start the timer that fires every second to check:
-(void)check {
self.counter++;
int i = [self.list check:self.counter];
if(i == 1) {
[self writeAllarmToLabel:self.label isPlayingAllarmNumber:self.counter];
}
else if (i == 2) {
[self writeAllarmToLabel:self.label theString: #"Stop"];
[self.time invalidate];
self.counter = 0;
}
else {
[self writeAllarmToLabel:self.label theString:[[NSString alloc]initWithFormat:#"controllo, %d", self.counter]];
}
NSLog(#"controllo %d", self.counter);
}
Check: simply reacts to the return value of [list check: int] methods of mtAllarmList. It returns 1 if timer must ring, 0 if not, and 2 if the sequence ends. In that case self.counter will be set to 0 and the NSTimer will be invalidate.
-(id)init {
self = [super init];
self.arrayOfAllarms = [[NSMutableArray alloc]initWithCapacity:0];
int i;
for(i=1;i<=30;++i) {
[self.arrayOfAllarms addObject: [[NSNumber alloc]initWithInt:i*1]];
}
for(NSNumber * elemento in self.arrayOfAllarms)
NSLog(#"ho creato un array con elemento %d", [elemento intValue]);
return self;
}
In mtAllarmList init method simulates the costruction an array (I've try a variety of patterns) and log all the elements.
-(int)check:(int)second {
int maxValue = [[self.arrayOfAllarms lastObject] intValue];
if(maxValue == second){
self.index = 0;
return 2;
} else {
if ([[self.arrayOfAllarms objectAtIndex:self.index] intValue] == second) {
self.index++;
return 1;
} else {
return 0;
}
}
}
Check methods instead is very elementary and I don't think needs explanations.
So, why this simple very stupid app leaks?
Since you're doing this on the main run loop, you can (and should) simplify the ClockRun method:
- (void)ClockRun {
self.time = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(check) userInfo:nil repeats:YES];
}
That NSInvocation code was unnecessary and the NSRunLoop code could only introduce problems.
Having said that, this is unlikely to be the source of your memory consumption. And nothing else in the provided code snippets looks like an obvious memory problem. If you're 100% confident that the timer is getting invalidated, then the timer is not the problem. I wonder about the object graph between the view controller at this mtClockController. Or perhaps some circular reference in view controllers (e.g. pushing from A to B and to A again). It's hard to say on the basis of what's been provided thus far.
Sadly, there's not much else we can suggest other than the routine diagnostics. First, I'd run the the app through the static analyzer (by pressing shift+command+B in Xcode, or choosing "Profile" from the Xcode "Product" menu).
Second, you should run your app through Leaks and Allocations tools to identify the what precisely is leaking on each iteration. Do you have extra instances of the view controllers? Or just the mtClockController?
Until you identify what's not being deallocated, it's hard to remedy it. And Instruments is the best tool for identifying what's not getting released. In WWDC 2012 video iOS App Performance: Memory the demonstration sections of the video give pragmatic demonstrations of using Instruments (as well as a wealth of good background info on memory management).
Third, when I've got a situation where I'm not sure if things are getting deallocated when they should, I sometimes include dealloc methods that tell me when the object is deallocated, e.g.:
- (void)dealloc {
NSLog(#"%s", __PRETTY_FUNCTION__);
}
I'd suggest this not only for your key model objects, but your view controller, too. (Sometimes we agonize over our model objects only to realize that it's the view controller, itself, which is be retained by something else.)
Clearly Instruments is a much richer tool, but this can be used to quickly identify failure to deallocate (and show you what's maintaining the strong references).
I ran you app through Instruments, watching your custom objects, and everything is being deallocated properly. Below, I marked generation A, hit the button, let the timer expire, marked generation B, hit the button again, etc. I did that four times, and I then simulated a memory warning, and did one final generation. Everything looks fine (this is a compilation of six screen snapshots in one, showing the total allocations at each of the six generations):
I inspected your Generations, as well as the Allocations themselves, and none of your objects are in there. Everything is getting released fine. The only things there are internal Cocoa objects associated with UIKit and NSString. Cocoa Touch does all sorts of caching of stuff behind the scenes that we have no control over. The reason I did that final "simulator memory warning" was to give Cocoa a chance to purge what it can (and you'll see that despite what Generations reports, the total allocations fell back down a bit).
Bottom line, your code is fine, and there is nothing to worry about here. In the future, don't worry about incidentally stuff showing up in the generations, but rather focus on (a) your classes; and (b) anything sizable. But neither of those apply here.
In fact, if you restrict Instruments to only record information for your classes with the mt prefix (you do this by stopping a recording of Instruments and tap on the "i" button on the Allocations graph and configure the "Recorded Types"), you'll see the sort of graph/generations that you were expecting:
A couple of observations:
Instead of using the invocation form of scheduledTimerWithInterval, try using the selector form directly, in this case it's a lot simpler and clearer to read.
Since you're call runUntilDate directly, I don't think you're getting any autorelease pools created/drained, which would lead to memory leakage, specifically in the check function. Either don't call runUntilDate and allow the normal run loop processing to handle things (the normal preferred mechanism) or wrap check in an #autoreleasepool block.

Strange EXC_BAD_ACCESS SpriteKit removeSubsprite crash

I am new to SpriteKit and just built my first game. Everything was working great until iOS 7.1. Now, after a few times of advancing to a new level and presenting a new Scene, it crashes. I don't think I am presenting it in an incorrect way:
ZSSMyScene *nextLevel = [[ZSSMyScene alloc] initWithSize:self.size level:self.level score:score];
[self.view presentScene:nextLevel];
I get a EXC_BAD_ACCESS error, and it looks like it is happening on removeSubsprite, but I can't find anywhere in my code that I would be removing a subsprite:
Not sure what other info to provide as this is just an obscure error that seemed to start when I updated to iOS 7.1 SDK.
This appears to be a bug, possibly only with SKShapeNodes.
My solution was to create an SKNode category and call this cleanup method when any node i'm removing has children.
- (void)cleanUpChildrenAndRemove {
for (SKNode *child in self.children) {
[child cleanUpChildrenAndRemove];
}
[self removeFromParent];
}

iOS7 Sprite Kit how to set up a series of actions that depend on animation timing?

Just to clarify: this is not a question about sequences or groups of SKActions.
I have 7-20 sprites representing monsters on a 2D game board. I would like them to act one after another. The action can be a move, or move and attack.
The issue that I'm running in is that the first few monsters seem to act normally, - one monster moves, attacks, then the next one, etc. But after a few of them (4-5) the sequence breaks and multiple monsters start to act at the same time.
Is this an issue with the recursive move/AI methods I have below or if something within Sprite Kit is interfering with the sequence?
I see that the call stack in xCode seems to grow, and I see multiple calls to processMonsterAI, but it's not a 1 call per monster situation.
In particular I'm interested if I should use NSTimers or Delayed dispatch queue blocks instead of relying on Sprite Kit's completion blocks?
Every turn, my AI has to move monsters on the board:
-(void)processMonsterAI
{
CharacterModelNode* monster = [allMonstersCopy lastObject];
[allMonstersCopy removeLastObject];
AIBase* ai = monster.character.AI;
ai.delegate = self;
ai doAIAction];
}
AI frequently makes decisions to move around the board, which are animated as move from one cell to the next, step by step until the monster is out of moves or destination is reached:
//do recursive movement animation
-(void)recursiveMoveWithCharacter:(CharacterModelNode*)characterModel path:(NSMutableArray*)path
{
//check if we ran out of moves
if(path.count == 0)
{
//notify delegate
[self moveCompleteForCharacter:characterModel];
}else
{
NSNumber* tileIndex = path[0];
CGPoint destination = [MapOfTiles positionForTileAtIndex:tileIndex.intValue];
[path removeObjectAtIndex:0];
[self runAction:[SKAction moveTo:destination duration:1]
completion:^{
characterModel.character.currentMoves = characterModel.character.currentMoves-1;
[self recursiveMoveWithCharacter:characterModel path:path];
}];
}
}
Once the animation completes, the AI is notified that the monster is in a correct position, and attack sequence can be performed:
//once animation is complete, call
-(void)moveComplete
{
if(self.bestTarget)
{
[self.actor attack:self.bestTarget];
}
//notify whoever is the delegate that we are done here and the next AI can do it's magic.
[self finishAction];
}
//AI is done, notify delegate to process next monster
-(void)finishAction
{
NSLog(#"Finished :%#",self.actor.character.name);
[self.actor debugFlashRed];
if([self.delegate respondsToSelector:#selector(didFinishAIAction:)])
{
[self.delegate didFinishAIAction:self];
}
}
Proceed to move/attack with the next monster
//either repeat the process, or terminate if all monsters acted
-(void)didFinishAIAction:(AIBase*)AI
{
if(allMonstersCopy.count==0)
{
//finished with monster turn
[self preventUserInteraction:NO];
[[GameDataManager sharedInstance] processEndTurn];
}else
{
[self processMonsterAI];
}
}
UPDATE:
Here's the call stack, It hits the assertion that finishAction gets called by some AI more than once:
Does doAIAction possibly go through multiple paths that may call finishAction more then once? That does seem to be a possibility. There isn't really any branching in the code you have shown that would lead to multiple processMonsterAI calls.

For loop CCSprites in CCScene tag check?

I am trying to put together a for loop in Cocos2D app that will loop through all the CCSprite's in my scene checking for sprites with a tag of lets say 2.
Currently I have been trying to do this without any success or even idea where to start because I am not an expert with for loops in Cocos2D since I am not too familiar with its API's.
Do I need to have an if in the for loop? Is that the best thing to do? How should I even accomplish this?
CCArray *arr = [self children];
for(CCSprite *sprite in arr)
{
if(2 == sprite.tag)
{
//do what you want
}
}

Game Center Sandbox : "Could not create game" issue

I'm currently developing a turn based game using Game Center to handle the online functionalities (for matchmaking and turns handling).
I'm using two sandbox accounts - one on my 3gs and one on the ios Simulator.
I've been testing my app using the GKTurnBasedMatchMakerViewController to do the match making for a while without any problems, but I'm now stuck with an issue:
Every time I want to invite another player for a new (with either one or the other player), the GKTurnBasedMatchMakerViewController displays a UIAlertView stating :
Could not create game - Please remove an existing game and try again.
The thing is, I've deleted all the matches for each player (none of them has any game in his list (not even a closed game). So none of the user is in any match at the moment.
In my GKTurnBaseMatchMakerViewControllerDelegate the turnBasedMatchmakerViewController:didFailWithError: is not called.
The only called function called in the delegate- when I click the OK button on the UIAlertView - is turnBasedMatchmakerViewControllerWasCancelled:
The only thing I can think of is that my games are actually not removed from GameCenter, but as I'm removing them using the GKMatchMakerViewController UI, I barely think so.
When quitting from a turn-based match I've implemented the turnBasedMatchmakerViewController:playerQuitForMatch: like this:
- (void)turnBasedMatchmakerViewController:(GKTurnBasedMatchmakerViewController *)viewController playerQuitForMatch:(GKTurnBasedMatch *)match
{
if ( [self isLocalPlayerCurrentPlayerForMatch:match] ) {
NSData* endData = match.matchData;
for (GKTurnBasedParticipant* participant in match.participants) {
participant.matchOutcome = GKTurnBasedMatchOutcomeWon;
}
match.currentParticipant.matchOutcome = GKTurnBasedMatchOutcomeLost;
[match endMatchInTurnWithMatchData:endData
completionHandler:^(NSError *error) {
if (error) {
NSLog(#"%#",error.description);
}
}];
}
}
(NB: I only have two players in the game)
where isLocalPlayerCurrentPlayerForMatch is:
- (BOOL) isLocalPlayerCurrentPlayerForMatch:(GKTurnBasedMatch*)match
{
return [[[GKLocalPlayer localPlayer] playerID] isEqualToString:match.currentParticipant.playerID];
}
Has anyone encountered and found a solution to this issue?
Am I doing something wrong here, or is it so obvious I just can't see it?
Thank you very much for any comments that would help me find the root of that issue.
Update
Thanks to #kaan-dedeoglu I managed to know that both users had an empty list of matches (consistent with the displayed state).
I also created a third Sandbox account.
Naming the two first accounts A and B, C the third one.
State 1:
A and B are not linked to any match.
A and B are both getting the "Could not create game" error while creating any game (A invites B, A||B invites other player, A||B creates new automatch).
State 2:
C (working account) can invite B and normally plays a party with B.
C (working) can invite B for another simultaneous party
C (working) invites A to play.
A can't play (can't access the list of current matches, the GKTurnBasedMatchMakerViewController directly goes to the creation of a new game).
C is not working anymore.
A, B and C are now stuck in "Could not create game" error.
As a complement here is how I initialize my GKTurnBasedMatchMakerViewController, but I don't see that being wrong.
- (void) displayMatchMakerVC
{
if (! [[GKLocalPlayer localPlayer] isAuthenticated] ) return;
GKMatchRequest* request = [[[GKMatchRequest alloc] init] autorelease];
int nbPlayers = 2;
request.minPlayers = nbPlayers;
request.maxPlayers = nbPlayers;
GKTurnBasedMatchmakerViewController* matchMakerVC = [[[GKTurnBasedMatchmakerViewController alloc] initWithMatchRequest:request] autorelease];
matchMakerVC.turnBasedMatchmakerDelegate = self;
matchMakerVC.showExistingMatches = YES;
[[CCDirector sharedDirector] presentModalViewController:matchMakerVC animated:YES];
}
NB: I'm not using ARC, could that be related to a memory issue? I'm not really a memory management guru, but it seems correct to my understanding.
Any idea of how this could be related to my code and not to game center?
Thank you very much for any answer that could help me go further.
Update 2: turnbasedMatchmakerViewController:didFindMatchMethod:
Here's my turnbasedMatchmakerViewController:didFindMatchMethod: method.
- (void)turnBasedMatchmakerViewController:(GKTurnBasedMatchmakerViewController *)viewController didFindMatch:(GKTurnBasedMatch *)match
{
BLTheme* theme = [[[BLGameConfig sharedConfig] localPlayer] userTheme];
GameSceneRemoteGCLoader* loader = [[GameSceneRemoteGCLoader alloc] initWithGKMatch:match andTheme:theme];
[viewController dismissViewControllerAnimated:NO completion:^{}];
[[CCDirector sharedDirector] replaceScene:loader];
}
When I'm launching an automatch it's launching the exact same error "Could not create game - Please remove an existing game and try again.".
This may or may not be the solution to your problem, but I had a similar issue and solved it in the following way.
It seems that either by default, or somehow, Game Center treats apps with differing CFBundleVersion (build number, not version number, or CFBundleShortVersionString) values as incompatible with one another, and thus does not show matches between apps with incremented build numbers. (Often, developers increment this number as new ad hoc builds or stable releases are distributed during development, so this is quite unfortunate).
To find and remove the "missing" games, I decremented my CFBundleVersion value (which revealed the games), and then deleted the offending matches.
Alternatively, tweaking some settings in iTunes Connect seems to have removed this CFBundleVersion incompatibility. It takes a while to propagate, but I think what did it was tapping on my app, tapping on View Details, making sure the Game Center switch is set to "Enabled", and making sure there is an item in the "Multiplayer Compatibility" table. You could also play with the possibilities within the "Manage Game Center" button from the original app screen, but I think the "Multiplayer Compatibility" setting is what finally allowed me to see all the "old" matches that were previously hidden.
Good luck!
Just to make sure: In both these devices, add these lines in your authentication completion handler and run it once. (then you can comment it out).
[GKTurnBasedMatch loadMatchesWithCompletionHandler:(^)(NSArray *matches, NSError *error) {
for (GKTurnbasedMatch *match in matches) {
[match removeWithCompletionHandler:NULL];
}
}];
This will ensure that all games are removed from your playerID.
It's ridiculous . You don't have to remove an existing match to create a new match. I'm developing a game like this and it actually works.
The following worked for me. First I ran the app on the device for each player, calling quitAllMatches. Then I ran the app again on each device, calling removeAllMatches.
In the long run, it has to be better to clean them up as you go along. But this solved the immediate problem.
-(void) quitAllMatches {
[GKTurnBasedMatch loadMatchesWithCompletionHandler:^(NSArray* matches, NSError* error) {
for (GKTurnBasedMatch* match in matches) {
GKTurnBasedParticipant* participant = match.currentParticipant;
NSString* playerID = participant.playerID;
NSString* localPlayerID = [GKLocalPlayer localPlayer].playerID;
if ([playerID isEqualToString: localPlayerID]) {
NSArray* participants = match.participants;
for (GKTurnBasedParticipant* participant in participants) {
participant.matchOutcome = GKTurnBasedMatchOutcomeTied;
}
NSData* data = [NSData data];
[match endMatchInTurnWithMatchData: data completionHandler:^(NSError* error) {
if (error) {
WJLog(#"did not end -- error %#", [error localizedDescription]);
}
else {
WJLog(#"match ended!");
}
}];
}
}
}];
}
-(void) removeAllMatches {
[GKTurnBasedMatch loadMatchesWithCompletionHandler:^(NSArray* matches, NSError* error) {
for (GKTurnBasedMatch* match in matches) {
[match removeWithCompletionHandler:^(NSError* error) {
if (error) {
WJLog(#"error: %#", [error localizedDescription]);
}
else {
WJLog(#"removed match");
}
}];
}
}];
}

Resources