Swift: Can't subclass SCNNode? Fatalerror - ios

Ok, I need to subclass SCNNode because I have different SCNNodes with different "abilities" in my game (I know people don't usually subclass SCNNode but I need to)
I have followed every other question like Subclassing SCNNode and Creating a subclass of a SCNNode
but continue to get this error:
fatal error: use of unimplemented initializer 'init()' for class 'LittleDude.Dude'
Where Dude is the name of my SCNNode subclass.
Following the second question, because of classing issues this is how I attempt to get the SCNNode from my .dae scene and assign it to my Dude():
var theDude = Dude(geometry: SCNSphere(radius: 0.1)) //random geometry first
var modelScene = SCNScene(named: "art.scnassets/ryderFinal3.dae")!
if let d = modelScene.rootNode.childNodes.first
{
theDude.transform = d.transform
theDude.geometry = d.geometry
theDude.rotation = d.rotation
theDude.position = d.position
theDude.boundingBox = d.boundingBox
theDude.geometry?.firstMaterial = d.geometry?.firstMaterial
}
print("DUDE: ", theDude)
Then in my Dude class:
class Dude: SCNNode {
init(geometry: SCNGeometry) {
super.init()
center(node: self)
self.scale = SCNVector3(x: modifier, y: modifier, z: modifier)
//theDude.transform = SCNMatrix4Mult(theDude.transform, SCNMatrix4MakeRotation(360, 0, 1, 0))
//theDude.worldOrientation = .
//self.theDude.position = SCNVector3Make(0, 0, -1)
for s in animScenes {
if let anim = animationFromSceneNamed(path: s)
{
animations.append(anim)
anim.usesSceneTimeBase = true
anim.repeatCount = Float.infinity
self.addAnimation(anim, forKey: anim.description)
}
}
}
}
/* Xcode required this */
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented222")
}
The error gets drawn on first line of this custom class and happens when I try to clone and add the custom SCNNode to my scene:
func makeDude(hitPosition: SCNVector3) {
//print("DUDE")
let clone = theDude.clone() as? SCNNode
clone?.position = hitPosition
self.sceneView.scene.rootNode.addChildNode(clone!)
}
Even though I cast to SCNNode to try to avoid an error. How can I just clone and use my custom SCNNode in my scene? What is wrong here?

Just to make clear, this answer is hidden on the comments of a previous answer, so to avoid the confusion here is the answer fully spelled out:
class NodeSubClass: SCNNode {
init(geometry: SCNGeometry?){
super.init()
self.geometry = geometry
}
...
}

If you subclass SCNNode and override its initializer init(geometry: SCNGeometry?) then you'll need to call the same initalizer of super during your init. Try changing
super.init()
to
super.init(geometry: geometry)

Related

How to add a primitive with a class in SceneKit

Making a class in SceneKit is important. However, I cannot get it to work.
Here is my class code
import UIKit
import SceneKit
class Ship: SCNNode {
override init(){
super.init()
let box = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0)
let node = SCNNode(geometry: box)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
And here is my code in the ViewController (I am using ARKit)
let tempShip = Ship()
tempShip.position = SCNVector3(0.1,0.1,0.1)
sceneView.scene.rootNode.addChildNode(tempShip)
I think I am missing something basic.
You probably have not created an SCNScene and added it to your view. At least there's no sign of that in the code you posted. You need to have something like
sceneView.scene = SCNScene()
or create it using one of SCNScene's init methods.
Then you'll have a scene you can hang your node on. Don't forget to add lighting and camera.
Also: don't subclass SCNNode. Use an extension instead. If you subclass SCNNode or SCNScene you can't use the Xcode Scene Editor. See SceneKit editor set custom class for node.
You should be seeing a warning that your node variable is not being used, you need to set the geometry on the node. Change your init method to this:
override init(){
super.init()
let box = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0)
self.geometry = box
}
An SCNView's scene property is an optional. Change the last line to:
guard let scene = sceneView.scene else { return }
scene.rootNode.addChildNode(tempShip)

SKAction not running on SKSpriteNode

The SKAction on my SKShapeNode isn't working, the code isn't getting executed, and the node isn't moving, even though the console is logging "Snake is moving". Is is because the node is a property of the SKScene and the actions are part of lower scope functions?
class LevelScene: SKScene, SnakeShower {
var snake: Snake {
let theSnake = Snake(inWorld: self.size)
return theSnake
}
override func didMove(to view: SKView) {
self.backgroundColor = .green
snake.delegate = self
}
var myNode: SKShapeNode {
let node = SKShapeNode(rectOf: snake.componentSize)
node.position = snake.head
node.fillColor = .red
return node
}
func presentSnake() { // function called by the snake in the delegate (self)
self.addChild(myNode)
startMoving()
}
func startMoving() {
print("snake is moving")
myNode.run(SKAction.repeatForever(SKAction.sequence([
SKAction.move(by: self.snake.direction.getVector(), duration: 0.2),
SKAction.run({
if self.myNode.position.y > (self.size.height / 2 - self.snake.componentSize.height / 2) {
self.myNode.removeAllActions()
}
})
])))
}
}
It used to work when they property was declared in the same function as the action
myNode is a computed property. self.addChild(myNode) adds a node, but there is no myNode stored property.
myNode.run First computes a new node without adding it to the scene. Then it calls the run the action on it. but since it's a different node that is not on the scene, it will never run.
Change your myNode defintion to:
var myNode : SKShapeNode!
and in didMove(to view: add:
myNode = SKShapeNode(rectOf: snake.componentSize)
myNode.position = snake.head
myNode.fillColor = .red
I had an issue almost identical to this, where I had unresponsive child nodes in a referenced node. Turns out that referenced nodes default to isPaused being true (answer thread found here)
The solution was to change the isPaused property to false.
referencedNode.isPaused = false

Custom SKSpriteNode addChild not working

I am trying to develop a game which need to have a common background across all the scenes using SpriteKit and Swift. Since the background is common and its actions need to be continuous, I created a custom singleton subclass of SKSpriteNode like this:
class BackgroundNode: SKSpriteNode {
static let sharedBackground = BackgroundNode()
private init()
{
let texture = SKTexture(imageNamed: "Background")
super.init(texture: texture, color: UIColor.clearColor(), size: texture.size())
addActors()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private func addActors() {
addClouds()
}
private func addClouds() {
addCloud1()
}
private func addCloud1() {
let cloud1 = SKSpriteNode(imageNamed: "Cloud1")
cloud1.size = CGSizeMake(0.41953125*self.size.width, 0.225*self.size.height)
cloud1.position = CGPointMake(-self.size.width/2, 0.7*self.size.height)
self.addChild(cloud1)
let moveAction = SKAction.moveTo(CGPointMake(self.size.width/2, 0.7*self.size.height), duration: 20)
cloud1.runAction(SKAction.repeatActionForever(moveAction))
}
}
And from the GameScene class I am adding this node to the current view like this:
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
BackgroundNode.sharedBackground.size = CGSizeMake(self.size.width, self.size.height)
BackgroundNode.sharedBackground.position = CGPointMake(self.size.width/2, self.size.height/2)
addChild(BackgroundNode.sharedBackground)
}
}
The background image is showing up correctly, but the cloud is not getting added. As of the code above The cloud should appear out of the screen and animate into and then again out of the screen through the other edge, but to verify if it is getting added, I even tried adding the cloud to the center of the screen without any animations. Still the cloud didn't show up. What can be the issue here? And how to fix it?
EDIT
I figured out that the child is actually getting added but is getting added and moving through some points far above the screen. I also figured out that it MIGHT have something to do with anchor point of the cloud, but whatever value I set as anchor point, the cloud always remains on the top right corner of the screen. What can I do about the anchor points to make the clouds appear as it should(considering the lower left corner as (0, 0) is what I want)
Solved the issue. The problem was that I had to manually set the anchor points of the Scene and the node. Setting the anchor point of both the Scene and the node to (0, 0) solved the issue. The new code looks as follows:
GameScene
override func didMoveToView(view: SKView) {
anchorPoint = CGPointMake(0, 0) //Added this
BackgroundNode.sharedBackground.size = CGSizeMake(self.size.width, self.size.height)
BackgroundNode.sharedBackground.position = CGPointMake(0, 0)
addChild(BackgroundNode.sharedBackground)
}
BackgroundNode
private init()
{
let texture = SKTexture(imageNamed: "Background")
super.init(texture: texture, color: UIColor.clearColor(), size: texture.size())
anchorPoint = CGPointMake(0, 0) //Added this
addActors()
}

How to optimize for better frame rate?

This is for a puzzle game. As you can see from the screenshot, there are 256 nodes on screen and frame rate hovers around 10 FPS.
I see plenty of puzzle games out there with what I assume amounts to hundreds of separate nodes on screen, yet with great frame rates... I'd like to achieve the same. What are some optimization points I can consider based on my code? (Drawing code below)
I'm literally just creating a bunch of SKShapeNodes, putting them into an NSArray, and then adding them as children to the scene on didMoveToView
class GameScene: SKScene {
var grid: Grid! = nil
override func didMoveToView(view: SKView) {
self.grid = Grid()
self.grid.initWithParent(parent: self, rows:16, columns:8, hexagonSize:40.0)
self.grid.drawGrid()
}
override func update(currentTime: CFTimeInterval)
{
// nothing here!! why so slow..
}
}
//…
class Grid : NSObject {
// this all gets initialized later
var hexagonSize: CGFloat! = nil
var hexagonArray: NSMutableArray! = nil
var numRows: Int! = nil
var numColumns: Int! = nil
var parentScene: SKScene? = nil
// …
func drawGrid(){
//…
if(self.parentScene != nil){
for rowIdx in 0..<self.numRows {
for colIdx in 0..<self.numColumns {
//…
let hex = Hexagon()
hex.initWithParentNode(node: self.parentScene!, size: self.hexagonSize)
hex.drawAtPoint(positionPoint: hexCenter)
self.hexagonArray.addObject(hex) hex.drawAtPoint(:positionPoint)
}
}
}
}
func initWithParent(#parent: SKScene, rows: Int, columns: Int, hexagonSize: CGFloat){
self.parentScene = parent
self.numRows = rows
self.numColumns = columns
self.hexagonSize = hexagonSize
}
}
//…
class Hexagon: NSObject {
//…
var parentNode : SKNode? = nil
var size : CGFloat! = nil
var shape : SKShapeNode! = nil
func drawAtPoint(#positionPoint: CGPoint){
let diameter = self.size
let radius = diameter/2
let normal = radius * sqrt(3)/2
var path = CGPathCreateMutable()
self.shape = SKShapeNode(path: path)
let point = CGPointZero
CGPathMoveToPoint(path, nil, point.x, point.y+radius)
CGPathAddLineToPoint(path, nil, point.x+normal, point.y+(radius/2))
CGPathAddLineToPoint(path, nil, point.x+normal, point.y-(radius/2))
CGPathAddLineToPoint(path, nil, point.x, point.y-(diameter/2))
CGPathAddLineToPoint(path, nil, point.x-normal, point.y-(radius/2))
CGPathAddLineToPoint(path, nil, point.x-normal, point.y+(radius/2))
CGPathAddLineToPoint(path, nil, point.x, point.y+radius)
self.shape?.path = path
self.shape?.fillColor = SKColor.blueColor()
if(self.shape != nil && self.parentNode != nil){
self.shape.position = positionPoint
self.parentNode!.addChild(self.shape!)
}
}
func initWithParentNode(#node: SKNode, size: CGFloat){
self.parentNode = node
self.size = size
}
}
Well... you are manually drawing on the screen... You should leave to the SpriteKit engine the responsibility to draw your sprites on the screen (it has tons of optimizations to do this better then we can usually do).
My suggestions
First of all throw away your classes Grid and Hexagon.
Done? Good :)
The image
Get a software like iDraw
Draw an hexagon with transparent background
Then export it as hexagon.png (I prepared the image for you)
Add it to the Xcode Asset catalog
The Hexagon class
Create a file Hexagon.swift and write into it the following code
import SpriteKit
class Hexagon : SKSpriteNode {
init() {
let texture = SKTexture(imageNamed: "hexagon")
super.init(texture: texture, color: UIColor.clearColor(), size: texture.size())
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Populating the scene
Now it's just a matter of creating a few Hexagon(s) and placing them on your scene.
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
let hexagon0 = Hexagon()
hexagon0.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame))
self.addChild(hexagon0)
// add more Hexagon(s) at different positions...
}
}
Conclusions
That's it. SpriteKit will take care of drawing the sprites you added to the scene.
Following this approach you can add a huge number of sprites on the screen, SpriteKit will do optimizations we mortal human beings will never know to keep the framerate as high as possible.
(The optimizations will be even better with Xcode 7 and iOS 9 since SpriteKit is now built on top of Metal).
Hope this helps.

Passing variable from UIViewController to SK Scene

I'm making an a game in XCode 7 using Swift 2. I have a variable that I want to pass from the start screen (which is an UIViewController) to the game scene (which is an SKScene). I want the player to select a character in a UIView and play with it in the SKScene. I also want the score that's in the SKScene to show in the game-over screen that's an UIView. I've seen tutorials for passing data between two UIViewControllers and between two SKScenes, but none of them work for this case.
How can I pass a variable from an UIViewController to a SKScene (and vice versa)?
I ran over this same problem yesterday.
Swift 5.2, xcode 12 and targeting iOS 14. Searched high and low. Eventually found the userData attribute of the SKScene.
Note: The app is based on SwiftUI and the SKScene is inside a UIViewRepresentable:
struct SpriteKitContainer: UIViewRepresentable {
typealias UIViewType = SKView
var skScene: SKScene!
#Binding var timerData : ModelProgressTimer
Not that I am passing a binding into the View - this contains a number of data items I wanted to make available to the scene.
I placed code in two places to populate skScene.userData:
func makeUIView(context: Context) -> SKView {
self.skScene.scaleMode = .resizeFill
self.skScene.backgroundColor = .green
debugPrint("setting user data")
self.skScene.userData = [:]
self.skScene.userData!["color"] = UIColor(timerData.flatColorTwo!)
self.skScene.userData!["running"] = timerData.isRunning
self.skScene.userData!["percent"] = timerData.percentComplete
let view = SKView(frame: .zero)
view.preferredFramesPerSecond = 60
// view.showsFPS = true
// view.showsNodeCount = true
view.backgroundColor = .clear
view.allowsTransparency = true
return view
}
func updateUIView(_ view: SKView, context: Context) {
self.skScene.userData = [:]
self.skScene.userData!["running"] = timerData.isRunning
self.skScene.userData!["color"] = UIColor(timerData.flatColorTwo!)
self.skScene.userData!["percent"] = timerData.percentComplete
view.presentScene(context.coordinator.scene)
}
Then, inside the skScene object, I am able to retrieve the userData values:
var item: SKShapeNode! = nil
override func sceneDidLoad() {
let scaleUp = SKAction.scale(by: 2.0, duration: 1.0)
let scaleDown = SKAction.scale(by: 0.5, duration: 1.0)
scaleUp.timingMode = .easeInEaseOut
scaleDown.timingMode = .easeInEaseOut
let sequence = SKAction.sequence([scaleUp,scaleDown])
actionRepeat = SKAction.repeatForever(sequence)
item = SKShapeNode(circleOfRadius: radius)
addChild(item)
}
override func didChangeSize(_ oldSize: CGSize) {
item.fillColor = self.userData!["color"] as! UIColor
item.strokeColor = .clear
let meRunning = self.userData!["running"] as! Bool
if meRunning {
item.run(actionRepeat)
} else {
item.removeAllActions()
}
}
This draws a circle that "pulses" if the "running" boolean is set in the userdata (a Bool value).
If you haven't already found the answer, I did find a way to do this. I was doing something very similar.
In my case I have a UIView that gets called as a pause menu from my scene. I have made the class and variable names a little more ambiguous so they can apply to your situation as well.
class MyView: UIView {
var scene: MyScene!
var button: UIButton!
init(frame: CGRect, scene: MyScene){
super.init(frame: frame)
self.scene = scene
//initialize the button
button.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "buttonTapped"))
}
func buttonTapped(){
self.removeFromSuperview() // this removes my menu from the scene
scene.doTheThing() // this calls the method on my scene
}
Here are the relevant parts of my scene.
class MyScene: SKScene {
func doTheThing(){
//this is a function on the scene. You can pass any variable you want through the function.
}
}
In your situation, it sounds like the first screen is a UIView and the second screen is the SKScene.
You may want to make your SKScene first, pause it, and then add the UIView in front of the Scene. Once the character is selected, you can remove the UIView and add your character into the scene.
I hope that this answers your question. If it doesn't let me know.
From the level select scene:
let scene = GameScene(fileNamed:"GameScene")
scene?.userData = [:]
scene?.userData!["level"] = 21
self.view?.presentScene(scene!)
From within the game scene:
let level = self.userData!["level"] as! Int

Resources