1

I have an SKScene which is the overworld for my 2D game. It's like a neighborhood that the player can explore. The neighborhood is full of houses. Players can freely enter and exit houses. When the player enters a house, I call skView.presentScene(newHouse) to present the interior of the house as a new scene. When the player exits the house, I call skView.presentScene(overworld) to present the neighborhood again. The player is likely to enter & exit houses many times as they explore the neighborhood. Thus, the neighborhood scene and new instances of the house scene are presented many times.

The problem is that each time I present a house scene, there is a spike in the memory. This is expected since we are loading a new house scene. But when I exit a house scene and return to the neighborhood scene, the amount of memory use does not go down. I've tested this by entering and then exiting houses over and over. The memory use climbs by a consistent amount (~2 MB) every time I enter a house. Eventually, the amount of memory use becomes very large and the game starts dropping frames (but only in the house scenes, not in the neighborhood scene) and eventually becomes unplayable.

Why is this happening?

When working with SpriteKit I thought that best practice is to use presentScene to transition the player to a substantially different "realm" of the game world (e.g. a different level), which is what I believe I am doing here. I also thought that you are not supposed to take any action to "unload" your old scene when you load and then transition to a new scene. Indeed, the SKView documentation does not have any methods for cleaning up unneeded objects from your old scene when you present a new scene. I thought that you are supposed to trust the OS to handle the job of removing the old scene's objects from memory. That is why I simply present new scenes and don't worry about old scenes taking up memory.

I profiled the app in Instruments to check for memory leaks (Instruments is still a bit over my head at this point). I did find some memory leaks that looked very small, but no leaks that seemed to be directly causing the large and consistent memory spikes that are happening each time I present a new house scene.

Am I doing something wrong?

I believe I am taking the correct approach by presenting new scenes and letting the OS handle the job of cleaning up the old scenes. But maybe I am making a mistake in my app design that is causing this memory issue. When I present a new house scene, I pass some information from the neighborhood scene to the new house scene. This information is related to things like the appearance of the house (color, texture, contents, etc.) which is necessary because the houses are procedurally generated: each one is unique. And when I return to the neighborhood scene, I pass some information from the house scene to the neighborhood scene. But maybe the way that I am passing information between scenes is somehow unintentionally causing objects to be retained in memory. If so, what should I do to make sure that this does not happen? Is there some code I should run to clear the unwanted objects from memory when I present a new scene?

I did notice that the SKScene documentation has several methods related to presenting a scene: sceneDidLoad(), willMove(from:), and didMove(to:). Which makes me wonder if I should be using these methods somehow to try and clear unneeded objects from memory when I transition between different scenes.

It may be that my app architecture is simply bad (it's already causing other problems for me). If so, then the solution would be to start by overhauling my app architecture to improve it. So basically, I am trying to determine if my bad app architecture is causing this memory bloat problem, or if the cause is related to SpriteKit and the way that scenes are presented.

peacetype
  • 1,928
  • 3
  • 29
  • 49
  • 2
    I've worked with SpriteKit extensively, and I haven't experienced this. It's VERY easy to create reference loops using SpriteKit, since there are references back and forth under the hood. Perhaps you should check whether the scene is being dealloc'ed at all, you could put a timer in it and let it print a message every second or something like that, just to see if it's still hanging around after it should've been eaten by garbage collection. Alternatively it may be the spritesheets, see if they're around 2mb in size and perhaps pass them around between scenes. – Theis Egeberg Mar 05 '18 at 20:05
  • 3
    if you want to check if your scene is being deallocated just put in the deinit func and see if it is getting called when you leave the scene. If it doesn't you have something with a strong reference that is preventing it from de-initing – Ron Myschuk Mar 05 '18 at 20:28
  • Do you use SKTileMapNode? There seems to be a memory leak with that documented here: https://stackoverflow.com/questions/49616019/spritekit-memory-leaks-changing-scenes-containing-sktilemapnodes – JKaz Oct 08 '18 at 17:31
  • @JKaz Not using that with this particular project. (But I am with a different project so thanks for making me aware!) – peacetype Oct 09 '18 at 00:16
  • fwiw, overnight I switched my tiled node to an array of nodes and calculated the position and movement of each. Not as convenient, but it's doable and does not suffer from the memory leak. – JKaz Oct 09 '18 at 20:16
  • @JKaz Thx for the tip! You’d lose the benefit of the tile map editor but maybe that’s better than suffering memory leaks. – peacetype Oct 09 '18 at 23:36

2 Answers2

4

First off you are presenting SKScene's correctly and you are correct you should be able to trust that the old scene will get cleaned up. By that I mean there is nothing else you need to do to make it release.

Now with that being said there are things that you may have done to create a circular reference. Hopefully a couple of these checks will help you track it down.

The first place I always look is at your player. If your scene has a player property and your player has a scene property this could prevent the scene from deallocating. Player holds on to the scene and scene holds on to player. I doubt this is your case but worth checking.

Second place to look is there anything else that has a reference or property to that scene? A common issue is when you create your own delegate method. If your player does an action and then calls a method back to that scene. The player should only have weak references to that scene. I have done this myself and seen other do it where they create a custom delegate or protocol and keep a strong reference to it instead of weak.

Third place to look is anywhere in your code where you call self. This is common in run blocks of SKActions. You may have an action that calls a method on the scene and you may have a property on that scene of that SKAction. The action has reference to the scene due to the run block and the scene has a reference to the action. So look for self and see if that object is a property on that scene.

Hopefully that helps you track it down. I know it can be frustrating tracking a leak like this down and those are common issues I have seen in the past.

Skyler Lauren
  • 3,792
  • 3
  • 18
  • 30
  • 1
    Yes, it's particularly frustrating to track down leaks in the spaghetti code I'm dealing with :) But this gives me a few places to look at, thanks! – peacetype Mar 13 '18 at 18:30
1

Skyler Lauren's answer put me in the right direction:

Second place to look is there anything else that has a reference or property to that scene? A common issue is when you create your own delegate method. If your player does an action and then calls a method back to that scene. The player should only have weak references to that scene. I have done this myself and seen other do it where they create a custom delegate or protocol and keep a strong reference to it instead of weak.

I have a lot of custom delegation in my SpriteKit game and the way i wrote the protocol was wrong. Below you can see one of my wrong protocol and delegate:

// WRONG VERSION    
import SpriteKit
        
class StartButtonGO: SKSpriteNode {
            
   var delegate: StartButtonGODelegate? // this needs to change

}
        
protocol StartButtonGODelegate { // this needs to change
   func startButtonSelectedAnimationHasStarted()
   func startButtonSelectedAnimationHasEnded()
}

So I've read some articles in the internet and finally changed all of my protocol and delegate declarations to the weak references like the code below:

// RIGHT VERSION    
import SpriteKit
                        
class StartButtonGO: SKSpriteNode {
                            
   weak var delegate: StartButtonGODelegate? // has to be 'weak'
                
}
                        
protocol StartButtonGODelegate: AnyObject { // has to be 'AnyObject'
   func startButtonSelectedAnimationHasStarted()
   func startButtonSelectedAnimationHasEnded()
}

After i've changed every protocol that i've written to the weak, i wrote a hack code inside my main view controller which will change the scenes every 3 second indefinitely. You can see the result below.

healthy-memory

Now as you can see some deallocations are happening. One of the scenes has a lot of animations and sounds and the other one is just kinda empty. So when my loop code loads my empty scene, memory drops and 3 seconds later when crowded scene loads, memory peaks.

Hope this helps.

EFE
  • 3,732
  • 4
  • 22
  • 30