5

I am writing a game using SpriteKit with Swift and have run into a memory concern.

The layout of my game is such that the GameViewController (UIViewController) presents the first SKScene (levelChooserScene) in the viewDidLoad Screen. This scene does nothing more than display a bunch of buttons. When the user selects a button the scene then transitions to the correct scene using skView.presentScene, and when the level is complete, that scene then transitions back to the levelChooserScene and the game is ready for the user to select the next level.

The problem is that when the transition back to the levelChooserScene occurs the memory allocated for the game play scene is not deallocated, so after selecting only a few levels I start receiving memory errors.

Is my design correct in transitioning from SKScene to SKScene, or should I instead return to the GameViewController each time and then transition to the next SKScene from there?

I have found a few posts on here that say I should call skView.presentScene(nil) between scenes, but I am confused on how or where to implement that.

I simply want to transition from one SKScene to another and have the memory used from the outgoing scene to be returned to the system.

This is an example of how I have implemented the SKScene:

class Level3: SKScene
{
   var explodingRockTimer = NSTimer()
   var blowingUpTheRocks = SKAction()

   override func didMoveToView(view: SKView)
   {
       NSTimer.scheduledTimerWithTimeInterval(5.0, target: self, selector: "dismissTheScene:", userInfo: nil, repeats: false)
       var wait = SKAction.waitForDuration(0.5)
       var run = SKAction.runBlock{
           // your code here ...
           self.explodeSomeRocks()
       }
       let runIt = SKAction.sequence([wait,run])
       self.runAction(SKAction.repeatActionForever(runIt), withKey: "blowingUpRocks")

       var dismissalWait = SKAction.waitForDuration(5.0)
       var dismissalRun = SKAction.runBlock{
           self.removeActionForKey("blowingUpRocks")
           self.dismissTheScene()

       }
       self.runAction(SKAction.sequence([dismissalWait,dismissalRun]))
   }

   func explodeSomeRocks()
   {
       println("Timer fired")
   }

   //MARK: - Dismiss back to the level selector
   func dismissTheScene()
   {
       let skView = self.view as SKView?
       var nextScene = SKScene()

       nextScene = LevelChooserScene()
       nextScene.size = skView!.bounds.size
       nextScene.scaleMode = .AspectFill
       var sceneTransition = SKTransition.fadeWithColor(UIColor.blackColor(), duration: 1.5) //WithDuration(2.0)
       //var sceneTransition = SKTransition.pushWithDirection(SKTransitionDirection.Down, duration: 0.75) //WithDuration(2.0)
       //var sceneTransition = SKTransition.crossFadeWithDuration(1.0)
       //var sceneTransition = SKTransition.doorwayWithDuration(1.0)
       sceneTransition.pausesOutgoingScene = true

       skView!.presentScene(nextScene, transition: sceneTransition)
   }
}
Scooter
  • 4,068
  • 4
  • 32
  • 47
  • What kind of memory errors are you getting? – sangony Apr 19 '15 at 19:36
  • I don't have the exact warning text written down, but it says in the log that it has received a low memory warning. If I continue to select new levels and keep playing it will eventually just terminate. – Scooter Apr 19 '15 at 19:39
  • Here is the log text: Received memory warning. When it terminates I receive a message in Xcode that says: Quit unexpectedly. Message from debugger: Terminated due to Memory Error – Scooter Apr 19 '15 at 19:41
  • 1
    Have you checked to see if dealloc is being called on your scenes? – ABakerSmith Apr 19 '15 at 19:45
  • I'm sure this is unrelated, but you are unnecessarily creating an SKScene on `nextScene = SKScene()` before you overwrite it with your `LevelChooserScene`... – jtbandes Apr 19 '15 at 19:46
  • I am using Swift, so there is no dealloc method to check, unless I am missing something. Instruments is not showing memory leaks, but the memory allocation keeps climbing each time I select the same scene. – Scooter Apr 19 '15 at 19:49
  • You can check to see if an object is being deallocated using Swift. Use the method `dealloc` in your `SKScene`. – ABakerSmith Apr 19 '15 at 19:52
  • Can't use dealloc in Swift. https://developer.apple.com/library/prerelease/mac/documentation/Swift/Conceptual/Swift_Programming_Language/Deinitialization.html Although I don't think this works exactly the same way. The deinit function is being called in the SKScene however. – Scooter Apr 19 '15 at 19:55
  • Sorry, I mean `deinit`. – ABakerSmith Apr 19 '15 at 19:59
  • Odd. The level in question that is causing me trouble is only having its deinit function called sometimes. The transition back to the levelChooserScene always happens correctly, but deinit is not always called. That makes no sense to me. – Scooter Apr 19 '15 at 20:01
  • Ok, now I am onto something I think. The only difference between when the deinit is called and when it is not is the type of ending the user experiences on the level. If they complete it successfully the view simply centers on a specific SpriteNode and the transition occurs. If they don't complete it, I insert several particleEmitters that run for a few seconds then transition back to the levelChooserScene. It is in the latter case that deinit is not called. – Scooter Apr 19 '15 at 20:13

2 Answers2

1

Well the thing that was causing my trouble was inserting particle emitters every half second for 5 seconds using SKAction.repeatActionForever() to call the emitter insert function.

This repeatAction apparently was not killed by transitioning to another scene, and was causing the memory for the whole scene to be retained. I switched to SKAction.repeatAction() instead and specify how many time it should fire. The scene now returns all of its memory when I transition to the new scene.

I am not sure I understand this behavior though.

Scooter
  • 4,068
  • 4
  • 32
  • 47
  • 1
    The problem wasn't that you used SKAction.repeatActionForever(), the problem was that you didn't removed the action. You created an action that will run until you will kill it. Swift didn't knew it was supposed to remove it, so it let the action live. In order to avoid this memory leak, just use removeAction(forKey: String) before preseting the new scene. – Alexandru Vasiliu Jul 14 '18 at 07:43
1

SpriteKit it's not strongly documented when it comes to create complex games. I personally had a problem like this for days until I managed to figure it out.

Some objects retain the reference, so it doesn't deinit. (SKActions, Timers, etc)

Before presenting a new scene I call a prepare_deinit() function where I manually remove the strong references which are usually not deallocated by swift.

func prepare_deinit()
{
    game_timer.invalidate() // for Timer()
    removeAction(forKey: "blowingUpRocks") // for SKAction in your case

    // I usually add the specific actions to an object and then remove 
    object.removeAllActions()

    // If you create your own object/class that doesn't deinit, remove all object 
    //actions and the object itself
    custom_object.removeAllActions()
    custom_object.removeFromParent()

}

deinit
{
   print("GameScene deinited")
}

The last problem I encountered was that the new scene was presented much faster than my prepare_deinit() so I had to present the new scene a little later, giving the prepare_deinit() enough time to deallocate all objects.

let new_scene =
{
   let transition = SKTransition.flipVertical(withDuration: 1.0)
   let next_scene = FinishScene(fileNamed: "FinishScene")
   next_scene?.scaleMode = self.scaleMode
   next_scene?.name = "finish"
   self.view?.presentScene(next_scene!, transition: transition)
}

run(SKAction.sequence([SKAction.run(prepare_deinit), SKAction.wait(forDuration: 0.25), SKAction.run(exit_to_finish)]))
Alexandru Vasiliu
  • 534
  • 1
  • 4
  • 18