Cocos2d - removeSpriteFrames and removeAllTextures do not work straight away - memory-management

EDIT:
I added the following code in both the dealloc and cleanup method of the previous scene (the one from which I call replaceScene but it does not have any effect. Even after 1/2/5 seconds from when InstructionScene has being created the memory does still contain the assets from the previous scene. The only way to force the removal of those is to remove them from the new scene 0.1f seconds after the scene is being created (via a Callback). This is kind of weird.
Here is the code of the previous scene cleanup and daelloc methods:
-(void) cleanup
{
CCLOG(#"");
CCLOG(#"");
CCLOG(#"PlanetSelection Menu cleanup");
CCLOG(#"");
[super cleanup];
[planetLayer removeAllChildrenWithCleanup:YES];
[self removeAllChildrenWithCleanup: YES];
[[CCSpriteFrameCache sharedSpriteFrameCache] removeSpriteFramesFromFile:textureFileName];
[[CCTextureCache sharedTextureCache] removeUnusedTextures];
[[CCSpriteFrameCache sharedSpriteFrameCache] removeUnusedSpriteFrames];
[CCAnimationCache purgeSharedAnimationCache];
}
-(void) dealloc
{
CCLOG(#"Dealloc gets caled");
[CCAnimationCache purgeSharedAnimationCache];
[[CCDirector sharedDirector] purgeCachedData];
[[CCSpriteFrameCache sharedSpriteFrameCache] removeUnusedSpriteFrames];
[[CCSpriteFrameCache sharedSpriteFrameCache] removeSpriteFramesFromFile:textureFileName];
}
Original question:
I got several scenes in my game and so far I used the following bit of code at the beginning of each scene to remove previously stored textures. However there is a case in which this doesn't work: when I replace a scene (lets call it A) with a new scene (lets call it B) with no sprites and only some label created from font image sheet.
[[CCDirector sharedDirector] replaceScene: [InstructionsScene sceneWithLevelName:FIRST_LEVEL]];
The new object does get created too fast as the following call doesn't seem to have any effect:
-(id) initWithLevelName:(LevelName)name
{
if ((self = [super init]))
{
//Remove stuff from previous scene
[[CCSpriteFrameCache sharedSpriteFrameCache] removeSpriteFrames];
[[CCSpriteFrameCache sharedSpriteFrameCache] removeUnusedSpriteFrames]; //Not really necessary
//Use these
[[CCTextureCache sharedTextureCache] removeUnusedTextures]; //Not really needed
[[CCTextureCache sharedTextureCache] removeAllTextures];
[[CCDirector sharedDirector] purgeCachedData];
[CCAnimationCache purgeSharedAnimationCache];
....
}
}
At the moment in which the replace scene method is being called the two CCLayer (CCScane) objects are alive at the same time. However texture from the previous scene do not get removed. The same code works perfectly if a sprite sheet is being added and used in the Instruction scene. A tweak to this is to use a callback to a selector removing all textures after 0.1f, however this is not very elegant and smooth:
[self runAction:[CCSequence actionOne:[CCDelayTime actionWithDuration:0.1f] two:[CCCallFunc actionWithTarget:self selector:#selector(removeStuffFromPreviousScene)]]];
Is this a known issue? It could cause potential crashes.
I paste here the code to make sure you can try it out:
//
// InstructionsScene.h
//
// Created by mm24 on 09/09/13.
// Copyright 2013 mm24. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "cocos2d.h"
#import "CommonEnumerations.h"
#interface InstructionsScene : CCLayer {
LevelName levelName;
float startTime;
CCLabelBMFont * levelNameTitle;
CCLabelBMFont * levelSubtitle;
CCLabelBMFont * instructionsHeader;
CCLabelBMFont * instructions;
}
+(id)sceneWithLevelName:(LevelName)name;
#end
//
// InstructionsScene.m
//
// Created by mm24 on 09/09/13.
// Copyright 2013 mm24. All rights reserved.
//
#import "InstructionsScene.h"
#import "ShooterScene.h"
#import "AppDelegate.h"
#import "mach/mach.h"
#implementation InstructionsScene
+(id)sceneWithLevelName:(LevelName)name
{
CCScene * scene = [CCScene node];
InstructionsScene * layer = [[self alloc] initWithLevelName:name];
[scene addChild:layer];
return scene;
}
-(id) initWithLevelName:(LevelName)name
{
if ((self = [super init]))
{
//Remove stuff from previous scene
[[CCSpriteFrameCache sharedSpriteFrameCache] removeSpriteFrames];
[[CCSpriteFrameCache sharedSpriteFrameCache] removeUnusedSpriteFrames];
//Use these
[[CCTextureCache sharedTextureCache] removeUnusedTextures];
[[CCTextureCache sharedTextureCache] removeAllTextures];
[[CCDirector sharedDirector] purgeCachedData];
[CCAnimationCache purgeSharedAnimationCache];
//Try out and use it. Not compulsory
[self removeAllChildrenWithCleanup: YES];
CCLOG(#"init with level name");
levelName = name;
startTime = 10.0f;
levelNameTitle = [CCLabelBMFont labelWithString:#"Title" fntFile:#"bitmapFontTest.fnt"];
levelNameTitle.position = CGPointMake(160.0f, 420.0f);
levelNameTitle.anchorPoint = CGPointMake(0.5f, 0.5f);
levelNameTitle.scale = 1.3f;
[self addChild:levelNameTitle z:1] ;
levelSubtitle = [CCLabelBMFont labelWithString:#"Subtitle" fntFile:#"bitmapFontTest.fnt"];
levelSubtitle.position = CGPointMake(160.0f, 400.0f);
levelSubtitle.anchorPoint = CGPointMake(0.5f, 0.5f);
levelSubtitle.scale = 0.7f;
[self addChild:levelSubtitle z:1] ;
instructionsHeader = [CCLabelBMFont labelWithString:#" Instructions " fntFile:#"bitmapFontTest.fnt"];
instructionsHeader.position = CGPointMake(160.0f, 240.0f);
instructionsHeader.anchorPoint = CGPointMake(0.5f, 0.5f);
instructionsHeader.scale = 0.7f;
[self addChild:instructionsHeader z:1] ;
instructions = [CCLabelBMFont labelWithString:#"Press any key" fntFile:#"bitmapFontTest.fnt"];
instructions.position = CGPointMake(160.0f, 200.0f);
instructions.anchorPoint = CGPointMake(0.5f, 0.5f);
instructions.scale = 0.7f;
[self addChild:instructions z:1] ;
[[CCDirector sharedDirector] resume];
// [self runAction:[CCSequence actionOne:[CCDelayTime actionWithDuration:0.1f] two:[CCCallFunc actionWithTarget:self selector:#selector(removeStuffFromPreviousScene)]]];
[self runAction:[CCSequence actionOne:[CCDelayTime actionWithDuration:1.0f] two:[CCCallFunc actionWithTarget:self selector:#selector(report_memory)]]];
[self runAction:[CCSequence actionOne:[CCDelayTime actionWithDuration:2.0f] two:[CCCallFunc actionWithTarget:self selector:#selector(report_memory)]]];
[self runAction:[CCSequence actionOne:[CCDelayTime actionWithDuration:5.0f] two:[CCCallFunc actionWithTarget:self selector:#selector(report_memory)]]];
[self callBackReplace];
}
return self;
}
-(void) removeStuffFromPreviousScene
{
//Use these
[[CCSpriteFrameCache sharedSpriteFrameCache] removeSpriteFrames];
[[CCSpriteFrameCache sharedSpriteFrameCache] removeUnusedSpriteFrames];
//Use these
[[CCTextureCache sharedTextureCache] removeUnusedTextures];
[[CCTextureCache sharedTextureCache] removeAllTextures];
[[CCDirector sharedDirector] purgeCachedData];
[CCAnimationCache purgeSharedAnimationCache];
}
-(void) report_memory {
CCLOG(#"");
CCLOG(#"");
CCLOG(#"InstructionScene info:");
CCLOG(#"");
[[CCTextureCache sharedTextureCache] dumpCachedTextureInfo];
struct task_basic_info info;
mach_msg_type_number_t size = sizeof(info);
kern_return_t kerr = task_info(mach_task_self(),
TASK_BASIC_INFO,
(task_info_t)&info,
&size);
if( kerr == KERN_SUCCESS ) {
NSLog(#"Memory in use (in bytes): %u", info.resident_size);
} else {
NSLog(#"Error with task_info(): %s", mach_error_string(kerr));
}
}
-(void) nextSuggestionPressed
{
[self stopAllActions];
[self callBackReplace];
}
-(void) callBackReplace
{
[self runAction:[CCSequence actions:
[CCDelayTime actionWithDuration:startTime] ,
[CCCallFunc actionWithTarget:self selector:#selector(replaceWithShooterScene)],
nil]
];
}
-(void) replaceWithShooterScene
{
[[CCDirector sharedDirector] replaceScene:[ShooterScene sceneWithLevelName:levelName]];
}
#end

Since you initialize the new scene within an already running scene, both scenes are alive at the same time. If you try to remove "unused" resources during init of the new scene, nothing (or not much) will happen since the existing scene is still using these assets.
You can only do two things:
a loading scene in between the two scenes to allow the first scene to deallocate before the next scene is initialized
unloading all unused assets during dealloc of a scene, not when initializing a new one
The loading scene is the best approach if both scenes use a lot of unique assets, so having both in memory at the same time may cause memory warnings or even the app forced to terminate due to memory pressure.
The other approach is best used in all other cases. Just be careful to unload specific resources rather than using the "unused" methods because another scene may currently be using this resource and will be forced to reload it if the other scene removes it.
PS: as long as your app isn't under memory pressure even on devices with the least amount of memory you shouldn't remove resources simply to prevent them from being reloaded again and again. Switching scenes will be a lot faster with already cached textures.

Related

Music through Sprite Kit Scenes?

So I have a sprite kit game and I'm adding music to it. The music worked fine and worked until I decided to add a slider to control the music volume. So to do this I added another scene for the "options menu." The problem is, when I exit the scene to go to the actual game interface, the game music completely disappears. Also I'm having trouble removing the actual slider. My code for the options scene will be listed down below. (It is still in the making) I'm new to this website so I'm sorry if the code is messed up.
#interface OptionsScene ()
#property BOOL contentCreated;
#end
#implementation OptionsScene
-(id)initWithSize:(CGSize)size
{
if (self = [super initWithSize:size]) {
self.anchorPoint = CGPointMake(0.5, 0.5);
}
return self;
}
-(void)didMoveToView:(SKView *)view
{
NSString *music = [[NSBundle mainBundle] pathForResource:#"InAmberClad" ofType:#"mp3"];
backGroundMusic = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:music] error:NULL];
backGroundMusic.delegate = self;
backGroundMusic.numberOfLoops = -1;
[backGroundMusic play];
CGRect frame = CGRectMake(200, 300, 150, 10);
_volumeSlider = [[UISlider alloc] initWithFrame:frame];
[_volumeSlider addTarget:self action:#selector(volumeControl) forControlEvents:UIControlEventValueChanged];
_volumeSlider.maximumValue = 1;
_volumeSlider.minimumValue = 0;
_volumeSlider.continuous = YES;
[_volumeSlider setValue:0.5];
[_volumeSlider setBackgroundColor:[UIColor blackColor]];
if (!self.contentCreated) {
[self.view addSubview:_volumeSlider];
[self createContents];
self.contentCreated = YES;
}
}
-(void)createContents
{
self.scaleMode = SKSceneScaleModeAspectFill;
}
-(void)volumeControl
{
[backGroundMusic setVolume:_volumeSlider.value];
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
SKScene *options = [[MyScene alloc] initWithSize:self.size];
[self.view presentScene:options];
}
#end
I think if you don't delegate the backgroundMusic to self, this problem will be solved. Also, before play the music, run a [backGroundMusic prepareToPlay]; will improve performance.
Here is a link to a tutorial, on page search for "Music" to see how he did it.
Ray Wenderlich

Cocos 2d: adding Sprite on a second loaded scene crashes

I have a cocos2d application for mac, with two scenes. In scene one, when I push a button it loads the second scene by using:
[[CCDirector sharedDirector] replaceScene:[CompetitiveScene node]];
Now, on this second CCScene, in the init method I load a texture using:
CCSpriteBatchNode *parentNodeBullet = [CCSpriteBatchNode batchNodeWithFile:#"bullet.png" capacity:100];
This makes the application crash on CCTextureCache.m : 276
dispatch_sync(_dictQueue, ^{
tex = [_textures objectForKey: path];
});
Apparently _textures has 0 key/values
I have no idea why is this happening, it's such a simple code! In scene1 I simply have one button and one label ...
It gets more strange because if I change my workflow and load the CompetitiveScene first, it works perfectly..
Any idea?
EDIT1
If I perform
CCSprite *player = [CCSprite spriteWithFile:#"bullet.png"]; instead of using the CCSpriteBatchNode, I have the exact same problem :/
EDIT2
If I perform CCTexture2D *texture = [[CCTextureCache sharedTextureCache] addImage:#"bullet.png"];, same problem
EDIT3
Here is the code of the project
First layer
// CompetitiveLayer.m
#import "CompetitiveLayer.h"
#import "cocos2d.h"
#import "CollectableIncreaseAmmo.h"
enum {
kTagParentNode = 1,
};
#interface CompetitiveLayer()
-(void) addNewSpriteAtPosition:(CGPoint)p;
#end
#implementation CompetitiveLayer
-(id) init
{
if( (self=[super init])) {
CGSize s = [CCDirector sharedDirector].winSize;
// enable events
self.mouseEnabled = YES;
// Use batch node. Faster
/*
CCSpriteBatchNode *parentNodeBullet = [CCSpriteBatchNode batchNodeWithFile:#"bullet.png" capacity:10];
spriteTextureBullet_ = [parentNodeBullet texture];
*/
NSImage *img = [NSImage imageNamed:#"bullet.png"];
if(img == nil) // log image is nil
{
CCLOG(#"NIL");
}
CCSpriteBatchNode *parentNodeBullet = [[[CCSpriteBatchNode batchNodeWithFile:#"bullet.png" capacity:15] retain] autorelease];
spriteTextureBullet_ = [parentNodeBullet texture];
[self addChild:parentNodeBullet z:0 tag:kTagParentNode];
[self addNewSpriteAtPosition:ccp(s.width/2, s.height/2)];
}
return self;
}
-(void) addNewSpriteAtPosition:(CGPoint)p
{
CCLOG(#"Add sprite %0.2f x %02.f",p.x,p.y);
/*
CCSprite *player = [CCSprite spriteWithFile:#"bullet.png"];
player.position = ccp(p.x, p.y);
[self addChild:player];
*/
CollectableIncreaseAmmo *sprite2 = [CollectableIncreaseAmmo spriteWithTexture:spriteTextureBullet_];
[sprite2 setPosition: ccp( p.x, p.y)];
[self addChild:sprite2];
/*
CCTexture2D *texture = [[CCTextureCache sharedTextureCache] addImage:#"bullet.png"];
CollectableIncreaseAmmo *sprite2 = [CollectableIncreaseAmmo spriteWithTexture:texture];
[sprite2 setPosition: ccp( p.x, p.y)];
[self addChild:sprite2];
*/
}
- (BOOL) ccMouseDown:(NSEvent *)event
{
CGPoint location = [(CCDirectorMac*)[CCDirector sharedDirector] convertEventToGL:event];
[self addNewSpriteAtPosition: location];
return YES;
}
- (void) dealloc
{
[super dealloc];
}
#end
Second layer
// TitleLayer.m
#import "TitleLayer.h"
#import "CompetitiveScene.h"
#import "cocos2d.h"
#interface TitleLayer()
-(void) createButtons;
-(void) createTitle;
#end
#implementation TitleLayer
-(id) init
{
if( (self=[super init])) {
// enable events
#ifdef __IPHONE_OS_VERSION_MAX_ALLOWED
self.touchEnabled = YES;
self.accelerometerEnabled = YES;
#elif defined(__MAC_OS_X_VERSION_MAX_ALLOWED)
self.mouseEnabled = YES;
#endif
// Create buttons
[self createButtons];
// Create title
[self createTitle];
//[self scheduleUpdate];
}
return self;
}
-(void) createButtons
{
CCMenuItemLabel *competitiveGameButton = [CCMenuItemFont itemWithString:#"Fight" block:^(id sender){
/*
CCScene *s = [CCScene node];
id child = [CompetitiveLayer node];
[s addChild:child];
[[CCDirector sharedDirector] replaceScene: s];
*/
// [[CCDirector sharedDirector] replaceScene: [CCTransitionZoomFlipX transitionWithDuration:1 scene:[CompetitiveScene node] ]];
[[CCDirector sharedDirector] replaceScene:[CompetitiveScene node]];
// We can also push and pop the scene
}];
CCMenuItemLabel *coperativeGameButton = [CCMenuItemFont itemWithString:#"Coperate" block:^(id sender){
/*
CCScene *s = [CCScene node];
id child = [TitleLayer node];
[s addChild:child];
[[CCDirector sharedDirector] replaceScene: s];
*/
}];
CCMenu *menu = [CCMenu menuWithItems:competitiveGameButton, coperativeGameButton, nil];
[menu alignItemsVertically];
CGSize s = [[CCDirector sharedDirector] winSize];
menu.position = ccp(s.width/2, s.height/2);
[self addChild: menu z:-1];
}
-(void) createTitle
{
CGSize s = [CCDirector sharedDirector].winSize;
CCLabelTTF *label = [CCLabelTTF labelWithString:#"Select type of game" fontName:#"Marker Felt" fontSize:32];
[self addChild:label z:0];
[label setColor:ccc3(0,0,255)];
label.position = ccp( s.width/2, s.height-50);
}
- (void) update:(ccTime) time {
CCLOG(#"Testing update");
}
- (void) dealloc
{
[super dealloc];
}
#end

NSMutableArray, add sprite and remove last object (last added sprite)?

-(void)createSprite{
CCSprite *shotV = [CCSprite spriteWithFile:#"green.png"];
[self addChild:shotV z:1];
[shotVArray addObject:shotV];
NSlog(#"%#",shotV);
}
-(void)aSpecialCase{
[self removeChild:[shotVArray lastObject] cleanup:YES];
}
I'm not getting this to work.
The function "createSprite" spams out sprites. "In aSpecialCase" I want to remove the last sprite that was created. Also hoping that removing it will end the current CCSequence for that instance.
-(void)aSpecialCase{
[self removeChild:[shotVArray lastObject] cleanup:YES];
}
This only removes the sprite from layer.. Doesn't remove it from array itself...
So better way is..
-(void)aSpecialCase{
CCSprite *sprite = [pshotVArray lastObject];
[self removeChild:sprite cleanup:YES];
[pshotVArray removeObject:sprite];
}
Hope this helps.. :)

Cocos2D - When I animate second sprite, first stops animating

So the issue here is when I create one animated projectile, everything is fine. As soon as the user creates a second, the first stops animating.
Alright, here's how I'm setting it up in my init:
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:
#"acorns.plist"];
CCSpriteBatchNode *spriteSheet = [CCSpriteBatchNode
batchNodeWithFile:#"acorns.png"];
[self addChild:spriteSheet];
NSMutableArray *flyingFrames = [NSMutableArray array];
for(int i = 1; i <= 4; ++i) {
[flyingFrames addObject:
[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:
[NSString stringWithFormat:#"acorn%d.png", i]]];
}
CCAnimation *flying = [CCAnimation
animationWithFrames:flyingFrames delay:0.5f];
self.flying = [CCRepeatForever actionWithAction:
[CCAnimate actionWithAnimation:flying restoreOriginalFrame:NO]];
Then, in my create bullets method which is called when the user taps the screen, I do:
CCSprite *bullet = [CCSprite spriteWithSpriteFrameName:#"acorn1.png"];
bullet.position = CGPointMake(140.0f, FLOOR_HEIGHT+145.0f);
[bullet runAction:_flying];
[self addChild:bullet z:9];
[bullets addObject:bullet];
So the first time the user taps, everything works fine. The second time, an animated bullet is created, but the existing one stops animating, and so on.
I believe each sprite should have its own actions, ie you cant reuse an action while the action is in progress. Something like:
CCSprite *bullet = [CCSprite spriteWithSpriteFrameName:#"acorn1.png"];
bullet.position = CGPointMake(140.0f, FLOOR_HEIGHT+145.0f);
id _fly=[CCAnimation animationWithFrames:flyingFrames delay:0.5f];
id _flyForever = [[CCRepeatForever _fly];
[bullet runAction:_flyForever];
[self addChild:bullet z:9];
[bullets addObject:buller];
The flyingFrames can be referenced by multiple animations, so you must take care of retaining the array for reuse.

Sprite not visible at times

I have a cocos2d based iphone app with a problem. When the user pauses the game and then hits the resume button, some CCSprites will disappear from the screen.
This behavior is random, no pattern followed. I just know that this only happens when the user resumes the game.
Here is the code, what am I doing wrong?
I first thought it might be a memory management problem, but I never get any EXC_BAD_ACCESS when the user hits resume... So the sprites probably still exist.
The sprites are a property within an object I'll call "myobject".
In myObject.h I have:
#interface myObject : CCNode{
CCSprite *_sprite1,*_sprite2;
// some other code
}
#property (nonatomic,retain) CCSprite *sprite1,*sprite2;
And in myObject.m file:
#synthesize sprite1=_sprite1;
#synthesize sprite2=_sprite2;
+(id)create:(CCLayer*)scene{
myobject.sprite1 = [CCSprite spriteWithFile:spriteFile];
[scene addChild:myobject.sprite1];
// same for sprite2
}
-(void) move:(ccTime)dt{
//SOMECODE
self.sprite1.position=ccp(self.x,self.y); // same for sprite2
}
then they get moved around with a function called on the myobject.
In the main scene, here is how these objects are created and moved around:
myObject *myObject;
NSMutableArray *_myObjects;
#implementation HelloWorld
+(id) scene
{
CCScene *scene = [CCScene node];
HelloWorld *layer = [HelloWorld node];
[scene addChild:layer z:0 tag:33];
return scene;
}
-(id) init
{
if( (self=[super init] )) {
_myObjects = [[NSMutableArray alloc] init];
// some other code
}
}
-(void) addObject(){
myObject=[myObject create:self];
[_fallingObjects addObject:fallingObject];
}
-(void) nextFrame:(ccTime) dt{
for(myObject *theObject in _myObjects){
[theObject move:dt];
}
}
// And here is the function that does the pause/unpause, here it is:
- (void) pauseGame{
if(pauseStatus==0){
[[CCDirector sharedDirector] pause];
pauseStatus=1;
// some code to display menu etc... such as:
[self addChild:pauseMenu z:10];
}
else{
[self removeChild:pauseMenu cleanup:YES];
[self removeChild:scoreLabel cleanup:YES];
[self removeChild:highscoreLabel cleanup:YES];
[self removeChild:titleLabel cleanup:YES];
[self removeChild:pauseLayer cleanup:YES];
[[CCDirector sharedDirector] resume];
pauseStatus=0;
}
}
==EDIT===
I have discovered that this problem is also true for sprites that I add directly to my scene, such as the sprite clown added as shown below:
CCSprite *clown;
#implementation HelloWorld
-(id) init
{
if( (self=[super init] )) {
// some code
clown = [[CCSprite spriteWithFile:#"clown.png"] retain];
[self addChild:clown z:2];
// some more code
}
}
#end
==END OF EDIT===
There's something odd about your setup. FallingObject is a CCNode that contains two sprites. Yet instead of adding the sprites to the FallingObject class (self), and then adding FallingObject to the scene, you are adding the sprites directly to the scene. This leaves the FallingObject node outside of the scene hierarchy.
That's also why you have to do things like updating sprite positions manually which would otherwise require no code at all:
self.sprite1.position=ccp(self.x,self.y);
For what it's worth, I suppose that due to the nonconformity of this setup you may be accidentally moving the sprites outside the screen, or the node either isn't properly paused or resumed.

Resources