Adding a custom GKComponent to an entity in Xcode SpriteKit scene editor sets GKScene.rootNode to nil

Viewed 685

When I add a CustomComponent (GKComponent) to an entity in Xcode SpriteKit scene editor and try to load that .sks file using a GKScene.init, GKScene.rootNode is not set. Even stranger, this happens only on iOS 13 and not on iOS 12.

I have a small sprite kit github project setup that demonstrates this issue clearly. Just run the app on an iOS 13 emulator to reproduce the issue. https://github.com/hdsenevi/answers-gkscene-rootnode-nil-bug

If I remove CustomComponent from SpriteKit scene editor entity/sprite, then it runs fine. ie: loads SKScene into GKScene.rootNode

  1. Is there any other special modifications that needs to happen when adding GKComponents from Xcode SpriteKit scene editor?
  2. Am I missing something obvious here?
  3. And why would this code work without an issue on iOS 12 and not iOS 13?
  4. Has SpriteKit functionality changed with regards to this in iOS 13?

For reference

import UIKit
import SpriteKit
import GameplayKit

class GameViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Load 'GameScene.sks' as a GKScene. This provides gameplay related content
        // including entities and graphs.
        if let scene = GKScene(fileNamed: "GameScene") {

            // Get the SKScene from the loaded GKScene
            if let sceneNode = scene.rootNode as! GameScene? {

                // Copy gameplay related content over to the scene
                sceneNode.entities = scene.entities
                sceneNode.graphs = scene.graphs

                // Set the scale mode to scale to fit the window
                sceneNode.scaleMode = .aspectFill

                // Present the scene
                if let view = self.view as! SKView? {
                    view.presentScene(sceneNode)

                    view.ignoresSiblingOrder = true

                    view.showsFPS = true
                    view.showsNodeCount = true
                }
            } else {
                print("Error. No GameScene was found on GKScene.rootNode")
            }
        } else {
            print("Error loading GKScene file GameScene")
        }
    }
}
import SpriteKit
import GameplayKit

class CustomComponent: GKComponent {
    override init() {
        super.init()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func didAddToEntity() {
        guard let gkSkNodeComponent = self.entity?.component(ofType: GKSKNodeComponent.self) else {
            print("Error. Cannot obtain a reference to GKSKNodeComponent")
            return
        }

        if let sprite = gkSkNodeComponent.node as? SKSpriteNode {
            sprite.texture?.filteringMode = .nearest
        }
    }
}

Update

Few details on my setup

  • macOS Mojave 10.14.6
  • Xcode 11.0 (tried on 10.1 and 10.3, same behaviour)
5 Answers

To solve the problem, your component needs to override variable supportsSecureCoding and it must return true.

This worked for me:

override class var supportsSecureCoding: Bool {
    return true
}

I have also just come across this and have sent a Feedback with a link to this article as it's a great description of the problem.

It was happening to me too. I have a Color Sprite with body type Alpha Mask, I found out that if I change the body type to any other type it fixes this and the rootNode works again.

So I've got a rather hacky workaround if you want to keep the same style of defining components in the SK Scene Editor by using UserData.

In the SK Scene Editor populate nodes UserData with attributes that your code then uses to turn into components, and then automate setup GKSKNodeComponents.

I've made a class TemplateScene which I put as the scene of the sks file.

import Foundation
import SpriteKit
import GameplayKit

class TemplateScene: SKScene {
    
    lazy var entities: [GKEntity] = {
        var entities: [GKEntity] = []
        // applyAllChildren is an extension I made that simple recurses all children, children's children etc
        self.applyAllChildren { node in
            if let userData = node.userData {
                var components: [GKComponent] = []
                if userData["c.BasicComponent"] as? Bool == true {
                    components.append(BasicComponent())
                }
                if components.count > 0 {
                    components.append(GKSKNodeComponent(node: node))
                    let entity = GKEntity()
                    for component in components {
                        entity.addComponent(component)
                    }
                    entities.append(entity)
                }
            }
        }
        return entities
    }()
}

Just keep extending the code to handle more components. If you have component attributes then have UserData in the form of c.BasicComponent.x etc

(To be clear, this is a hack to get around the fact that Apple has a serious bug that makes a large chunk of SpriteKit Editor functionality unsuable in an entire version of iOS. Maybe it's fixed in iOS 14?)

Prior to iOS 13, I accessed the root node of GKScene. For reasons still unclear to me, that results in nil with iOS 13+. Lo and behold, though, you can access the legacy "root node" directly from the GameScene. I did not have to alter any custom classes in any way in order to restore full functionality.

//From GameViewController (UIViewController):

- (void)viewDidLoad {

 [super viewDidLoad];
 GameScene *sceneNode;

 NSOperatingSystemVersion ios13_0_0 = (NSOperatingSystemVersion){13, 0, 0};
 if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:ios13_0_0]) {
    // iOS 13.0.0 and above logic for rootNode
    // Load the SKScene from 'GameScene.sks'
    GameScene *scene = (GameScene *)[SKScene nodeWithFileNamed:@"GameScene"];

    SKView *skView = (SKView *)self.view;

     // The fix for use in iOS 13.0+
    sceneNode = scene;

    // Set the scale mode to scale to fit the window
    sceneNode.scaleMode = SKSceneScaleModeAspectFill;

    // Present the scene
    [skView presentScene:scene];
} else {
   // prior to iOS 13.0.0 logic
    GKScene *scene = [GKScene sceneWithFileNamed:@"GameScene"];

    SKView *skView = (SKView *)self.view;

     // This will result in nil if used in iOS 13.0+
     sceneNode = (GameScene *)(scene.rootNode);

    // Set the scale mode to scale to fit the window
    sceneNode.scaleMode = SKSceneScaleModeAspectFill;

    // Present the scene
    [skView presentScene:sceneNode];
 } 
}

This instantiates an instance of GameScene (SKScene). In GameScene, I am able to access custom nodes that I've added into the scene with a custom name via:

for (SKNode *node in self.children)
{
       if ([node.name containsString:@"NameOfNodeHere"])
    {
         // Access your custom node in your .sks here
    }
}
Related