After my game ends I have a button called play again but when it runs it throws and error saying Attemped to add a SKNode which already has a parent. My question is can I reset or dealloc the SKScene so that it is like a fresh slate, like the app was never run?
Assuming the code is being executed in your scene I think what you want is this..
MyScene *newScene = [[MyScene alloc]initWithSize:self.size];
[self.view presentScene:newScene];
Where MyScene is SKScene subclass.
Hopefully that is what you were looking for.
You can use [self removeAllChildren]; in your SKScene to remove all children nodes.
Other objects like arrays, strings, etc... you will have to deal with on a one to one basis.
You can check if a node already has a parent before adding by doing this:
if(myNode.parent == nil)
[self addChild:myNode];
Related
I am working with SpriteBuilder and Cocos2d to build a simple game, and I want to display an error message inside an if statement.
My problem is trying to initialize the CCNode I created in SpriteBuilder to show up on-screen.
I tried creating a CCNode layer and just creating all the objects via SpriteBuilder, but wasn't exactly sure how I was supposed to get that to show up on-screen as what I tried did not work correctly. I tried just using [self addChild:errorLayer] in the if statement and it crashed my app with the error message Argument must be non-nil, so I set up a breakpoint and errorLayer is nil, but I'm not sure how to make it non-nil.
I also tried creating a CCNode programmatically, but when the if-statement was run it didn't display anything on-screen. Here is the code I tried:
CCNode *errorLayer = [[CCNode alloc] init];
[errorLayer setContentSize:CGSizeMake(50, 100)];
[errorLayer setColor:[CCColor redColor]];
[self addChild:errorLayer];
Could anyone give me some tips on getting this to work? Thanks.
MainScene, which is the scene that the above code is called in, is initialized in AppController like this
- (CCScene*) startScene
{
return [CCBReader loadAsScene:#"MainScene"];
}
I use a dispatch_once NSObject to create data pointers. So all game asset pointers are made when the main viewcontroller appears. In order to play a game, the user taps a UIButton corresponding to a particular level on a UIViewController. Let me call it LevelSelectionController. When the game is over, the user will tap a label (SKLabel). And all actions and nodes will be removed.
[self removeAllActions];
[self removeAllChildren];
[self removeFromParent];
Moreover, an SKScene subclass for a particular level delegates the task of returning the user to LevelSelectionController to the viewcontroller presenting game SKView as follows.
- (void)closeScene {
SKView *spriteView = [[SKView alloc] init];
[spriteView presentScene:nil];
[self.navigationController popViewControllerAnimated:YES];
}
The only issue that I have is that the memory remains high when the user leaves the game scene (SKScene). The game requires a lot of assets. So when the game starts, the memory usage will jump to 200 MB. When the user returns to the original level selection view controller, the game simulator is still consuming 200 MB according to Activity Monitor. When the user enters a different level, the memory usage will jump by another 10 MB. So how can I release the memory for the last game once the user leaves SKScene?
I'm using ARC. The Xcode version is 5.1. The development target is iOS 7.1.
Thank you for your help.
-- Edit 1 --
I'm silly. I know what the problem is. When I close the scene, I'm creating a new SKView, which I then set it to nil to get of out the current scene. It works. But that should not be the way of doing it. Instead, I need to set the current SKView to a variable before presenting it. When I close the scene, I need to set that variable to nil. Hmm... I wasn't thinking.
-- Edit 2 --
There's little change when the current scene is presented with nil. Removing it from removeFromSuperview doesn't do much.
It has been noticed by a few people on SO that an SKScene gets deallocated when the containing SKView is removed from it's superview.
Have a look at these questions and their answers:
Deallocate SKScene after transition to another SKScene in SpriteKit
iOS 7 Sprite Kit freeing up memory
Also, try modifying the closeScene method like so:
- (void)closeScene {
SKView *spriteView = (SKView*)self.view;
[spriteView presentScene:nil];
[self.navigationController popViewControllerAnimated:YES];
}
Put NSLog() to SKScene's dealloc method to be sure that it's deallocating.
Also, resources may be not released just after reference count of your scene reaches 0. Due to internal optimizations assets may stay in memory until receiving Memory warning signal.
Been banging my head against this for a day now, and I really have no idea what's going on.
I have a very simple setup: an SKNode (let's call it base) which contains another SKNode (sub-base), as well as several SKShapeNode objects. At some time, I move one of the SKShapeNode objects (using removeFromParent) from the base node to the sub-base node. Then I apply an SKAction, which moves the node to some arbitrary position.
Except, that SKAction does not work when the SKShapeNode has been removed and added to the sub-base object. If I remove it from the sub-base, and put it back in the base, SKActions once again work.
I am completely stumped. Is there some property that gets set on a node when it's added to another node, which isn't getting properly reset when I remove it...? I can't imagine this is something that should be happening.
Any ideas would be so very welcome.
Update:
Here's some code that I can produce it with. This function is inside a subclass of SKNode. The class adds a bunch of SKShapeNodes, and it also has this other SKNode called testNode, so, without further ado:
-(void) removeThenAdd
{
[someNode removeFromParent];
[self.testNode addChild:someNode];
SKAction* action = [SKAction moveTo:CGPointMake(200, 200) duration:1];
SKNode* thatSameNodeJustAdded = [self.testNode.children objectAtIndex:0];
[thatSameNodeJustAdded runAction:action];
}
Another update!
I just found that, if I add an SKAction to the node whilst it is sitting inside the testNode, then after that remove it from the testNode and add it back to its original parent, the action is then activated. Like, what am I missing here? This must be some kind of designed behaviour I'm just not using right..
This seems to be a bug in the SDK. I had this issue because I wanted to make advanced sprites (sprites with children, emitters, etc) in their own scene files so that I could selectively load and then add them to my scenes. I came up with a work around using NSKeyedArchiver and NSKeyedUnarchiver -- Convert the node (or parent node) to data, and then back again, and the new object is ready to be added to a scene, if it's an emitter, or has child nodes that are emitters they will all be copied through and properly re-added. Here is the extension I made for swift, works like a charm:
extension SKNode {
// Pulling a node from one scene and putting it into another causes some problems with broken emitters :(
// Fix here is to archive and then unarchive the node before returning
class func nodeFromScene(nodeName : String, sceneFileName : String) -> SKNode? {
if let scene = SKScene(fileNamed: sceneFileName), node = scene.childNodeWithName(nodeName) {
let archive = NSKeyedArchiver.archivedDataWithRootObject(node)
return NSKeyedUnarchiver.unarchiveObjectWithData(archive) as? SKNode
}
return nil
}
}
I was also seeing this issue occur in my SpriteKit project. In my case I was removing a node from an SKScene file I'd created in the Scene Editor and adding it to my current scene.
Following some debugging it showed that the actions weren't being run at all. And upon further investigation I discovered why... it seems that when you add a node to the scene in this manner the isPaused property is set to true. Whether this is intentional or a bug I can't find out for sure.
From the docs:
https://developer.apple.com/documentation/spritekit/sknode/1483113-ispaused
isPaused
If the value is true, the node (and all of its descendants) are skipped when a scene processes actions.
So in order to "fix" this issue, before running any SKActions on the node, unpause the node:
node.isPaused = false
So in my project, I call an instance method called "-(void)fire" in the class "Survival.m":
-(void)fire {
NSLog(#"Firing");
CCSprite *sprite = [CCSprite spriteWithFile:#"bullet.png"];
sprite.position = player.position;
NSLog(#"%#",NSStringFromCGPoint(player.position));
[self addChild:sprite z:100];
}
When I do this, the sprite doesn't show on the screen.
The method is being called from another layer but since it logs "Firing" every time I tap the button, it's not the problem.
I am using a TMXTiledMap as well if that could cause any problems.
Please help, thanks!
EDIT---------
I can create sprites in the other layer, HUDLayer but not in the Survival layer which contains the player and the tiled map. If I create a sprite in the "init" method, it works correctly but If i do it in the method "fire" it doesn't work. The method "fire" is being called from the HUDLayer but I still now the method is being called since I see in the log that it says "Firing"
Could it be that:
1. The sprite is being created out of sight?
2. The sprite is not being created?
3. The sprite is not added to the correct parent?
Any suggestions?
to add a sprite in cocos2d do something like this
CCNode *parent = [self getChildByTag:kTagParentNode];
[parent addChild:sprite];
if that doesnt fix it, make sure the point where you are adding it is on the screen and try changing the z value to see if that helps
EDIT
add this under where you import files
enum {
kTagParentNode = 1,
};
Thanks Everybody, Solved it in some mysterious way. Thanks for the help anyway. I think I declared the layer in a wrong way.
Have a nice day!
I am using some static instance of a GameScene subclass of CCScene. Calling from GameScene
[[CCDirector sharedDirector] replaceScene:[MainMenuScene scene]];
doesn't trigger the dealloc method of the GameScene.
The method is called once I load again the scene (and a new GameScene is created):
+(id) sceneWithId:(int)sceneId
{
CCScene* scene = [CCScene node];
GameScene* gameScene = [[self alloc] initWithId:sceneId];
[scene addChild:gameScene z:0 tag:GameSceneLayerTagGame];
return scene;
}
-(id) initWithId:(int)sceneId
{
CCLOG(#"scene With id");
if ((self = [super init]))
{
instanceOfGameScene = self;
//ONLY NOW the previous object becomes unreferenced and the memory management system is allowed to deallocate it
I wanted to understand if there is a way of forcing the dealloc method to be called every time I replace a (static) scene and not only when the memory gets "freed".
Or, if I should instead, write some cleanups methods that stops ongoing processes that I don't want to effect the MainMenuScene (e.g. I have put a stop background music method call in the dealloc method of GameScene but as also the background music is in a static class -and is not added to GameScene as child- then it keeps playing once back in the MainMenuScene).
My quick fix proposal hence is to do something like this:
[self stopAllStuffInOtherStaticClassesThatAreRelatedOnlyToGameScene];
[[CCDirector sharedDirector] replaceScene:[MainMenuScene scene]];
Is this a good approach?
EDIT: when is sensible to add this code in the dealloc method?
[self removeAllChildrenWithCleanup:TRUE];
I smell bad practice. Never, ever keep a static instance of a node around. Especially not outside the scene hierarchy. It just breaks cocos2d's way of handling memory management.
If you need to preserve state, save this state to a separate class but by all means let the scene go when you change scenes. Then restore the state when the scene inits again.
You can not force the dealloc method. Wanting to do so is a sign of code smell. You can however override the -(void) cleanup method to run de-initialization code before dealloc and after the node has been removed as child.
EDIT: when is sensible to add this code in the dealloc method?
[self removeAllChildrenWithCleanup:TRUE];
Never. Ever.
Again, if you find you need to do this, there's a terrible bug somewhere. Cocos2D will have removed the child nodes by this time. In fact, it does so during the cleanup method, one way to cause cocos2d not to remove a node's children is when you override cleanup and not call [super cleanup].
Or perhaps when you keep it as a static instance, so it'll never actually call the cleanup method. And even worse would then continue to run scheduled updates and actions.
You trouble is that you don't release your game scene. Create it as
GameScene* gameScene = [[[self alloc] initWithId:sceneId] autorelease];
and it will be deallocated right.
Anyway, I don't see any need to create game scene as a child of other scene, but maybe i don't know something about your project.
And I cannot understand, what do you mean under "static CCScene"
Using 'Instruments' under Developer Tools, I was able to make some progress on this matter:
When the scene is created it should follow a few simple rules to keep the memory low:
Allocate the sprites from smallest to largest
Make sure to call [self unscheduleAllSelectors]; at the right times
Override the -(void)dealloc method with the following code if you want to see significant memory improvements in your case, as this will hit every time the CCScene is loaded:
// Clean up memory allocations from sprites
[[CCSpriteFrameCache sharedSpriteFrameCache] removeUnusedSpriteFrames];
[[CCDirector sharedDirector] purgeCachedData];
[[CCSpriteFrameCache sharedSpriteFrameCache] removeSpriteFrames];
[CCSpriteFrameCache purgeSharedSpriteFrameCache];