0

I have an arcade style iPhone game that is built with Cocos2d-iPhone and works fine except that it randomly crashes for no apparent reason after an unpredictable amount of playing. Sometimes you can play several levels without a problem and others it crashes after a few seconds. There are a few common lines reported in XCode as usually EXC_BAD_ACCESS which would suggest a memory thing but the objects are all autorelease with extra retains and the crashes are very inconsistent. One thing that seems to fit is that as more is going on at once the likelihood of a crash increases which again suggests some memory issue but then there is no app memory warning fired.

Some example crash lines are:

if(gameGlobals.gameState == GAME_STATE_PAUSED) {...}
if(![hearts isKindOfClass:[CCSprite class]]) {...}

but both these lines have global objects that are fune for ages until the crash.

So, my question is really how do I go about tracking down the problem and are there any likely culprits?

Thanks.

UPDATE

I have been doing a bit of detective work and I at least have some consistency. I think that there are two things going on:

1) is that the maths used to detect a pixel perfect collision with an object with deletion of pixel data is cooking the CPU.

2) I have some zombie sprites and otherwise that are being released with out my realising.

When I add a bullet sprite I use:

CCSprite *bullet = [CCSprite spriteWithFile:@"bullet.png"];
bullet.tag = BULLET_TAG_GOODY;
bullet.position = ccp(player.position.x,50);
[self addChild:bullet];

[bullet runAction:[CCSequence actions:
[CCMoveTo actionWithDuration:2 position:ccp(player.position.x,320)],
                                   [CCCallFuncN actionWithTarget:self selector:@selector(bulletMoveFinished:)],
                                   nil]];
[bullets addObject:bullet];

And then I loop through the bullets array. At the moment it crashes saying that I am trying to access a deallocated instance.

So, 1) how do I set this up so my sprites don't get released? Is it a good idea to remove the autorelease from CCSprite? 2) What is the best way to detect bullets colliding with a space invaders style base attriting the sprite as it gets shot?

Currently:

if([self overlapBases: bullet.position.x :bullet.position.y :true]) {...}

-(bool)overlapBases :(GLfloat)x :(GLfloat)y :(bool)up {
//   NSLog(@"overlapping - %f,%f",x,y);
 if(y > 20 && y < 100) {
     int bn = -1;
     if(x > 28 && x < 118) {
         bn = 0;
     } 

     if(x>140 && x<230 ) {
         bn = 1;
     }

     if(x>254 && x<343 ) {
         bn = 2;
     }

     if(x>365 && x<453 ) {
         bn = 3;
     }

     if(bn> -1) {
     NSLog(@"overlapping - %f,%f",x,y);
//     for (int ix = 0; ix < NUM_BASES; ix++) {

         if (overLap(x, 2, aspeaker[bn].position.x, BASE_WIDTH * aspeaker[bn].scale)) {
             if (overLap(y, 4, aspeaker[bn].position.y, BASE_HEIGHT * aspeaker[bn].scale)) {
                 NSLog(@"ix: %i, x: %f, y: %f",bn,x,y);
                 return [self fineCollision:bn :x :y :up];
             }
         }
     }
}
return false;    
}


-(bool)fineCollision :(GLuint)baseNum :(GLfloat)x :(GLfloat)y :(bool)up {
 //convert  bullet x and y passed in to this base coords
 //check if solid
 //if so, remove bits in gfx and array, return true
 //else return false;

 GLfloat bullx = (x - aspeaker[baseNum].position.x + BASE_WIDTH) / 2.0;
 GLfloat bully = (y - aspeaker[baseNum].position.y + BASE_HEIGHT) / 2.0;
 GLint testx = (GLint)bullx;
 if ((testx < 0) | (testx >= BASE_WIDTH)) {
 return false;
 }

 GLuint testy;  
 bool hit = false;

for (int iy = -2; iy < 2; iy++) {

testy = (GLint)(bully - iy);                            
if ((testy >= 0) & (testy < BASE_HEIGHT)) {

if (baseShape[baseNum][15 - testy][testx] > 0) {


 if(showrm == YES) {
     CCSprite *maskSprite = [CCSprite spriteWithFile:@"bullet-mask.png"];

     [maskSprite setBlendFunc: (ccBlendFunc) {GL_ZERO, GL_ONE_MINUS_SRC_ALPHA}];

     maskSprite.position = ccp( x , y-(40+22-5) );

     NSLog(@"sprite: %f",maskSprite.position.x);

     NSLog(@"render mask: %f",rm.frameInterval);
     [rm addSpriteMask:maskSprite];





     [rm drawCurrent];


 }


 [self remove:testx :testy :baseNum :up];

 GLuint seed = rand()%64;
 if (seed & 1) {

 [self remove:testx - 1:testy - 1 :baseNum :up];
 }
 if (seed & 2) {
 [self remove:testx - 1:testy :baseNum :up];
 }
 if (seed & 4) {
 [self remove:testx - 1:testy + 1:baseNum :up];
 }
 if (seed & 8) {
 [self remove:testx + 1:testy - 1:baseNum :up];
 }
 if (seed & 16) {
 [self remove:testx + 1:testy :baseNum :up];
 }              
 if (seed & 32) {
 [self remove:testx + 1:testy + 1:baseNum :up];                 
 }

 hit = true;                
 }
 }
 }

 return hit;
 }



 - (void)remove:(GLint)offX :(GLint)offY :(GLuint)baseNum :(bool)up {
 if ((offX < 0) | (offX >= BASE_WIDTH)) {
 return;
 }

 if ((offY < 0) | (offY >= BASE_HEIGHT)) {
 return;
 }  
 baseShape[baseNum][15 - offY][offX] = 0;


 }

bool overLap(GLfloat x1, GLfloat w1, GLfloat x2, GLfloat w2) {

GLfloat left1, left2;
GLfloat right1, right2;

GLfloat halfWidth1 = w1 * 0.5f;
GLfloat halfWidth2 = w2 * 0.5f;

left1 = x1 - halfWidth1;
left2 = x2 - halfWidth2;
right1 = x1 + halfWidth1;
right2 = x2 + halfWidth2;

if (left1 > right2) {
    return false;
}

if (right1 < left2) {
    return false;
}

return true;
}

For detection and a rendermask think using lots of sprites and their blend mode for the attrition.

Thanks.

UPDATE AGAIN :)

Ok, I found some Zombies!

Address Category    Event Type  RefCt   Timestamp   Size    Responsible Library Responsible Caller

0   0x13a00e90  CCSprite    Malloc  1   00:32.974.212   432 SpacedInvaders  +[CCSprite spriteWithFile:]

1   0x13a00e90  CCSprite    Autorelease <null>  00:32.974.235   0   SpacedInvaders  +[CCSprite spriteWithFile:]

2   0x13a00e90  CCSprite    Retain  2   00:32.974.546   0   SpacedInvaders  -[CCArray insertObject:atIndex:]

3   0x13a00e90  CCSprite    Retain  3   00:32.974.629   0   SpacedInvaders  -[CCActionManager addAction:target:paused:]

4   0x13a00e90  CCSprite    Retain  4   00:32.974.634   0   SpacedInvaders  -[CCArray addObject:]

5   0x13a00e90  CCSprite    Release 3   00:32.986.279   0   QuartzCore  CA::Display::DisplayLink::dispatch(unsigned long long, unsigned long long)

6   0x13a00e90  CCSprite    Release 2   00:33.074.889   0   SpacedInvaders  -[CCArray removeObject:]

7   0x13a00e90  CCSprite    Release 1   00:33.074.915   0   SpacedInvaders  -[CCActionManager deleteHashElement:]

8   0x13a00e90  CCSprite    Release 0   00:33.074.918   0   SpacedInvaders  -[CCArray removeObject:]

9   0x13a00e90  CCSprite    Zombie  -1  00:33.074.939   0   SpacedInvaders  -[GameLayer update:]

So what's going on here? I see that the retain count has gone too low but how do I avoid this. I assume I am doing something dumb btw.

Thanks.

Martin
  • 1,135
  • 1
  • 8
  • 19
  • OK, thanks for comments so far. – Martin Nov 08 '11 at 12:35
  • OK, thanks for comments so far (again). To be clear the autoreleases are part of cocos2d core code. Zombies are enabled but there aren't any in instruments. There are no significant leaks or otherwise also (that I can see). – Martin Nov 08 '11 at 12:41
  • 1
    Zombies are not for instruments, instead they change EXC_BAD_ACCESS crashes into messages that read "xxxx sent to deallocated instance of yyyy" which help you pinpoint the problem. Maybe this debugging crash course might help: http://www.learn-cocos2d.com/2011/10/xcode-4-debugging-crashcourse/ – CodeSmile Nov 08 '11 at 14:48

5 Answers5

1

The only way this line can crash

if(gameGlobals.gameState == GAME_STATE_PAUSED) {...}

is if gameGlobals is an Objective-C object that has already been deallocated.

If gameGlobals is a C struct, you would not see EXC_BAD_ACCESS because it is not a pointer.

If gameGlobals is a nil object, you would not see EXC_BAD_ACCESS because sending messages to nil is OK.

Your statement "the objects are all autorelease with extra retains" rings alarm bells to me because it suggests you don't really understand the memory management rules. Global objects that should live for the entire lifetime of the app, do not need to be autoreleased. You should just alloc it at the beginning and that's it. Alternatively, consider making your global objects properties of you app delegate.

If you still can't figure it out, the first part of ade's answer is the way to go (NSZombie), although the second part probably won't help. I doubt if your objects ever get to be nil.

JeremyP
  • 84,577
  • 15
  • 123
  • 161
  • GameGlobals is an Objective C class. It is instantiated as a global instance with a simple alloc-init. This object is not autoreleased, only the Cocos2d objects are which is the default situation. – Martin Nov 08 '11 at 12:44
  • @Martin: Can you show your declaration, definition and initialisation of it please. – JeremyP Nov 08 '11 at 13:37
0

It definitely sounds like memory problems to me. If it's a huge memory leak you also won't necessarily get a memory warning before.

Did you try profiling your game for leaks with XCode? If it is a memory problem you will see it when it crashes during profiling.

TheEye
  • 9,280
  • 2
  • 42
  • 58
0

"objects are all autorelease" sounds like the likely cause. Autorelease takes effect when the UI "comes up for air" (control returns from the current UI event handling logic), and if you have an autoreleased variable you're expecting to continue to use after this, it's likely to have gone "poof" (though often you can use it for a time after, until the storage is reused).

Did you run the analyzer or any other tools?

Hot Licks
  • 47,103
  • 17
  • 93
  • 151
0

If I've understood you correctly then "autorelease with extra retains" sounds like a problem (unless you also have extra releases), that could cause a leak leading to increased memory usage causing other objects to be released which you then subsequently try to access.

try enabling nszombies: How to enable NSZombie in Xcode?

also use nslog statements to trace when objects are null when you expect them to not be.

if you know of some if cases where you get crashes then maybe you can put a breakpoint in the else of a NULL check prior to that:

if(gameGlobals.gameState != NULL) {
    if(gameGlobals.gameState == GAME_STATE_PAUSED) {...}
} else {
    //break or nslog here
}
Community
  • 1
  • 1
ader
  • 5,403
  • 1
  • 21
  • 26
0

The first thing to realise is that there may be nothing wrong with the code where the EXC_BAD_ACCESSoccurs. However, it is worth investigating that particular object just in case. The first avenue to pursue is that one of those objects has been deallocated because it has not been retained sufficiently. So, yes, definitely enable Zombies as per other people's advice here.

Objects that are released may not be deallocated immediately. That's why theEXC_BAD_ACCESScan occur at weird times. The objects are only deallocated when the chunk of memory that they were using is no longer needed. Other objects would of course use the same chunk. So when that chunk becomes eligible for deallocation is out of your control as the developer. That's handled by the runtime.

Best thing is to first run the Analyzer, then run Instruments, profiling with the Leaks instrument.

There's a great explanation of all this, and point by point advice on tracking downEXC_BAD_ACCESSerrors on Lou Franco's site:

Understanding EXC_BAD_ACCESS

Max MacLeod
  • 26,115
  • 13
  • 104
  • 132