Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trouble subclassing SCNScene

Tags:

swift

scenekit

I've been trying to subclass SCNScene as that seems like the best place to keep my scene related logic. Now I'm not sure wether that's reccomended so my first question is - Should I be subclassing SCNScene, and if not why not? The documentation seems to suggest it's common but I've read comments online that suggest I shouldn't subclass it. Scrap that, I was looking at the documentation for SKScene. The SCNScene class reference makes no mention of subclassing.

Assuming it's ok to structure my game that way, here's my progress

//  GameScene.swift

import Foundation
import SceneKit


class GameScene: SCNScene {

    lazy var enityManager: BREntityManager = {
        return BREntityManager(scene: self)
    }()

    override init() {
        print("GameScene init")
        super.init()
    }

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


    func someMethod() {
        // Here's where I plan to setup the GameplayKit stuff
        // for the scene
        print("someMethod called")
    }

}

Note: I'm using lazy var as per the answer to this question

In my view controller, i'm trying to use GameScene like this

class GameViewController: UIViewController {

    override func viewDidLoad() {

        super.viewDidLoad()

        // create a new scene
        let scene = GameScene(named: "art.scnassets/game.scn")!

        // retrieve the SCNView
        let scnView = self.view as! SCNView

        // set the scene to the view
        scnView.scene = scene

        // Should print "someMethod called"
        scene.someMethod()
    }
}

However, the call to GameScene.someMethod() triggers an EXEC_BAD_ACCESS error.

Also, if I omit the call to GameScene.someMethod, the scene loads correctly, but the overridden initializer in GameScene doesn't appear to be called.

I'm not sure what's going on here. It's clear there's something about subclassing in Swift that I've not understood. Or perhaps there' some aspect of order in which things are meant to run that I've missed.

like image 923
gargantuan Avatar asked Dec 19 '22 21:12

gargantuan


2 Answers

Should I be subclassing SCNScene, and if not why not?

No, you don't need to subclass, and you're probably better off not subclassing. SCNScene is more like basic data/model classes (NSString, UIImage, etc) than like behavior/controller/view classes (UIViewController, UIControl, etc). That is, it's a general description of or container for something (in this case, a 3D scene) and doesn't really provide behavior. Since there's not much in the way of behavior, there's not much opportunity to override behavior with a subclass. (Nor is the class designed around subclass entry points meant to be overridden, like viewDidLoad and whatnot.)

While it's possible to subclass SCNScene, you don't gain much from doing so, and some APIs might not work as you expect...

However, the call to GameScene.someMethod() triggers an EXEC_BAD_ACCESS error.

Also, if I omit the call to GameScene.someMethod, the scene loads correctly, but the overridden initializer in GameScene doesn't appear to be called.

A .scn file is actually an NSKeyedArchiver archive, which means that the objects inside it have the same classes that were used to create it. when you call init(named:) on your SCNScene subclass, the superclass' implementation eventually calls through to NSKeyedUnarchiver unarchiveObjectWithData: to load the archive. Absent some unarchiver mangling, that method will instantiate a SCNScene, not an instance of your subclass.

Because you don't get an instance of your subclass, your subclass initializers aren't called, and attempting to call your subclass' methods results in a runtime error.

Aside: Why is SpriteKit different from SceneKit here? SKScene is a bit of an odd duck in that it's neither a pure model class nor a pure controller class. This is why you see a lot of projects, including the Xcode template, using an SKScene subclass. There are drawbacks to this approach, however — if you don't plan carefully, it gets too easy to wed your game logic tightly to your game data, which makes expanding your game (say, to multiple levels) require tedious coding instead of data editing. There's a WWDC 2014 session on this topic that's well worth watching.

So, what to do?

You have a few choices here...

  1. Don't subclass. Keep your game logic in a separate class. This doesn't necessarily have to be a view controller class — you could have one or more Game objects that are owned by your view controller or app delegate. (This makes especially good sense if your game logic transitions between or manipulates the content of multiple scenes.)

  2. Subclass, but arrange to put the stuff from an unarchived SCNScene into an instance of your subclass — instantiate your subclass, load an SCNScene, then move all children of its root node into your subclass' root node.

  3. Subclass, but force NSKeyedUnarchiver to load your subclass instead of SCNScene. (You can do this with class name mappings.)

Of these, #1 is probably the best.

like image 92
rickster Avatar answered Jan 19 '23 03:01

rickster


tl;dr: Extend SCNNode to generate your scene programmatically.

This started as a comment on Rickster's excellent answer but I could see it getting too long. I taught a course this quarter that spent a substantial amount of time on SceneKit. We subclassed SCNScene all the time and didn't run into any trouble.

However, after spending an hour just now reviewing the code I presented, the code the students wrote, and a ton of other sample code (from Apple and from other developers), I would not subclass SCNScene again.

None of the Apple sample code subclasses SCNScene. That should be a very strong clue. Handling the serialization (initWithCoder and the read from file) is a recurring question on StackOverflow. That's a second strong clue that you don't want to go there.

In all of the code I reviewed that subclassed SCNScene, the reason to do it was to generate the scene programmatically. That's Rickster's choice #2. But none of that code really needs to be on SCNScene. If you're building a bunch of nodes in particular configurations, adding lighting and look-at constraints, and setting up materials, it feels like you're building an SCNScene. But really you're building a tree of SCNNode instances.

So I think in hindsight, choice #2 does not provide a good enough reason to subclass. All of that construction can be done by writing a generator function, in an extension to SCNNode.

It's tempting to subclass SCNNode for this custom tree. Don't do it. You'll be able to save it and read it just fine (assuming you write the appropriate NSSecureCoding functions). But you will be unable to open your scene with the Xcode Scene Editor, because the Scene Editor doesn't know how to unarchive and instantiate your custom SCNNode subclasses.

like image 45
Hal Mueller Avatar answered Jan 19 '23 05:01

Hal Mueller