Unarchived Node doesn't appear when added to parent in SpriteKit - ios

Previously I was questioning if i could use the Scene editor to create complex SKSpriteNodes vs just using it to layout SKScenes. With the help of #Anton Rogachevskyi the previous question I started using unarchiveNodeFromFile to locate the .SKS file. It loads the sks file as I would expect, but when I try to add it to the current scene nothing appears. If I break on the addChild line and preview the object it displays in the preview window properly, so I know it is finding the correct file. Inside the sks file is a custom dialog class and setting breakpoints in "required init?(coder aDecoder: NSCoder)" I can tell that the custom object is getting initialized properly.
inside the dialog class I programmatically add a pink box to the object to show that it does reach the init of the class
class TeamDialog: SKSpriteNode {
init(size: CGSize) {
super.init(texture: nil, color: .red, size: size)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
let pinkBox = SKSpriteNode(color: .magenta, size: CGSize(width: 100, height: 100))
pinkBox.zPosition = 500
addChild(pinkBox)
}
}
And in the Scene this is how I find the object and try to add it to the scene, it appears in the preview but nothing shows up on the screen
private func displayDialog() {
if let wtf = unarchiveNodeFromFile(file: "TeamDialog")! as SKNode? {
let layer = SKSpriteNode(color: .clear, size: self.size)
layer.zPosition = 5000
layer.position = CGPoint(x: self.size.width / 2, y: self.size.height / 2)
addChild(layer)
for child in wtf.children {
print("child \(child.name)")
if let teamDialog = child as? TeamDialog {
teamDialog.removeFromParent()
layer.zPosition = 1
layer.addChild(teamDialog)
}
}
}
}
func unarchiveNodeFromFile(file: String) -> SKNode? {
if let path = Bundle.main.path(forResource: file, ofType: "sks") {
let fileData = NSData.init(contentsOfFile: path)
let archiver = NSKeyedUnarchiver.init(forReadingWith: fileData as Data!)
archiver.setClass(SKNode.classForKeyedUnarchiver(), forClassName: "SKScene")
let node = archiver.decodeObject(forKey: NSKeyedArchiveRootObjectKey) as! SKNode
archiver.finishDecoding()
return node
} else {
return nil
}
}
Again, I can see it in the preview window but nothing appears when it is added to the scene.
Do I have to handle an object that has been unarchived differently or do something to it before I can add it to the scene?
I pulled the code out and have put it in a test project with the same results.
I've tried adding the sks file (as a node) to the scene with no luck, and i've tried isolating out the dialog from the sks file and adding it with no luck

The problem wasn't whether or not Loaded it as an unarchived file or if I loaded it as a SKReferenceNode. Although after finding out about and researching this SKReferenceNode is EXACTLY the way to go and was probably made for this purpose.
The problem was that in the scene editor the custom object was positioned beside the scene (x = -1500) because when I originally constructed it was in the scene.sks file and I didn't want it on top of the scene objects. when I transferred it to it's own sks file it kept it's positioning. I didn't think that this was a big deal because after I loaded the sks file into a variable I set it's position to CGPoint(x: 0, y: 0) in code. The problem is that you CANNOT move the loaded scene object around the existing scene, so if it is off screen it will stay offscreen. Setting the position did nothing, and it didn't matter if it was an unarchived file or SKReferenceNode it wouldn't move.
So i referenced the first child in the sks file and set it's position and viola!
If anyone is interested at what can be done with this setup, you can create and layout complex custom objects in their own sks file and then load them into your scene file. The beauty of doing it this way is that your sks files don't become cluttered and then your custom object sks's can be reference in multiple different scenes.
let path = Bundle.main.path(forResource: "TeamDialog", ofType: "sks")
let dialogScene = SKReferenceNode (url: URL (fileURLWithPath: path!))
if let teamDialog = dialogScene.childNode(withName: "//teamDialog") as? TeamDialog {
teamDialog.setup(scene: self)
teamDialog.position = CGPoint(x: 0, y: 0)
dialogScene.zPosition = 5000
addChild(dialogScene)
}

Related

How to create new scene from .sks file and programmatically change its size

Right now I load my GameScene like this
if let scene = SKScene(fileNamed: "GameScene") {
if bool {
// Change size to CGSize(a, b)
} else {
// Change size to CGSize(x, y)
}
view.allowsTransparency = true
scene.backgroundColor = .clear
view.presentScene(scene)
}
However, depending on a certain bool I want to be able to change the initial size of the GameScene. I know this can be done using let scene = GameScene(size: size) but that means I can't use the .sks file for the GameScene which I have most stuff on.
How can I variably change the initial size of the GameScene while still using the .sks file?
P.S. scene.size = CGSize(a, b) does not seem to change anything

Swift: Can't subclass SCNNode? Fatalerror

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)

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)

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()
}

Adding a spark particle sprite inside a view controller

I created an .sks particle emitter based on the spark template.
My app is a normal app (not a game). When a user clicks a button, I have a new View controller that shows modally over fullscreen so that I can blur the background.
In this modal, I created a view and gave it a class of SCNView see image below:
How can I load the particle .sks file to do the animation on that viewController on the Particles view?
Update
How to load a SceneKit particle systems in view controller?
As mentioned by #mnuages, you can use .scnp file instead of .sks, which is a SceneKit Particle System.
So the steps are:
Create a SceneKit Particle System, I called it ConfettiSceneKitParticleSystem.scnp
Then in your art-board, select the view and select the class SCNView for it like in the printscreen of the question
In your UIViewController:
class SomeVC: UIViewController {
#IBOutlet weak var particles: SCNView!
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene()
let particlesNode = SCNNode()
let particleSystem = SCNParticleSystem(named: "ConfettiSceneKitParticleSystem", inDirectory: "")
particlesNode.addParticleSystem(particleSystem!)
scene.rootNode.addChildNode(particlesNode)
particles.scene = scene
}
}
Et Voila...you have you animation :)
.sks files are SpriteKit particle systems. You can also create SceneKit particle systems in Xcode, they are .scnp files.
A .scnp file is basically an archived SCNParticleSystem that you can load with NSKeyedUnarchiver and add to your scene using -addParticleSystem:withTransform:.
It might be easier to create a SpriteKit Particle File (which is what you did). You can add it to your main view in your UIViewController.
Add this somewhere:
extension SKView {
convenience init(withEmitter name: String) {
self.init()
self.frame = UIScreen.main.bounds
backgroundColor = .clear
let scene = SKScene(size: self.frame.size)
scene.backgroundColor = .clear
guard let emitter = SKEmitterNode(fileNamed: name + ".sks") else { return }
emitter.name = name
emitter.position = CGPoint(x: self.frame.size.width / 2, y: self.frame.size.height / 2)
scene.addChild(emitter)
presentScene(scene)
}
}
To use:
override func viewWillAppear(_ animated: Bool) {
view.addSubview(SKView(withEmitter: "ParticleFileName"))
}

Resources