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

5

9

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)
Colossians answered 5/10, 2019 at 3:21 Comment(4)
Did you properly clean the project when using to build for iOS 13?Ane
Yes. I did all the things I can possibly think about. 1) deleted build folder 2) deleted derived data folder 3) reseted both iOS 12 and 13 simulators 4) tried different device simulators (6, 8, X, 11). You can try it for your self by just checking out the repo that I've mentioned in the question and running that sample project on an iOS 12 and iOS 13 simulator. Even the iOS 13 one works if you remove the CustomComponent attached to the red sprite node in GameScene.sks file. But obviously I would like to attach components from the SpriteKit scene editor.Colossians
I have exactly the same issue with you after installation of Xcode11. We need to file the bug report to Apple! More reports may expedite the process of debugging in the team in Apple.Isolt
Bummer, this essentially makes the Scene Editor portion of the Components system useless. That's too bad, though, such. great featureRinge
S
5

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
}
Sergei answered 2/10, 2020 at 23:58 Comment(1)
How on earth did you discover this fix to a most annoying bug (that persists thru iOS 16 and Xcode 14)? Thank you for this!Endocardial
S
3

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.

Samella answered 27/10, 2019 at 11:3 Comment(3)
Any news on the bug report? Do you have the link, so maybe we can check directly or is it a private one? I have the same issue too on Xcode 11.2.1 GM.Mitrailleuse
I have not had a response (FB7411636). I found another issue in that when copying a node, the components are not copied so you have to re-add them all. My solution was to write some code that goes through the nodes on loading and add components programmatically based on either the name or setting a value in userData such as playerInputComponent: true.Samella
@AndrewEades, can you please post the link to the article which describes the issue and/or the workaround code you used?Estancia
E
0

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
    }
}
Estancia answered 5/1, 2020 at 9:19 Comment(2)
That gives you a reference to the SKScene, but is there a way to get access to the GKScene object from the iOS 13 code branch, though? Say, to get a list of the entities in the scene? Seems like this code is only usable if the .sks file doesn't use components.Worley
@Worley - Yes. I've updated my answer above - hopefully addressed your question.Estancia
P
0

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.

Peculium answered 17/1, 2020 at 21:56 Comment(0)
E
0

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?)

Escrow answered 26/7, 2020 at 11:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.