Trouble Subclassing Scnscene

Trouble subclassing SCNScene

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.

Scenekit: Why SCNNode should not be subclassed?

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...

Subclassing SCNScene and SceneKit Editor

I have a workaround for that problem. Not nice but it works. I instantiate a SCNScene(named: "art.scnassets/testScene.scn") then I instantiate a TheSubclassScene() and I clone the rootNode of the scene and add it as a child node to the subclass scene.

let testScene = SCNScene(named:"art.scnassets/testScene.scn")!
let subclassScene = TheSubclassScene()
subclassScene.rootNode.addChildNode(testScene.rootNode.clone())

SceneKit editor set custom class for node

You can't do it in the scene editor. And you probably shouldn't do it at all. You'll have to implement your own serialization code (so that SCNScene loading works correctly). And all of the methods for searching nodes (by name, hit test, within a volume) return SCNNode, so you'll be forever casting your results.

Per Apple developers, SCNScene is not meant to be subclassed, and I think that extends to SCNNode. Write an extension instead.

See:

  • Trouble subclassing SCNScene
  • Subclassing SCNNode
  • Subclassing SCNScene and SceneKit Editor

What methods do I need in custom scene class of Scenekit ios?

Don't subclass SCNScene.

  • Trouble subclassing SCNScene
  • Subclassing SCNScene and SceneKit Editor
  • Subclassing SCNScene in Swift - override init

Instead, your createScene method (perhaps implemented in an Objective-C category on SCNNode) should return an SCNNode which is the root node of your scene.

For even better startup performance, you can run createScene once, in an auxiliary program, and then archive the scene. The archived scene can then be embedded as a resource in your final product, and even tweaked using the SceneKit editor in Xcode..

swift/scenekit problems getting touch events from SCNScene and overlaySKScene

This is "lifted" straight out of Xcode's Game template......

Add a gesture recognizer in your viewDidLoad:

       // add a tap gesture recognizer
let tapGesture = UITapGestureRecognizer(target: self, action:
#selector(handleTap(_:)))
scnView.addGestureRecognizer(tapGesture)

func handleTap(_ gestureRecognize: UIGestureRecognizer) {
// retrieve the SCNView
let scnView = self.view as! SCNView

// check what nodes are tapped
let p = gestureRecognize.location(in: scnView)
let hitResults = scnView.hitTest(p, options: [:])
// check that we clicked on at least one object
if hitResults.count > 0 {
// retrieved the first clicked object
let result: AnyObject = hitResults[0]

// result.node is the node that the user tapped on
// perform any actions you want on it


}
}

Swift SCNNode subclass hittest always returns SCNNode *not* subclass

Just cast the object to your subclass:

// check what nodes are tapped
let p = gestureRecognize.location(in: scnView)
let hitResults = scnView.hitTest(p, options: [:])

for hit in hitResults {
if let hitnode = hit.node as? ExSCNNode {


}

Subclass SCNReferenceNode within SceneKit

You need to provide values for initialization parameters:

let refNode = CustomRNode(tip: tip, x: 0.12, y: 0.25, tag: "ref_01", url: url)

...or use a convenience initializer:

class CustomRNode: SCNReferenceNode {
var tip: String!
var x: Double!
var y: Double!
var tag: String!

convenience init(tip:String!, x:Double!, y:Double!, tag:String, url:URL) {
self.init(url: url)!
self.tip = tip
self.x = x
self.y = y
self.tag = tag
}
}

let gameURL = Bundle.main.url(forResource: "GameModel", withExtension: "usdz")!
let refNode = CustomRNode(url: gameURL)

(refNode?.referenceURL)! // file:///<folder>/<folder>...


Related Topics



Leave a reply



Submit