How to copy SKSpriteNode with SKPhysicsBody? - swift

I am curious about a situation that I came across today when trying to unarchive and copy a SKSpriteNode from one SKScene to another. In the output from the playground below you can see that both linearDamping and angularDamping or not being maintained after the copy (they seem to be dropping back to default values)
// PLAYGROUND_SW1.2 - SKSpriteNode Copy
import UIKit
import SpriteKit
// ORIGINAL
let spriteNode = SKSpriteNode()
spriteNode.name = "HAPPY_NODE"
let size = CGSize(width: 55.0, height: 150.0)
let physics = SKPhysicsBody(rectangleOfSize: size)
physics.linearDamping = 0.123
physics.angularDamping = 0.456
spriteNode.physicsBody = physics
// COPY
let spriteCopy = spriteNode.copy() as! SKSpriteNode
// ORIGINAL
spriteNode.name
spriteNode.physicsBody?.linearDamping
spriteNode.physicsBody?.angularDamping
spriteNode.physicsBody?.area
// COPY
spriteCopy.name
spriteCopy.physicsBody?.linearDamping
spriteCopy.physicsBody?.angularDamping
spriteCopy.physicsBody?.area
PLAYGROUND OUTPUT
I am not sure that I am copying this correctly, both SKSpriteNode and SKPhysicsBody conform to NSCopying If you look at the output above the area property is maintained after the copy and to my knowledge this is based on the size specified when the SKPhysicsBody was created.
Can anyone cast some light on this and maybe provide me with a pointer as to how I should be deep copying an SKSpriteNode?

I take one way to resolve your problem, probably is not the best way, but
//COPY
let spriteCopy = spriteNode.copy() as SKSpriteNode
let physicsCopy:SKPhysicsBody = spriteNode.physicsBody!;
...
//COPY PHYSICS BODY HARD MODE
spriteCopy.physicsBody = physicsCopy;
To fix this problem, I created one extension, and #mogelbuster suggested override default copy(), ant it sounds great.
extension SKSpriteNode
{
override open func copy() -> Any {
let node = super.copy() as! SKSpriteNode;
node.physicsBody = super.physicsBody;
return node;
}
}
With this extension you can do it, the default copy() method return Any because this you need cast to SKSpriteNode.
// COPY
let spriteCopy = spriteNode.copy() as! SKSpriteNode;
// ORIGINAL
spriteNode.name
spriteNode.physicsBody?.linearDamping
spriteNode.physicsBody?.angularDamping
spriteNode.physicsBody?.area
// COPY
spriteCopy.name
spriteCopy.physicsBody?.linearDamping
spriteCopy.physicsBody?.angularDamping
spriteCopy.physicsBody?.area

Related

Found nil when unwrapping texture value which I have previously set

so I am trying to add an SKSpritenode to my scene level2, and I want to give it a physics body with a texture that comes from an image I have uploaded into the assets folder of my project. the image is there and the name is right, but when launch the app I get a fatal error. I don't get where I'm going wrong as the app used to run smoothly until I added that last line of code.
import UIKit
import SpriteKit
import GameplayKit
class level2: SKScene {
var entities = [GKEntity]()
var graphs = [String : GKGraph]()
var perno = SKSpriteNode(fileNamed: "perno")
override func sceneDidLoad () {
print ("view2 Loaded")
self.perno = self.childNode(withName: "perno") as? SKSpriteNode
self.perno?.texture = SKTexture(imageNamed: "perno")
self.perno?.size.width = self.perno!.size.width * 5
self.perno!.physicsBody = SKPhysicsBody(texture: (perno?.texture)!, size: (perno?.texture?.size())!)
}
any hints?
Thanks!
Your optional handling is really inconsistent. Sometimes you use optional casting and optional chaining just to then use force unwrapping on the same variable in the same line. You should force unwrap the return value of SKSpriteNode(fileNamed: "perno"), since if that returns nil that's a programmer error and should be caught ASAP.
Your immediate issue seems to be that you try to overwrite perno in sceneDidLoad:
self.perno = self.childNode(withName: "perno") as? SKSpriteNode
There's no childNode added to your scene, so you just set perno to nil instead of its previously set value using var perno = SKSpriteNode(fileNamed: "perno"). Simply remove that line from sceneDidLoad, since it makes no sense, perno is already loaded during initialisation, since you assign a value to it.
class Level2: SKScene {
var entities = [GKEntity]()
var graphs = [String : GKGraph]()
let perno = SKSpriteNode(fileNamed: "perno")!
override func sceneDidLoad () {
print ("view2 Loaded")
self.perno.texture = SKTexture(imageNamed: "perno")
self.perno.size.width = self.perno.size.width * 5
self.perno.physicsBody = SKPhysicsBody(texture: perno.texture!, size: perno.texture!.size())
}
}
A better way to protect yourself is to use the guard statement. guard allows you to safely check if a variable is not nil without having to nest your code.
class Level2: SKScene {
var entities = [GKEntity]()
var graphs = [String : GKGraph]()
let perno = SKSpriteNode(fileNamed: "perno")!
override func sceneDidLoad () {
print ("view2 Loaded")
guard let texture = SKTexture(imageNamed: "perno")
else{
fatalError("Unable to find texture "perno")
}
self.perno.texture = texture
self.perno.size.width = self.perno.size.width * 5
self.perno.physicsBody = SKPhysicsBody(texture: texture, size: texture.size())
}
}
As you can see, we are not excessively unwrapping self.perno.texture, which means there will be less chances for us to accidentally use a ? instead of a !, and we have a clear cut error as to why our code is crashing.

swift - use an integer for loop in a variable's name

I' like to doe something like this :
for i in 0...3 {
let skspritenode\(i) = SKSpriteNode(imageNamed: "Layer_\(i)")
self.addChild(skspritenode\(i))
}
that doesn't work because my skspritenode(i) is a let and not a string
and I have tried this:
for i in 0...3 {
let skspritenode = SKSpriteNode(imageNamed: "Layer_\(i)")
self.addchild(skspritenode)
}
which doesn't work because skspritenode already has a parent.
Is there a way of doing this in a for loop or do i have to declare each let separately? Thank you in advanced
Why would you need to name each node differently? skspritenode variable is scoped in for loop, thus the following code should work just fine:
for i in 0...3 {
let skspritenode = SKSpriteNode(imageNamed: "Layer_\(i)")
self.addchild(skspritenode)
}
Your point
which doesn't work because skspritenode already has a parent
is incorrect, since you're creating new SKSpriteNode on each iteration.

SceneKit -– How to get animations for a .dae model?

Ok, I am working with ARKit and SceneKit here and I am having trouble looking at the other questions dealing with just SceneKit trying to have a model in .dae format and load in various animations to have that model run - now that we're in iOS11 seems that some solutions don't work.
Here is how I get my model - from a base .dae scene where no animations are applied. I am importing these with Maya -
var modelScene = SCNScene(named: "art.scnassets/ryderFinal3.dae")!
if let d = modelScene.rootNode.childNodes.first {
theDude.node = d
theDude.setupNode()
}
Then in Dude class:
func setupNode() {
node.scale = SCNVector3(x: modifier, y: modifier, z: modifier)
center(node: node)
}
the scaling and centering of axes is needing because my model was just not at the origin. That worked. Then now with a different scene called "Idle.dae" I try to load in an animation to later run on the model:
func animationFromSceneNamed(path: String) -> CAAnimation? {
let scene = SCNScene(named: path)
var animation:CAAnimation?
scene?.rootNode.enumerateChildNodes({ child, stop in
if let animKey = child.animationKeys.first {
animation = child.animation(forKey: animKey)
stop.pointee = true
}
})
return animation
}
I was going to do this for all my animations scenes that I import into Xcode and store all the animations in
var animations = [CAAnimation]()
First Xcode says animation(forKey: is deprecated and This does not work it seems to (from what I can tell) de-center and de-scale my model back to the huge size it was. It screws up its position because I expect making the model move in an animation, for example, would make the instantiated model in my game snap to that same position.
and other attempts cause crashes. I am very new to scene kit and trying to get a grip on how to properly animate a .dae model that I instantiate anywhere in the scene -
How in iOS11 does one load in an array of animations to apply to their SCNNode?
How do you make it so those animations are run on the model WHEREVER THE MODEL IS (not snapping it to some other position)?
At first I should confirm that CoreAnimation framework and some of its methods like animation(forKey:) instance method are really deprecated in iOS and macOS. But some parts of CoreAnimation framework are now implemented into SceneKit and other modules. In iOS 11+ and macOS 10.13+ you can use SCNAnimation class:
let animation = CAAnimation(scnAnimation: SCNAnimation)
and here SCNAnimation class has three useful initializers:
SCNAnimation(caAnimation: CAAnimation)
SCNAnimation(contentsOf: URL)
SCNAnimation(named: String)
In addition I should add that you can use not only animations baked in .dae file format, but also in .abc, .scn and .usdz.
Also, you can use SCNSceneSource class (iOS 8+ and macOS 10.8+) to examine the contents of a SCNScene file or to selectively extract certain elements of a scene without keeping the entire scene and all the assets it contains.
Here's how a code with implemented SCNSceneSource might look like:
#IBOutlet var sceneView: ARSCNView!
var animations = [String: CAAnimation]()
var idle: Bool = true
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
let scene = SCNScene()
sceneView.scene = scene
loadMultipleAnimations()
}
func loadMultipleAnimations() {
let idleScene = SCNScene(named: "art.scnassets/model.dae")!
let node = SCNNode()
for child in idleScene.rootNode.childNodes {
node.addChildNode(child)
}
node.position = SCNVector3(0, 0, -5)
node.scale = SCNVector3(0.45, 0.45, 0.45)
sceneView.scene.rootNode.addChildNode(node)
loadAnimation(withKey: "walking",
sceneName: "art.scnassets/walk_straight",
animationIdentifier: "walk_version02")
}
...
func loadAnimation(withKey: String, sceneName: String, animationIdentifier: String) {
let sceneURL = Bundle.main.url(forResource: sceneName, withExtension: "dae")
let sceneSource = SCNSceneSource(url: sceneURL!, options: nil)
if let animationObj = sceneSource?.entryWithIdentifier(animationIdentifier,
withClass: CAAnimation.self) {
animationObj.repeatCount = 1
animationObj.fadeInDuration = CGFloat(1)
animationObj.fadeOutDuration = CGFloat(0.5)
animations[withKey] = animationObj
}
}
...
func playAnimation(key: String) {
sceneView.scene.rootNode.addAnimation(animations[key]!, forKey: key)
}

Swift 3 (SpriteKit): Create a texture from a node tree

I have been trying to render a node tree into an SKTexture as a possible solution to another question here. I tried searching for an answer to this, and I came across one of Apple's Developer pages (view it here). It says the command which converts a node tree into an SKTexture is this:
func texture(from node: SKNode) -> SKTexture?
Although, the code is incomplete since there is no code which executes the conversion of a node tree to an SKTexture. Am I doing something wrong or what is the complete code that I should use to render a node tree into an SKTexture.
Thanks for any advice, help, and answers!
texture(from:) is an instance method of SKView that takes a node (with zero or more children) as a parameter and returns an optional SKTexture. Since the returned value is an optional, you'll need to unwrap it before using it.
From didMove(to:), you can use the view parameter to create a texture
if let texture = view.texture(from: node) {
let sprite = SKSpriteNode(texture:texture)
addChild(sprite)
}
From other methods in the SKScene subclass, use optional chaining
if let texture = self.view?.texture(from: node) {
let sprite = SKSpriteNode(texture:texture)
addChild(sprite)
}
You can also render the portion of the node-tree's contents inside of a rectangle with
let rect = CGRect(x:-width/2, y:-height/2, width:width, height:height)
if let texture = self.view?.texture(from: node, crop: rect) {
let sprite = SKSpriteNode(texture:texture)
addChild(sprite)
}

Adding multiple sprite nodes to the scene using single function/method in swift

Let's say I have 10 nodes, where all of the nodes are the dots image, which are node1 thru node10. I create node1 as the following:
func createNode1() -> SKNode {
let spriteNode = SKNode()
spriteNode.position = CGPointMake(CGRectGetMidX(self.frame)/1.35, CGRectGetMidY(self.frame)/1.32)
let sprite = SKSpriteNode(imageNamed: "dot_1")
sprite.zPosition = 3.0
sprite.name = "A1_Dot"
spriteNode.addChild(sprite)
return spriteNode
}
I create the rest of nodes by creating 9 more functions, where next one would be as func createNode2etc, all the way up to 10 functions, where the only difference between them is node's name and its location. Basically each node has different location in the scene and of course different image name. Is there a way to load of the 10 nodes to the scene at once and manipulate node's locations at the time of use.? I'm looking for a way to load all 10 nodes to scene using a single function or method and assign node's positions within this same function. Thanks.
You need to use a loop to iterate through an array of positions, and move your code that adds the node to the scene into the loop:
let positions = [CGPointMake(CGRectGetMidX(self.frame)/1.35, CGRectGetMidY(self.frame)/1.32), ... /*add your 9 other positions here*/]
positions.enumerate().forEach { (index, point) in
let spriteNode = SKNode()
spriteNode.position = point
let sprite = SKSpriteNode(imageNamed: "dot_\(index + 1)")
sprite.zPosition = 3.0
sprite.name = "A\(index + 1)_Dot"
spriteNode.addChild(sprite)
// Add spriteNode to the scene here
}
You can either use a loop as Jugale suggested or you could just pass the values you want into the method
For example
func createNode1(imageNamed imageNamed: String, name: String, pos: CGPoint) -> SKNode {
let spriteNode = SKNode()
spriteNode.position = pos
let sprite = SKSpriteNode(imageNamed: imageNamed)
sprite.zPosition = 3.0
sprite.name = name
spriteNode.addChild(sprite)
return spriteNode
}
And now in your scene you can add the nodes like so
let node1Pos = ...
node1 = createNode1(imageNamed: "...", name: "A1_Dot", pos: node1Pos)
let node2Pos = ...
node2 = createNode1(imageNamed: "...", name: "A1_Dot", pos: node2Pos)
I am saying ImageNamed twice in the create Node function because when you pass stuff into functions Swift by default does not require the first description to be typed when calling the method. (see below)
So if would say imageNamed only once than you would call it like so.
node1 = createNode1("...", pos: node1Pos)
Also your creating node function could be simplified, unless you specifically want to return a SKNode in the method.
func createNode1(imageNamed imageNamed: String, name: String, pos: CGPoint) -> SKSpriteNode {
let sprite = SKSpriteNode(imageNamed: imageNamed)
sprite.position = pos
sprite.zPosition = 3.0
sprite.name = name
addChild(sprite)
return sprite
}
Either way is acceptable solution. Here are full details. In baseScene we create function createNodes and call that function in didMoveToView where nodeA1 is given position and added to the scene as shown below.
override func didMoveToView(view: SKView) {
let nodeA1Pos = CGPointMake(CGRectGetMidX(self.frame)/1.5, CGRectGetMidY(self.frame)/2.0)
nodeA1 = createNodes(imageNamed: "dot_1", name: "A1_Dot", pos: nodeA1Pos)
addChild(nodeA1)
}
Then in Level1Scene which is subclass of baseScene we just give a new position to nodeA1 which will override position originally set in baseScene:
override func didMoveToView(view: SKView) {
super.didMoveToView(view)
nodeA1.position = CGPointMake(CGRectGetMidX(self.frame)/1.3, CGRectGetMidY(self.frame)/0.67)
}
This way of subclassing saves a lot of time and code as only one function is used to generate all common sprite nodes.
All thanks to crashoverride777 !!!!