Search code examples
iosswift3dscenekitarkit

Scenekit: Why SCNNode should not be subclassed?


I keep reading in StackOverflow that we shouldnt subclass SCNNode, can someone please point me in the right direction for Scenekit best practices ?

I feel that subclassing a SCNNode will help me having special methods different types of subclasses. Following Object Oriented Programming.. maybe a 3d Car which is a subclass of SCNNode could have methods for starting the engine, moving, opening the doors, etc..

If that is not the right way.. how can be couple SCNNodes to extra properties and methods? And also, how can be distinguish SCNNodes that are cars, against SCNNodes that are trucks or AirPlanes or anything else ?


Solution

  • I personally don't see anything wrong with subclassing SCNNode, depending of course on why you need to do so.

    A key consideration here is the following:

    If you are adding general purpose functionalities that should be available to every SCNNode, then make an extension.

    All SCNNode instances can then call these new methods.

    On the other hand:

    If you are adding functionality that should be restricted to special instances of SCNNode, and you need to identify these specifically: then make a subclass, since only instances of these can use your new methods.

    If you chose to use an extension of SCNNode, this would mean that any functions that your create could be applied to any SCNNode.

    Let's say for example therefore, that you wanted allow any SCNNode to grow and shrink then an extension would be your best bet e.g:

    extension SCNNode{
    
        /// Doubles The Size Of The SCNNode & Then Returns It To Its Original Size
        func growAndShrink(){
    
            //1. Create An SCNAction Which Will Double The Size Of Our Node
            let growAction = SCNAction.scale(by: 2, duration: 5)
    
            //2. Create Another SCNAction Wjich Will Revert Our Node Back To It's Original Size
            let shrinkAction = SCNAction.scale(by: 0.5, duration: 5)
    
            //3. Create An Animation Sequence Which Will Store Our Actions
            let animationSequence = SCNAction.sequence([growAction, shrinkAction])
    
            //4. Run The Sequence
            self.runAction(animationSequence)
    
        }
    
    }
    

    However, if you wanted for example to create an SCNNode which had functions which would only be available to that instance, then creating a subclass may be the way forward.

    Let's say then that we needed to create an SCNNode with an SCNPlaneGeometry that provided us specific information about that Node, then we might create a subclass like so:

    class PlaneNode: SCNNode {
    
        let DEFAULT_IMAGE: String = "defaultGrid"
        let NAME: String = "PlaneNode"
        var planeGeometry: SCNPlane
        var planeAnchor: ARPlaneAnchor
    
        var widthInfo: String!
        var heightInfo: String!
        var alignmentInfo: String!
    
        //---------------
        //MARK: LifeCycle
        //---------------
    
        /// Inititialization
        ///
        /// - Parameters:
        ///   - anchor: ARPlaneAnchor
        ///   - node: SCNNode
        ///   - node: Bool
        init(anchor: ARPlaneAnchor, node: SCNNode, image: Bool, identifier: Int, opacity: CGFloat = 0.25){
    
            //1. Create The SCNPlaneGeometry
            self.planeAnchor = anchor
            self.planeGeometry = SCNPlane(width: CGFloat(anchor.extent.x), height: CGFloat(anchor.extent.z))
            let planeNode = SCNNode(geometry: planeGeometry)
    
            super.init()
    
            //2. If The Image Bool Is True We Use The Default Image From The Assets Bundle
            let planeMaterial = SCNMaterial()
    
            if image{
    
                planeMaterial.diffuse.contents = UIImage(named: DEFAULT_IMAGE)
    
            }else{
    
                planeMaterial.diffuse.contents = UIColor.cyan
            }
    
            //3. Set The Geometries Contents
            self.planeGeometry.materials = [planeMaterial]
    
            //4. Set The Position Of The PlaneNode
            planeNode.simdPosition = float3(self.planeAnchor.center.x, 0, self.planeAnchor.center.z)
    
            //5. Rotate It On It's XAxis
            planeNode.eulerAngles.x = -.pi / 2
    
            //6. Set The Opacity Of The Node
            planeNode.opacity = opacity
    
            //7. Add The PlaneNode
            node.addChildNode(planeNode)
    
            //8. Set The Nodes ID
            node.name = "\(NAME) \(identifier)"
    
        }
    
        required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
    
    
        /// Updates The Size Of The Plane As & When The ARPlaneAnchor Has Been Updated
        ///
        /// - Parameter anchor: ARPlaneAnchor
        func update(_ anchor: ARPlaneAnchor) {
    
            self.planeAnchor = anchor
    
            self.planeGeometry.width = CGFloat(anchor.extent.x)
            self.planeGeometry.height = CGFloat(anchor.extent.z)
    
            self.position = SCNVector3Make(anchor.center.x, 0.01, anchor.center.z)
    
            returnPlaneInfo()
        }
    
        //-----------------------
        //MARK: Plane Information
        //-----------------------
    
        /// Returns The Size Of The ARPlaneAnchor & Its Alignment
        func returnPlaneInfo(){
    
            let widthOfPlane = self.planeAnchor.extent.x
            let heightOfPlane = self.planeAnchor.extent.z
    
            var planeAlignment: String!
    
            switch planeAnchor.alignment {
    
            case .horizontal:
                planeAlignment = "Horizontal"
            case .vertical:
                planeAlignment = "Vertical"
            }
    
            #if DEBUG
            print("""
                Width Of Plane =  \(String(format: "%.2fm", widthOfPlane))
                Height Of Plane =  \(String(format: "%.2fm", heightOfPlane))
                Plane Alignment = \(planeAlignment)
                """)
            #endif
    
            self.widthInfo = String(format: "%.2fm", widthOfPlane)
            self.heightInfo = String(format: "%.2fm", heightOfPlane)
            self.alignmentInfo = planeAlignment
        }
    
    }
    

    It seems, in your case, that since you plan to have very specific instances e.g. trucks, airplanes etc, each with their own specific functions, then using an SCNNode subclass could be the way forward.

    Hope it helps...

    Update: As per you request e.g. How would this work in the case of using an .scn file?

    Some pseudo code might look like so:

    /// Creates & Manages The Car Model
    class Car: SCNNode {
    
        let MODEL_SCALE = SCNVector3(0.5, 0.5, 0.5)
        let MODEL_POSITION = SCNVector3(1, 0, -2.5)
        let MODEL_ROTATION: CGFloat = 30.45
        let TURN_DURATION: Double = 1
    
        var leftFrontWheel: SCNNode!
        var rightFrontWheel: SCNNode!
        var leftBackWheel: SCNNode!
        var rightBackWheel: SCNNode!
    
        //--------------------
        //MARK: Initialization
        //--------------------
    
        override init() {
    
            super.init()
    
            //1. Get The Car Model From The Assetts Bundle
            guard let carModel = SCNScene(named: "StackOverflow.scnassets/Models/Car.scn"),
                let modelNode = carModel.rootNode.childNode(withName: "Root", recursively: false),
                let frontLeftWheel = modelNode.childNode(withName: "leftFront", recursively: false),
                let frontRightWheel = modelNode.childNode(withName: "rightFront", recursively: false),
                let rearLeftWheel = modelNode.childNode(withName: "leftRear", recursively: false),
                let rearRightWheel = modelNode.childNode(withName: "rightRear", recursively: false) else { return }
    
    
            //2. Scale, Rotate & Position The Car
            self.scale = MODEL_SCALE
            self.simdRotation = simd_float4 (0, 1, 0, Float(MODEL_ROTATION.degreesToRadians))
            self.position = MODEL_POSITION
    
            //2. Create A Reference To Each Wheel
            self.leftFrontWheel = frontLeftWheel
            self.rightFrontWheel = frontRightWheel
            self.leftBackWheel = rearLeftWheel
            self.rightBackWheel = rearRightWheel
    
    
            //3. Add The Car To The Root Node
            self.addChildNode(modelNode)
    
            print("""
                Loaded Car Model
                Scale = \(MODEL_SCALE)
                Rotation = \(MODEL_ROTATION)
                Position = \(MODEL_POSITION)
                """)
    
        }
    
        required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
    
        //---------------
        //MARK: Animation
        //---------------
    
        /// Runs The Wheel Animation
        func animateWheels(){
    
            let wheelTurnAnimationOut = SCNAction.rotate(toAxisAngle:  SCNVector4(0 , 0 , 1, CGFloat(45).degreesToRadians), duration: TURN_DURATION)
            let wheelTurnAnimationIn = SCNAction.rotate(toAxisAngle:  SCNVector4(0 , 0 , 1, CGFloat(0).degreesToRadians), duration: TURN_DURATION)
            let turningSequence = SCNAction.sequence([wheelTurnAnimationOut, wheelTurnAnimationIn])
            let turningAction = SCNAction.repeatForever(turningSequence)
            leftFrontWheel.runAction(turningAction)
            rightFrontWheel.runAction(turningAction)
            leftBackWheel.runAction(turningAction)
            rightBackWheel.runAction(turningAction)
    
        }
    
    }
    

    Which you could then initialise and manage the functions like so:

     let car = Car()
     self.augmentedRealityView.scene.rootNode.addChildNode(car)
     car.animateWheels()
    

    Hope it helps...