SpriteKit sound memory consumption issue - ios

In an attempt to further familiarize myself with SpriteKit, I made a simple little app that would spawn a random (3 choices) ball at whatever location you click or touch (which automatically despawn after 7 seconds), then apply physics and gravity, and finish with manipulating gravity at set intervals. It was simple enough and worked quite well. However, I decided to add sound to it so the balls would make noise when hitting both the walls or each other.
While it worked, it brought up an issue I couldn't figure out on my own: the soundless version would lag if you spawned so many balls it couldn't handle it, but the sound version continued to lag thereafter. I checked out Xcode's trusty Debug Navigator and found that the memory of the app continued to expand with every ball added to the scene. The soundless version didn't expand on the memory NEARLY as much and additionally reclaimed some bits after the balls had been removed from their parents.
I can't help but think there was something wrong in my implementation that's not only keeping the sound files around in memory after they've been used, but keeping multiples of each file.
I'll happily share my project with anyone who requests, but this is the approximation of the methods that happen:
initialization, sound actions are created and stored in the scene class so they can be accessed later:
#interface REP_Balls () {
SKAction* ballSound01;
}
#end
#implementation REP_Balls {
...
...
...
-(void) setUpSounds {
NSArray* array = #[ #"ball_hit_01.wav",]; //I have more in the array, I'm just simplifying code for stackoverflow
ballSound01 = [SKAction playSoundFileNamed:[array objectAtIndex:0] waitForCompletion:NO];
}
I then have a contact listener activate a method that randomizes from the available sound actions and returns one of them:
-(void) didBeginContact:(SKPhysicsContact *)contact {
SKPhysicsBody *firstBody, *secondBody;
firstBody = contact.bodyA;
secondBody = contact.bodyB;
SKAction* randomSound;
//blah blah blah if categories match and whatnot, do this:
REP_BallSpawn* ball = (REP_BallSpawn*) firstBody.node;
randomSound = [self soundBankRandomizer:YES];
[ball runAction:randomSound];
And the method that randomizes:
-(SKAction*) soundBankRandomizer:(BOOL)isWallHit {
NSArray* array;
switch (isWallHit) {
case YES:
array = #[ //according sound actions
];
break;
case NO:
array = #[
ballSound01, //and other sound actions
];
default:
array = #[
//if neither case is true somehow, just choose from a bank of all sound actions
];
break;
}
int randomChoice = arc4random() % [array count];
SKAction* sound = (SKAction*)[array objectAtIndex:randomChoice];
return sound;
}
Beyond that, after 7 seconds the balls automatically despawn themselves, and I would assume any children with them (which I also assume should include actions, such as sound actions).
I even made a method to confirm no nodes are sticking around after they despawn:
[self enumerateChildNodesWithName:#"//*" usingBlock:^(SKNode *node, BOOL *stop) {
string = [NSString stringWithFormat:#"%# (())(()) %#", string, node];
}];
The result is then copied to the clipboard which results in a list of all nodes and children in the scene at the moment. (the " (())(()) " is in there for a unique pattern to search for in find/replace to make new lines)
Edit: I also noticed that CPU usage remains high after everything has despawned as well.
Please tell me I did something wrong!
===================================================
So I made some changes to my code. As mentioned below, I got it to work right, but upon some further testing, it appears to be finicky as to how exactly it gets implemented. Maybe someone else will see why, but it makes no sense to me.
METHOD THAT WORKS
Same as above with these changes:
-(void) setUpSounds {
NSArray* array = #[
#"ball_hit_01.m4a",
#"ball_hit_02.m4a",
#"ball_hit_03.m4a",
#"ball_hit_04.m4a",
#"wall_hit_01.m4a",
#"wall_hit_02.m4a",
#"floor_hit_01.m4a",
#"floor_hit_02.m4a",
];
SKAction* ballSound01 = [SKAction playSoundFileNamed:[array objectAtIndex:0] waitForCompletion:NO];
// etc
sounds = [NSArray arrayWithObjects:ballSound01, ballSound02, ballSound03, ballSound04, wallSound01, wallSound02, floorSound01, floorSound02, nil]; //is declared class wide
}
-(SKAction*) soundBankRandomizer:(BOOL)isWallHit {
int randomChoice = arc4random() % ([sounds count] / 2);
if (isWallHit) {
randomChoice += ([sounds count] / 2);
}
return [sounds objectAtIndex:randomChoice];
}
The contact listener is nearly identical (I removed the casting for the ball and applied the action to the world node instead). I've also tried it using a dictionary instead of an array and it works fine.
METHOD THAT DOESN'T WORK FOR SOME REASON
I would remove the soundBankRandomizer method, as well as remove any action calls from the contact listener. Instead, I added this method:
-(void)playSound:(BOOL)isWallHit {
int randomChoice = arc4random() % ([sounds count] / 2);
if (isWallHit) {
randomChoice += ([sounds count] / 2);
}
SKAction* randomSound = [sounds objectAtIndex:randomChoice];
[bgNull runAction:randomSound];
}
and then called it from the contact listener via:
[self playSound:YES]; //for wall hit
[self playSound:NO]; //for ball on ball action
For some reason, this resulted in the exact same performance issues I had initially. Does it make sense to anyone?!

I see a couple issues....By declaring the SKAction ballSound01 it'll stick around to be used again. Also I follow your thinking that by removing every node, it would remove every action, but again I think by declaring it, that won't happen. Also SKActions could be applied to multiple nodes, so I don't think you can really consider them children of the object they run on.
I think you are better off declaring an array which holds onto those SKActions, instead of creating it each time in the soundBankRandomizer.
Also I've never seen a switch statement with a bool variable like that. Just doing this...
if (isWallHit == YES) {
} else{
}
Is more to the point because the default block is never going to run anyway. Since isWallHit is either YES or NO, those two conditions are it, no need for the default.
You might want to consider making a Singleton class as a sound manager and keeping it totally separate from the nodes. So instead of calling...
randomSound = [self soundBankRandomizer:YES];
[ball runAction:randomSound];
You could call something like...
[[SoundManager sharedSounds] playRandomWallSound];
No need to make a specific node part of playing the sound.

Related

a way to count the number of nodes on-screen

Someone please push me in the right direction.
Here's the thing: what I'm trying to do is to limit the amount of projectiles (shot by player) that can be on screen. That limit is recorded in a constant. So while the number of projectile nodes on the screen is equal to that constant, the player will not be able to shoot any more.
Once a projectile goes off screen, that projectile is getting removed, decreasing the amount of on-screen nodes, and the player should be able to shoot again, of course without exceeding the limit. (The shots are being done using a push of a button, not tapping the screen in the direction you want to shoot, if that is important. You can only shoot one at a time.)
What kind of algorithm can I use to solve this? How to keep up the number of projectiles currently on-screen? How to let the system know WHEN to decrease that amount (I've been using collision detection, having a "screen bounds node") and WHAT node to remove (I've been recording node ID in the name, but looks like doing that the wrong way)? Please suggest a solid solution to this. Hope what I have requested is possible.
You have made two claims here:
A projectile is created when shot by a player.
A projectile is deleted when it goes off screen.
If you already have code removing the projectile when it goes off screen, you could just leverage a simple static count:
#implementation Projectile
static NSUInteger projectileCount = 0;
+ (instancetype)fireProjectile {
if (projectileCount < MAX_PROJECTILES) {
projectileCount++;
return [[Projectile alloc] init];
}
return nil;
}
- (void)dealloc {
projectileCount--;
}
#end
Not sure if I understand exactly what you want, but based on what I have read you could have an array of projectiles that contains the amount of projectiles you want to allow.
// create an array
NSMutableArray *projectiles = [NSMutableArray array];
// populate the array with your projectiles
for (int index = 0;index < kMaxProjectiles;index++)
{
Projectile *projectile = [[Projectile alloc]init];
[projectiles addObject:projectile]
}
Now whenever you want to shoot a projectile, remove it from that array and shoot it.
Projectile *projectile = [projectiles objectAtIndex:0];
[projectiles removeObjectAtIndex:0];
When it goes off the screen, remove it from the parent and push it right back into the array.
[projectile removeFromParent];
[projectiles addObject:projectile];
It's basically a pooling system where you would create your projectiles once and recycle them. When choosing to shoot, you can just check the count of the array to determine if you have a projectile available :
if (projectiles.count > 0)
{
// yep, there are projectiles in the pool, fire away
}
Added advantage of pooling is that you are not constantly creating and killing your projectiles, which will result in pauses when Garbage Collection occurs or if you ever tried to create alot of projectiles at once.

copy CCSprites between NSMutableArrays

i am copying a sprite from one NSMutableArray to another, but when i delete the CCSprite from the first NSMutableArray then it is also deleted in the second Array.
How can i prevent this?
In the init method the Arrays are initialized like below.
spriteTempArray = [[NSMutableArray alloc] init] ;
myPowerUpArray = [[NSMutableArray alloc] init] ;
This is the first method were the sprite is placed on screen and animated somewhere on the screen
CCSprite *powerUpSprite = [[CCSprite alloc] initWithFile:SpriteFileName ] ;
powerUpSprite.position = ccp(xcenter,ycenter);
powerUpSprite.scale = 0;
[self addChild:powerUpSprite z:20 tag:puTag];
[spriteTempArray addObject:powerUpSprite];
id zoomIn = [CCScaleTo actionWithDuration:0.2 scale:1] ;
id moveTo = [CCMoveTo actionWithDuration:0.2 position:ccp(winSize.width/2,120)];
[powerUpSprite runAction:zoomIn];
[powerUpSprite runAction:moveTo];
Then when de Sprite is touched, it's moved to the corner of the screen and also stored in another NSMutableArray (myPowerUpArray). But the delete action erases the sprite in both arrays.
CCSprite *powerUpSprite = [spriteTempArray objectAtIndex:0];
id zoomOut = [CCScaleTo actionWithDuration:0.2 scale:0.35] ;
id moveTo = [CCMoveTo actionWithDuration:0.2 position:ccp(winSize.width - 24,winSize.height -31 * myPowerUps -80)];
[powerUpSprite runAction:zoomOut];
[powerUpSprite runAction:moveTo]
[myPowerUpArray addObject:powerUpSprite];
[self deleteSpriteTempArray];
Below the sprite delete method.
-(void)deleteSpriteTempArray{
NSMutableArray *filesToRemove = [[NSMutableArray alloc] init];
for ( id obj in spriteTempArray) {
[filesToRemove addObject:obj];
[self removeChild:obj cleanup:YES];
}
[spriteTempArray removeObjectsInArray:filesToRemove];
}
I haven't observed the Array, other than de sprite is disappearing from the screen.
Your delete array method is odd. To begin why are you calling:
[self removeChild:obj cleanup:YES];
If your intent is not to remove the sprite from the scene? You are telling your code here to remove the sprite from the scene and to clean it up, but based on your post I don't get the impression you actually want that to happen at that moment. If it is your intent that it gets removed after it moves to the corner of your screen, then you should do the move and scale in a CCSpawn (let's simply call it moveScale) and in a CCSequence you should be adding that moveScale action followed by a block action that calls a method on the sprite to remove itself from its parent with cleanup:
CGSize winSize = [[CCDirector sharedDirector] winSize];
CCSprite* powerUp = ...;
float duration = 0.2f;
float desiredScale = 1.0f;
CGPoint desiredPosition = ccp(winSize.width / 2,120);
id zoomIn = [CCScaleTo actionWithDuration:duration scale:desiredScale] ;
id moveTo = [CCMoveTo actionWithDuration:duration position:desiredPosition];
id remove = [CCCallBlock actionWithBlock:^
{
[powerUp removeFromParentAndCleanup:YES];
}];
id moveScale = [CCSpawn actions:zoomIn, moveTo, nil];
id moveScaleRemove = [CCSequence actions:moveScale, remove, nil];
[powerUp runAction:moveScaleRemove];
Secondly why do you have a "files to remove" array in your deletion method that is built with objects to remove? That "files to remove" array is adding all objects from the sprite temp array to it, so it seems a little pointless. Just remove all objects from your sprite temp array. No need to build another temp list with everything to remove, just to essentially be saying remove everything from my original temp array list. That would only be useful if you were removing only SOME of the objects in the temp array. Since you are removing all, the "files to remove" array doesn't serve a purpose. Or at least that is what your code is showing that you are doing. Whether you intend on that behavior is another question.
Also if you have two arrays and an object in both, removing it from one array wouldn't have an impact on the second.
Another issue with your code is in the first code block. If you intend on running a move and a scale action together, they need to be executed via a CCSpawn. Instead you are telling it to scale, then immediately telling it to move which therefore it will be moving but not scaling as it moves. In other words both are not occurring at the same time. On the other hand if you intend for them to be done in sequence, you should be executing them via a CCSequence.
Another thing I noticed about your code is why are you deleting the entire sprite temp array when only one sprite was touched? Did you intend to only remove that one sprite that was touched? Why not just remove that one object, unless I am missing something?
[spriteTempArray removeObject:thisPowerUpIJustTouched];
A final issue I see is with your naming. Your code example indicates that the order of each example code block is the order in which events are occurring right? So why is the initial array you add powerups to called spriteTempArray while the array you add objects to after it is touched is called powerUpArray? Based on the code you've provided and the order they are shown, there is no point to the spriteTempArray. And even if there was, the naming looks backwards. Unless of course I'm missing something. It looks like your intention is to have a power ups array and when a power up is touched it is added to another array to mark that it is to be deleted. But as I've shown earlier in this post, it doesn't look like that second array has a true purpose since your desired behavior is A) not being done and B) would be done using the CCSpawn/CCSequence combo I showed earlier.
Hope this helped. I just woke up so hopefully I addressed your issue properly.

iOS - Adding ‘explosion’ effect (which breaks Sprite in to little pieces and then fall) to sprite in SpriteKit

I want to apply the same effect to a number of different sprites in my iOS game so am looking in to a general approach rather than an animation created in another program and using its images to create a UIImageView animation.
This effect is an ‘explosion’ type animation where my sprite’s image gets chopped in to different pieces, then, using the physics engine, those pieces get exploded out in different directions before falling down.
I’m new to SpriteKit, but am assuming I have to do this in Quartz? Then re-add the new images as sprites and apply the animation?
I don’t really know where to start, let alone how to continue with the rest of the steps.
Does anyone have any ideas?
Thanks.
1) Upon initialization of said exploding object, first we create a SKTextureAtlas which holds all of the images being animated. You must have the texture atlas resources available.
2) Load all of the relevant SKTextures into an array. This array should be private to the object to which the explosion is happening.
A safe way to do this:
- (void)loadContactImages {
NSMutableArray *frames = [NSMutableArray array];
SKTextureAtlas *explosionFrames = [SKTextureAtlas atlasNamed:#"explosionFrames"];
for(int i = 0; i < explosionFrames.textureNames.count; i++) {
NSString *textureName = [NSString stringWithFormat:#"explosionFrame_%d" , i];
SKTexture *texture = [explosionFrames textureNamed:textureName];
if(texture) {
[frames addObject:texture];
}
}
self.contactFrames = frames;
}
3) You will then define a method which will animate the explosion. Look into the SKAction header for more info. Plug in time per frame that makes sense to your explosion
- (SKAction *)runExplosion {
return [SKAction animateWithTextures:self.contactFrames timePerFrame:0.1];
}
4) As for the exploding pieces, you should preload them, so to limit the overhead of sending the pieces into random directions (with this we won't have the extra overhead of creating all those new sprites at the time of the explosion). Don't forget to give them physics bodies (SKPhysicsBody) and set the isAffectedByGravity property to YES!
- (SKAction *)explosionOfPieces {
return [SKAction runBlock:^ {
for(SKSpriteNode *piece in self.explodingPieces) {
piece.hidden = NO;
piece.position = self.position;
[self addChild:piece];
[piece.physicsBody applyImpulse:CGVectorMake(dx , dy)]; //specify the direction here
}
}];
}
5) Bringing it all together, expose a method to return a sequence of these actions. If you would like to have these two actions occur together, there is also a class method on SKAction called [SKAction group: (NSArray *)]
- (void)explosionSequence {
[self runAction: [SKAction sequence:#[ [self runExplosion] , [self explosionOfPieces]]]];
}
Note: Physics reactions occur in SKScene's. We expose this action so the scene can run it on your object within the scene. You will also want to conform to a protocol called SKPhysicsContactDelegate within your scene and call:
[myExplodingObject explosionSequence];
within the delegate method:
- (void)didBeginContact:(SKPhysicsContact *)contact
Within this SKPhysicsContact object lie two colliding bodies. Both of these bodies ("exploding object" and "explosion trigger") must have their SKPhysicsBody property initialized and their contactTestBitMask property set (so this object knows what it can collide with). I would also set the categoryBitMask property so we can directly introspect the bodies' type. We reference these colliding bodies like this:
- (void)didBeginContact:(SKPhysicsContact *)contact {
if(contact.bodyA.categoryBitMask == explodingObjectCategory && contact.bodyB.categoryBitMask == explodingTriggerCategory) {
[self.myExplodingObject explosionSequence];
}
}
Please look into Apple's docs for more info.

How to run the same block of code - Cocos2d

I am using cocos2d and have 2 AI sprites called TheEvilOne and TheEvilTwo. These 2 sprites call the same bloc of code from the same class sending the sprite as a parameter. However the game gets buggy as I run this code as only one of the sprites preforms the actions and my code stops working. My question is is it possible to call the same bloc of code for multiple sprites simultaneously or is there something wrong with my code. Below is an example of what I am running on my program.
-(void)LoadingEvilActions:(id)sender { //This is Getting Constantly Called
if(loaded == NO) {
theEvilOne = [CCSprite spriteWithFile:#"Evil_Alt_Idle_00.png"]; //These sprites have been declared in my .h file
[self addChild:theEvilOne z:200];
theEvilTwo = [CCSprite spriteWithFile:#"Evil_Alt_Idle_00.png"];
[self addChild:theEvilTwo z:200];
loaded = YES;
}
[self CheckCollision:theEvilOne];
[self setCenterofScreen:theEvilOne];
[self StayonScreen:theEvilOne];
[self AiCharacter:theEvilOne];
[self CheckCollision:theEvilTwo];
[self setCenterofScreen:theEvilTwo];
[self StayonScreen:theEvilTwo];
[self AiCharacter:theEvilTwo];
}
-(void)AiCharacter:(id)sender {
CCSprite *EvilCharacter = (CCSprite *)sender;
if (aiactionrunning == NO){
.... Do More Stuff // For E.G.
id jump_Up = [CCJumpBy actionWithDuration:0.6f position:ccp(randomjump, EvilCharacter.contentSize.height)
height:25 jumps:1];
id jump_Down = [CCJumpBy actionWithDuration:0.42f position:ccp(randomjump,-EvilCharacter.contentSize.height)
height:25 jumps:1];
id seq = [CCSequence actions:jump_Up,jump_Down, [CCCallFuncN actionWithTarget:self selector:#selector(stopAiAction:)], nil];
[EvilCharacter stopAllActions];
[EvilCharacter runAction:seq];
aiactionrunning = YES;
}
}
-(void)stopAiAction:(id)sender {
aiactionrunning = NO;
}
EDIT
I'm running random number generators within my AI Character method and many actions.
The Game already works with just 1 enemy sprite and I decided to try and implement a way to create many.
EDIT 2
I just added the part of the method that stops the sprites being constantly, I was trying to simplify all the code that was irrelevant to my question and forgot to add that part.
I changed the way I am calling my methods like suggested by LearnCocos2d not solving my problem but making it much simpler. I am also not using ARC
One of my sprites is the only one preforming the majority of actions however in some instances the other sprite may preform an actions aswell, but is mainly preformed one sprite. I think my main question is can I call the same method using different sprites passing the sprite as a parameter.
EDIT 3
I have figured out that my problem is there is a Boolean value flag that is enclosing my AiCharacter method, where when one sprite runs through the method it stops the other sprite running the method. Is there some way I can implement an array of records or such so each sprite have their own Boolean flags.
With making this 'array' is it possible to change the Boolean for TheEvilOne and TheEvilTwo using the temp sprite EvilCharacter without doing both separately.
If I understand your question correct you are trying to take one action and run it on more than one character at a time, like:
id move = [CCMoveTo actionWithDuration:0.5f position:ccp(0,0)];
[theEvilOne runAction:move];
[theEvilTwo runAction:move];
That wouldn't work because Evil 1 will not have anything performed on it since it was moved to running on Evil 2. When an action is running it is running on one target. Instead you would:
id move = [CCMoveTo actionWithDuration:0.5f position:ccp(0,0)];
[theEvilOne runAction:move];
[theEvilTwo runAction:[move copy]];
Assuming you are using arc. If not then you'd want to release the copy of course. When it comes to your particular example, why are you trying actions to call methods. Why not just call the method? It seems pointless as LearnCocos2D has pointed out.
Another potential problem you have is that you are constantly creating more and more sprites each time the method is called. Is that on purpose or are there only suppose to be one Evil 1 and one Evil 2? If so then you shouldn't be constantly creating more sprites.
Edit:
Remove your bool. Do something like this instead:
CCSprite *EvilCharacter = (CCSprite *)sender;
if ([EvilCharacter numberOfRunningActions] == 0){
...
}

Synchronize SKActions on two or more SKSpriteNode's?

I want to move two (or more) SKSpriteNodes in sync. Any difference will show. I tried to trigger the SKAction for each sprite in order and when the last one is finished it triggers a new move. But it turns out that the actions doesn't end in the same order they are started, which causes a slightly time difference that is noticeable.
Is there a way to run parallel SKActions on two or more sprites with a duration so that they end at exactly the same time or at least in the order they are started?
Here is an principle example of what is not working:
- (void)testMethod1{
SKSpriteNode *child_1=[arrayWithSprites objectAtIndex:1];
SKSpriteNode *child_2=[arrayWithSprites objectAtIndex:2];
//This doesn't work.
[child_1 runAction:[SKAction moveToX:20.0 duration:0.5]];
[child_2 runAction:[SKAction moveToX:20.0 duration:0.5]
completion:^{[self testMethod1];}];
//Actions might not be finished in the order they are started.
}
And here is a way I haven't tried yet but wonder if it might solve my problem:
- (void)testMethod2{
SKSpriteNode *child_1=[arrayWithSprites objectAtIndex:1];
SKSpriteNode *child_2=[arrayWithSprites objectAtIndex:2];
//Will this guarantee total syncronisation?
[self runAction:[SKAction group:[NSArray arrayWithObjects:
[SKAction runBlock:^{[child_1 runAction:[SKAction moveToX:20.0 duration:0.5]];}],
[SKAction runBlock:^{[child_2 runAction:[SKAction moveToX:20.0 duration:0.5]];}],
nil]]
completion:^{[self testMethod2];}];
}
I hope my English and thoughts are understandable.
//Micke....
Just add a container SKNode, and then they will both move at once:
SKNode *containerNode = [[SKNode alloc] init];
[containerNode addChild:node1];
[containerNode addChild:node2]; //add as many as necessary
[containerNode runAction:someAction]; //declare the action you want them both to perform
The solution by Tyler works perfectly.
But if you can't or don't want to do that, you should know that very likely actions are multithreaded and thus they can finish in any order, but they should still finish in the same frame.
To ensure they both ran to completion before running testMethod, you should perform the code in didEvaluateActions. Then you need to find a way to figure out whether both actions have finished. One way is to use a key.
[child_1 runAction:[SKAction moveToX:20.0 duration:0.5] withKey:#"action1"];
[child_2 runAction:[SKAction moveToX:20.0 duration:0.5] withKey:#"action2"];
Then check if both of these actions have run to completion by checking if they still exist:
-(void) didEvaluateActions
{
if ([child_1 actionForKey:#"action1"] == nil &&
[child_2 actionForKey:#"action2"] == nil)
{
// both actions have ended, start new ones here ...
}
}
Alternatively you can use a completion block that both actions run. Increase a NSUInteger counter variable and in the block increase the counter by 1, then test if the counter's value is equal to (or greater) than the number of concurrent actions. If it is, you know that both actions have run to completion:
__block NSUInteger counter = 0;
void (^synchBlock)(void) = ^{
counter++;
if (counter == 2)
{
[self testMethod1];
}
};
[child_1 runAction:[SKAction moveToX:20.0 duration:0.5] completion:synchBlock];
[child_2 runAction:[SKAction moveToX:20.0 duration:0.5] completion:synchBlock];
Is your framerate constant ? Your first example technically should work, however the completion order might be based on something else, like their draw order.
One concept you could employ is to temporarily encapsulate them in a container node, and then move just the container node via an action. When the action is complete remove them from the container and back to their original parent.
Are you finding that they move out of sync visually ? Or does your testMethod1 in some way require that both actions are complete ?
Although the container concept I have suggested will work for your specific example, it's not a valid option unless it's desired that the objects are moving as if they are connected. An example of where the container would not work, is if each object had to move to different locations in 1 second.

Resources