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.
Related
I'm fairly new to iOS development, so I apologize in advance if my question sounds not worthy of being on stackoverfow.
I am building a simple game with a home ViewController and a gameplay ViewController as far as the structure is concerned.
I've added a very simple function to kill activities to make sure nothing gets left behind, which gets called right before it leaves the gameplay ViewController. the following is the code I'm using:
private func cleanThis(){
//removing objects from array
activeEnemies.removeAll()
//removing objects from array
activeTargets.removeAll()
//removing the rest
let subViews = self.view.subviews
for subview in subViews{
subview.removeFromSuperview()
}
}
I must be missing something here, and when I test it without moving on to the homepage, there's a constant memory activity of 38MB. And I haven't figured out yet as to how to monitor what is still left in the ViewController.
Any help is much appreciated.
p.s. when there's no objects in ViewController, the memory activity should be 0, is this correct?
iOS doesn't use a garbage collector. It uses a something called automatic reference counting. The main way you get into trouble is cyclical references (A has a strong reference to B which has a strong reference to A).
Xcode Instruments will show you all memory allocations and deallocations, and can show memory leaks as well. Here's a screenshot to demonstrate:
I am trying to reset my game to the start screen every time this function is called by just creating a new scene:
func restart() {
let newScene = GameScene(size: view!.bounds.size)
newScene.scaleMode = .AspectFill
view!.presentScene(newScene)
}
The first time it is called, it works correctly. However, the second time it is called I get an (apparently very common) error:
fatal error: unexpectedly found nil while unwrapping an Optional value
I don't understand why I recieve this error in this scenario.
Is there a better way to reload the entire GameScene? Do I need to do anything with the old discarded GameScene instance?
You have probably defined this method inside your GameScene class, right?
Let's see what the API doc says about the view property of SKScene.
The view that is currently presenting the scene. (read-only) To
present a scene, you call the presentScene: method or
presentScene:transition: method on the SKView class. If the scene is
not currently presented, this property holds nil.
So I guess you are calling this method after the current scene has been removed from the view.
Is there a better way to reload the entire GameScene?
This approach is right, but you should call restart before the current scene has been removed from the view, otherwise it has no reference to your view.
Do I need to do anything with the old discarded GameScene instance?
Nope.
Once you replace the current scene with the new one, if you don't have strong references to the old scene, ARC does remove it from memory.
To be sure the current scene is being removed you have several easy tools:
1. Debug Navigator
While the game is running take a look ⌘ + 6 at the Debug Navigator in Xcode, then invoke restart several times.
If the used memory indicator grows up, then probably you are not releasing the GameScene from memory (or some other object).
Not releasing the GameScene is one of the worst thing that can happen in a videogame because a scene holds references (directly or indirectly) to every other node in you current level/screen. It happened during the development of my game so I know what I'm talking about :D
2. deinit
Add the deinit method to you GameScene
class GameScene : SKScene {
deinit {
print("The GameScene has been removed from memory")
}
}
deinit is automatically called when the object is removed from memory. Now each time you restart your scene, you should see the message
The GameScene has been removed from memory
on the console coming from the dead scene.
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];
I have encountered a delay/pause that I was not expecting and the reason so far has me scratching my head. I have a simple game setup where the UIViewController shows a number of UIButtons [PLAY GAME] [VIEW SCORES] etc. which in turn present a different SKScene
My problem is that when I try and set the visibility of these buttons to visible (perviously set to hidden in viewDidLoad) from the UIViewController they take about 5 seconds to actually show.
#implementation ViewController
- (void)presentTitleScene {
// SHOW BUTTONS
[[self logoLabel] setHidden:NO];
[[self gameButton] setHidden:NO];
[[self scoreButton] setHidden:NO];
[[self creditsButton] setHidden:NO];
// PRESENT SCENE
SKScene *titleScene = [TitleScene sceneWithSize:[[self spriteKitView] bounds].size];
[titleScene setName:#"TITLE_SCENE"];
[titleScene setScaleMode:SKSceneScaleModeAspectFill];
[(SKView *)[self view] presentScene:titleScene];
[self setCurrentScene:titleScene];
}
#end
What happens is all the code runs, the SKScene presents correctly, then after about 5-6 seconds the buttons appear? Is there anything I can do about this (force an update) or is it just a case of design it out or live with it?
This happens on both the simulator and the device.
EDIT:
Looking at the output log you can clearly see that after calling preloadTextureAtlases:withCompletionHandler: that execution jumps to another thread. The method preloadTextureAtlases:withCompletionHandler: is called on the main thread and its supposed to preload the textureAtlas(s) on a background thread, but I was under the impression that the completionHandler would call back onto the main thread, is this assumption right or am I wrong?
EDIT_002:
Moved to answer below.
With regards to preloadTextureAtlases:withCompletionHandler: the completionHandler gets called on a background thread, I would assume the same one that was used to preload the atlases. The problem that I was having was that I was using the completion handler to send an NSNotification to my viewController saying "the assets have loaded, start the game" The issue with this is that "Notifications are delivered in the same thread that they are sent from" so my game also started in the background thread. As a consequence the code that set the UIButtons to visible was also running on that same background thread, hence the delay in them reacting to either being made visible or hidden.
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];