I am trying to create a simple 2d platform game using Swift, SpriteKit, and SKTileMaps. But every time i change between scenes containing SKTileMaps I see a lot of memory leaks in the Xcode Instruments.
I have recreated the problem as simple as I can. I am using a .sks file to create the scene and this file only contains 1 tileMap filled with some tiles.
The code in the view controller for presenting the scene:
if let view = self.view as! SKView? {
let scene = LoadingScene(size: CGSize(width: 2048, height: 1536))
scene.scaleMode = .aspectFill
view.presentScene(scene)
The code for the scene:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let scene = GameScene(fileNamed: "WorldScene") else{fatalError("Could not open world scene")}
view?.presentScene(scene)
}
}
I am choosing GameScene as the custom class int the .sks scene file.
This will result in a lot of small memory leaks each time I change the scene:
picture of memory leak in Instruments
This is the leak from only one scene change. Am I doing something wrong or is this a SpriteKit bug?
Edit1
The SKCTileMapNode::_ensureChunkForTileIndex(unsigned int) leaks happen each time i load the tile map, while the rest only appear when changing a scene
Edit2
Changed the GameViewController to skip the LoadingScene and go straight to GameScene. The memory leak is still there:
if let view = self.view as! SKView? {
guard let scene = GameScene(fileNamed: "WorldScene") else{fatalError("Could not open world scene")}
scene.scaleMode = .aspectFill
view.presentScene(scene)
}
I have come across this same issue and after looking into it, I believe it is a problem that is inherent to all SKTileMapNodes. It is a bug in SpriteKit.
When you use an SKTileMapNode with ANY tiles filled (not a blank tile map), then the memory for the tile map will persist even when loading subsequent scenes. If you keep loading levels with SKTileMapNodes, then the memory will keep increasing until the game ultimately crashes. I have tested this with different games I have coded and using the code of others as well.
Interestingly, if the tile map has all blank tiles (even if it has an SKTileSet assigned), then no memory leak will occur.
From the best that I can guess, when an SKTileMapNode has any tile in it besides blank tiles, the entire SKTileSet that it is using, is being kept in memory and never gets removed.
From my experiments, you can prevent this problem by swapping the SKTileSet with a blank tileset in didMove. You cannot do it anywhere else except within didMove (or a function called by didMove).
So my solution has been to extract the tile set into individual sprites and then "nullify" the tileset on the tilemap. You can do so with the following code:
extension SKTileMapNode {
func extractSprites(to curScene: SKScene) {
for col in 0..<numberOfColumns {
for row in 0..<numberOfRows {
if tileDefinition(atColumn: col, row: row) != nil {
let tileDef = tileDefinition(atColumn: col, row: row)
let newSprite = SKSpriteNode(texture: tileDef!.textures.first!)
curScene.addChild(newSprite)
let tempPos = centerOfTile(atColumn: col, row: row)
newSprite.position = convert(tempPos, to: curScene)
}
}
}
eraseTileSet()
}
func eraseTileSet() {
let blankGroup: [SKTileGroup] = []
let blankTileSet = SKTileSet(tileGroups: blankGroup)
tileSet = blankTileSet
}
}
Basically within didMove, you will need to call extractSprites on each SKTileMapNode. This will simply create SKSpriteNodes from each tile and put them into the scene. Then the SKTileSet will be "switched" to a blank one. Magically the memory leak will disappear.
This is a simplified solution which you will need to expand upon. This only puts the sprites there, but doesn't define how they behave. Sorry but this is the only solution I have found and I believe your problem is a major bug in SpriteKit.
Related
I have an ARKit app that uses SpriteKit to display SKNodes that correspond to ARAnchors in the AR session. I am using the ARSKViewDelegate method
func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode?
to display the nodes. This works for nodes I create and return in that method, but I have some more complicated nodes that I would like to design in an .sks file and then display for some anchors. When I return a node instantiated from an .sks file in that method the app crashes with an error saying the node is already in the scene (if the node is in the .sks file for the current scene), or already has a parent (if it is in an .sks file for a different scene).
How can I display sprites in ARKit that are drawn in an .sks file?
Try this approach:
if let view = self.view as! SKView? {
if let scene = SKScene(fileNamed: "mySpriteKitScene") {
guard let clone = scene.childNode(withName: "myRedSprite")?
.copy() as? SKSpriteNode
else { return }
clone.name = "mySecondRedSprite"
clone.position.x += 250
clone.physicsBody = SKPhysicsBody()
scene.addChild(clone)
view.presentScene(scene)
}
}
Recently, I decided to apply my previous knowledge in C++ and Python to learning Swift. After which, I decided to see what I could do with the SceneKit framework. After hours of checking through the documentation, and consulting a tutorial, I have to wonder what's going wrong with my code:
class GameViewController: UIViewController {
var gameView:SCNView!
var gameScene:SCNScene!
var cameraNode:SCNNode!
override func viewDidLoad() {
super.viewDidLoad()
initScene()
initView()
initCamera()
}
func initView() {
//initialize the game view - this view holds everything else in the game!
gameView = self.view as! SCNView
//allow the camera to move to gestures - mainly for testing purposes
gameView.allowsCameraControl = true
//use default lighting while still practicing
gameView.autoenablesDefaultLighting = true
}
func initScene() {
//initialize the scene
gameScene = SCNScene()
//set the scen in the gameView object to the scene created by this function
gameView.scene = gameScene
}
func initCamera() {
//create a node that will become the camera
cameraNode = SCNNode()
//since a node can be any object in the scene, this needs to be set up as a camera
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3 (x:0, y:5, z:15)
}
}
After more checking through the documentation and making sure that I was now copying from the tutorial directly to get it to work, I still have no luck with this. According to a lot of the other questions I found here on StackOverflow, it looks like it has something to do with the forced unwrapping, the exclamation points, but I'm not exactly sure why that is.
I've probably been staring the answer in the face combing through this documentation, but I'm not quite seeing what the problem is.
Also, apologies if my comments are a bit long and/or distracting.
You have the following problems:
1) you should re-order the initializations in your viewDidLoad, doing so:
initView() // must be initialized before the scene
initScene() // you have been crashing here on getting `gameView.scene`, but gameView was nil
initCamera()
2) cameraNode is not attached on the rootNode, so you may add the following code at the end of initCamera:
gameScene.rootNode.addChildNode(cameraNode)
I have ready many (if not all) articles on SO and other sites about the disasters of dealing with SpriteKit and memory issues. My problem, as many others have had, is after i leave my SpriteKit scene barely any of the memory added during the scene session is released. I've tried to implement all suggested solutions in the articles i've found, including, but not limited to...
1) Confirm the deinit method is called in the SKScene class.
2) Confirm no strong references to the parent VC in the scene class.
3) Forcefully remove all children and actions, and set the scene to nil when the VC disappears. (Setting the scene to nil was what got the deinit method to eventually get called)
However, after all of that, memory still exists. Some background, this app goes between standard UIKit view controllers and a SpriteKit scene (it's a professional drawing app). As an example, the app is using around 400 MB before entering a SpriteKit scene. After entering the scene and creating multiple nodes, the memory grows to over 1 GB (all fine so far). When i leave the scene, the memory drops maybe 100 MB. And if i re-enter the scene, it continues to pile on. Are there any ways or suggestions on how to completely free all memory that was used during a SpriteKit session? Below is a few of the methods being used to try and fix this.
SKScene class
func cleanScene() {
if let s = self.view?.scene {
NotificationCenter.default.removeObserver(self)
self.children
.forEach {
$0.removeAllActions()
$0.removeAllChildren()
$0.removeFromParent()
}
s.removeAllActions()
s.removeAllChildren()
s.removeFromParent()
}
}
override func willMove(from view: SKView) {
cleanScene()
self.removeAllActions()
self.removeAllChildren()
}
Presenting VC
var scene: DrawingScene?
override func viewDidLoad(){
let skView = self.view as! SKView
skView.ignoresSiblingOrder = true
scene = DrawingScene(size: skView.frame.size)
scene?.scaleMode = .aspectFill
scene?.backgroundColor = UIColor.white
drawingNameLabel.text = self.currentDrawing?.name!
scene?.currentDrawing = self.currentDrawing!
scene?.drawingViewManager = self
skView.presentScene(scene)
}
override func viewDidDisappear(_ animated: Bool) {
if let view = self.view as? SKView{
self.scene = nil //This is the line that actually got the scene to call denit.
view.presentScene(nil)
}
}
As discussed in the comments, the problem is probably related to a strong reference cycle.
Next steps
Recreate a simple game where the scene is properly deallocated but some of the nodes are not.
I'll reload the scene several time. You'll see the scene is properly deallocated but some nodes into the scene are not. This will cause a bigger memory consumption each time we replace the old scene with a new one.
I'll show you how to find the origin of the problem with Instruments
And finally I'll show you how to fix the problem.
1. Let's create a game with a memory problem
Let's just create a new game with Xcode based on SpriteKit.
We need to create a new file Enemy.swift with the following content
import SpriteKit
class Enemy: SKNode {
private let data = Array(0...1_000_000) // just to make the node more memory consuming
var friend: Enemy?
override init() {
super.init()
print("Enemy init")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
print("Enemy deinit")
}
}
We also need to replace the content of Scene.swift with the following source code
import SpriteKit
class GameScene: SKScene {
override init(size: CGSize) {
super.init(size: size)
print("Scene init")
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
print("Scene init")
}
override func didMove(to view: SKView) {
let enemy0 = Enemy()
let enemy1 = Enemy()
addChild(enemy0)
addChild(enemy1)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let newScene = GameScene(size: self.size)
self.view?.presentScene(newScene)
}
deinit {
print("Scene deinit")
}
}
As you can see the game is designed to replace the current scene with a new one each time the user taps the screen.
Let's start the game and look at the console. Will' see
Scene init
Enemy init
Enemy init
It means we have a total of 3 nodes.
Now let's tap on the screen and let's look again at the console
Scene init
Enemy init
Enemy init
Scene init
Enemy init
Enemy init
Scene deinit
Enemy deinit
Enemy deinit
We can see that a new scene and 2 new enemies have been created (lines 4, 5, 6). Finally the old scene is deallocated (line 7) and the 2 old enemies are deallocated (lines 8 and 9).
So we still have 3 nodes in memory. And this is good, we don't have memory leeks.
If we monitor the memory consumption with Xcode we can verify that there is no increase in the memory requirements each time we restart the scene.
2. Let create a strong reference cycle
We can update the didMove method in Scene.swift like follows
override func didMove(to view: SKView) {
let enemy0 = Enemy()
let enemy1 = Enemy()
// ☠️☠️☠️ this is a scary strong retain cycle ☠️☠️☠️
enemy0.friend = enemy1
enemy1.friend = enemy0
// **************************************************
addChild(enemy0)
addChild(enemy1)
}
As you can see we now have a strong cycle between enemy0 and enemy1.
Let's run the game again.
If now we tap on the screen and the look at the console we'll see
Scene init
Enemy init
Enemy init
Scene init
Enemy init
Enemy init
Scene deinit
As you can see the Scene is deallocated but the Enemy(s) are no longer removed from memory.
Let's look at Xcode Memory Report
Now the memory consumption goes up every time we replace the old scene with a new one.
3. Finding the issue with Instruments
Of course we know exactly where the problem is (we added the strong retain cycles 1 minute ago). But how could we detect a strong retain cycle in a big project?
Let click on the Instrument button in Xcode (while the game is running into the Simulator).
And let's click on Transfer on the next dialog.
Now we need to select the Leak Checks
Good, at this point as soon as a leak is detected, it will appear in the bottom of Instruments.
4. Let's make the leak happen
Back to the simulator and tap again. The scene will be replaced again.
Go back to Instruments, wait a few seconds and...
Here it is our leak.
Let's expand it.
Instruments is telling us exactly that 8 objects of type Enemy have been leaked.
We can also select the view Cycles and Root and Instrument will show us this
That's our strong retain cycle!
Specifically Instrument is showing 4 Strong Retain Cycles (with a total of 8 Enemy(s) leaked because I tapped the screen of the simulator 4 times).
5. Fixing the problem
Now that we know the problem is the Enemy class, we can go back to our project and fix the issue.
We can simply make the friend property weak.
Let's update the Enemy class.
class Enemy: SKNode {
private let data = Array(0...1_000_000)
weak var friend: Enemy?
...
We can check again to verify the problem is gone.
I am wanting to 'reset' and 'restart' the GameScene so it is as if the GameScene was first called. I have looked at different methods for doing this, but each time I get a warning that I'm trying to add a node to a parent which already has a parent. However, in my code I delete all my existing nodes so I'm really confused as to how to reset the GameScene. This is how I do it now (this code is called when I want to restart the GameScene from scratch and it is called within the GameScene class):
let scene = GameScene(size: self.size)
scene.scaleMode = .aspectFill
let animation = SKTransition.fade(withDuration: 1.0)
self.view?.presentScene(scene, transition: animation)
self.removeAllChildren()
self.removeAllActions()
self.scene?.removeFromParent()
1.Edited: I realised that why I was getting this warning: "I'm trying to add a node to a parent which already has a parent" was because I had all the variables for the scene outside of the class and as global variables. However, now when the game restarts, the game is in the bottom left corner. Why is this the case and how do I fix this? - FIXED
2.Edited: Everything works fine now, but now my concern is that deinit{} isn't called even though every node is deleted and the fps doesn't drop over time. Here is what I have in my GameViewController for setting the scene and in my GameScene (every instance relating to the scenes so basically all that is relevant):
import UIKit
import SpriteKit
import GameplayKit
var screenSize = CGSize()
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
// Load the SKScene from 'GameScene.sks'
if let scene = SKScene(fileNamed: "GameScene") {
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFill
screenSize = scene.size
// Present the scene
view.presentScene(scene)
}
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
Then my GameScene is basically:
import SpriteKit
import GameplayKit
class GameScene: SKScene, SKPhysicsContactDelegate {
//Declare and initialise variables and enumerations here
deinit{print("GameScene deinited")}
override func didMove(to view: SKView) {
//Setup scene and nodes
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//Do other things depending on when and where you touch
//When I want to reset the GameScene
let newScene = GameScene(size: self.size)
newScene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
newScene.scaleMode = self.scaleMode
let animation = SKTransition.fade(withDuration: 1.0)
self.view?.presentScene(newScene, transition: animation)
}
Any answers would be greatly appreciated :)
How to reset the scene?
You just have to present a new, same scene again whenever you want. So, you are doing it fine.
Possible leaking problems?
Also, if you don't have leaks in your game, means no strong reference cycles, you don't even need self.removeAllChildren() and self.removeAllActions()... Of course if you explicitly want to stop actions before transition animation starts, the using this method make sense. The point is, when scene deallocates, all objects that depends on it should / will deallocate as well.
Still, if you don't know from the beginning what you are doing and how to prevent from leaks, eg. you are using strong self in block which is a part of an action sequence, which repeats forever, then you certainly have a leak, and self.removeAllActions() might help (in many cases, but it is not an ultimate solution). I would recommend to read about capture lists and ARC in general because it can be useful to know how all that work just because of these situations.
Scene is a root node
Calling removeFromParent() on a scene itself has no effect. Scene is a root node, so it can't be removed in your current context. If you check scene's parent property you will notice that it is nil. Of course it is possible to add a scene to another scene, but in that case, the scene which is added as a child, will act as an ordinary node.
And finally, how to present the new scene ? Easy, like this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let newScene = GameScene(size: self.size)
newScene.scaleMode = self.scaleMode
let animation = SKTransition.fade(withDuration: 1.0)
self.view?.presentScene(newScene, transition: animation)
}
If something doesn't work for you, it is likely that you have leaks (means your scene isn't deallocated). To check this, somewhere in your GameScene override deinit method, like this:
deinit{print("GameScene deinited")}
To explain you this a bit further... What should happen is that you should present a new scene, a transition should occur, an old scene should be deallocated, and you should see a new scene with an initial state.
Also overriding deinit will just tell you if the scene is deallocated properly or not. But it will not tell you why. It is up to you to find what in your code retaining the scene.
There are 2 main ways that I can think of that do this. The main way that I go this is that if the game is over, (due to the character health falling to zero, or they collide with an object that causes the round to be over, or time is up or whatever), when that happens I like to transition to a new scene that is a summary screen of their score, how far they made it etc.
I do this by having a bool variable in the main GamePlay scene like this.
var gameOver: Bool = false
Then in the code that fires off to cause the game to end set that variable = true.
In the update function check to see if gameOver == true and transition to the GameOverScene.
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
// Initialize _lastUpdateTime if it has not already been
if (self.lastUpdateTime == 0) {
self.lastUpdateTime = currentTime
}
// Calculate time since last update
let dt = currentTime - self.lastUpdateTime
// Update entities
for entity in self.entities {
entity.update(deltaTime: dt)
}
self.lastUpdateTime = currentTime
if gameOver == true {
print("Game Over!")
let nextScene = GameOverScene(size: self.scene!.size)
nextScene.scaleMode = self.scaleMode
nextScene.backgroundColor = UIColor.black
self.view?.presentScene(nextScene, transition: SKTransition.fade(with: UIColor.black, duration: 1.5))
}
}
The update function will check at each frame render to see if the game is over and if it is found to be over it will perform any actions that you need it to and then present the next scene.
Then on the GameOverScene I put a button saying "Retry" and when they click that it fires off the GamePlayScene again, running the view DidLoad function and setting up the GamePlayScene from scratch the way that it should.
Here is an example of how I handle that. There are a few different ways to call a scene transtion. You can give this one a try if it isn't working quite right.
if node.name == "retryButton" {
if let scene = GameScene(fileNamed:"GameScene") {
// Configure the view.
let skView = self.view! as SKView
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .aspectFill
scene.size = skView.bounds.size
skView.presentScene(scene, transition: SKTransition.fade(withDuration: 2.0))
}
}
That is my preferred method of handling the game over transitions.
The other method would be to create a function that resets all of the variables that have changed during the course of playing. Then you could use the same code from the Update function above, but instead of transitioning you could create a label on the scene. If the user clicks the label it would fire off of that function to reset all of the variables that have changed, reset the players locations, stops all actions, resets players health etc. Depending on how many things you have changing during the course of gameplay it'll probably be more practical, (as I've found), to transition to a new scene to give a summary and then reload the GamePlayScene through a button. Then everything will load up just the same as it does the first time that the user entered that main GamePlayScene.
I have a main screen which has a button, when this button is pressed it should transition immediately to another scene, but it doesn't. It actually takes a few seconds. Is there a way that I could load all the nodes in that scene beforehand? (Example: in the games load screen)
This is my code:
let pressButton = SKAction.setTexture(SKTexture(imageNamed: "playButtonP.png"))
let buttonPressed = SKAction.waitForDuration(0.15)
let buttonNormal = SKAction.setTexture(SKTexture(imageNamed: "playButton.png"))
let gameTrans = SKAction.runBlock(){
let doors = SKTransition.doorsOpenHorizontalWithDuration(0)
let levelerScene = LevelerScene(fileNamed: "LevelerScene")
self.view?.presentScene(levelerScene, transition: doors)
}
playButton.runAction(SKAction.sequence([pressButton,buttonPressed,buttonNormal,gameTrans]))
You could preload the SKTextures you're using in LevelerScene before presenting the scene. Then, once the loading has finished you would then present the scene. Here's an example from the Apple's Documentation, translated to Swift:
SKTexture.preloadTextures(arrayOfYourTextures) {
if let scene = GameScene(fileNamed: "GameScene") {
let skView = self.view as! SKView
skView.presentScene(scene)
}
}
In your case you have a couple of options:
1.
Keep an array of the textures, which you need to use in LevelerScene, that you preload in GameScene:
class LevelerScene : SKScene {
// You need to keep a strong reference to your textures to keep
// them in memory after they've been loaded.
let textures = [SKTexture(imageNamed: "Tex1"), SKTexture(imageNamed: "Tex1")]
// You could now reference the texture you want using the array.
//...
}
Now in GameScene, when the user presses the button:
if let view = self.view {
let leveler = LevelerScene(fileNamed: "LevelerScene")
SKTexture.preloadTextures(leveler.textures) {
// Done loading!
view.presentScene(leveler)
}
}
There's no way you can get around having to wait a little bit, but taking this approach the main thread won't get blocked and you'll be able to interact with GameScene whilst LevelerScene is loading.
You could also use this approach to make a loading SKScene for LevelerScene. GameScene would take you to the loading scene, which would load the textures and then move you to LevelerScene once it was complete.
It's important to note that because the reference to the textures is in LevelerScene, once LevelerScene is deinit-ed the textures will be removed from memory. Therefore, if you want to go back to LevelerScene you'll need to load the textures again.
2. You could use SKTexture.preloadTextures in GameViewController before any SKScenes have been presented. You'd need to keep a strong reference to these textures (perhaps in a singleton) which you could then reference in LevelerScene (or anywhere else you needed them in the app).
With this approach, because the SKTextures are stored outside of a scene, they won't be removed from memory when you transition to the next scene. This means you won't have to load the textures again if you leave and then go back to a scene. However, if you've got a lot of textures taking up a lot of memory you could run into some memory issues.
For more information see Preloading Textures Into Memory from Working with Sprites.
Hope that helps!