How to use properties from SCNFloor with Swift and Scenekit? - swift

I can't figure out how to access properties from the Scenekit SCNFloor class using Swift, in objective-c they do it like this:
SCNNode*floor = [SCNNode node];
floor.geometry = [SCNFloor floor];
((SCNFloor*)floor.geometry).reflectionFalloffEnd = 10;
This creates the floor, how do i acces "reflectionFalloffEnd"?
let levelFloor = SCNNode()
levelFloor.geometry = SCNFloor()
Thanks
EDIT Thanks for the quick responses it works now :)

Whether you're in ObjC or Swift, for the compiler to be happy with you using SCNFloor properties or methods on a reference, that reference needs to have the type SCNFloor.
You can do that inline with a cast:
// ObjC
((SCNFloor*)floor.geometry).reflectionFalloffEnd = 10;
// Swift
(levelFloor.geometry as SCNFloor).reflectionFalloffEnd = 10
But that can get rather unwieldy, especially if you're going to set a bunch of properties on a geometry. I prefer to just use local variables (or constants, rather, since the references don't need to change):
let floor = SCNFloor()
floor.reflectionFalloffEnd = 10
floor.reflectivity = 0.5
let floorNode = SCNNode(geometry: floor)
floorNode.position = SCNVector3(x: 0, y: -10.0, z: 0)
scene.rootNode.addChildNode(floorNode)

(levelFloor.geometry as SCNFloor).reflectionFalloffEnd = 10 is how you would make that cast in Swift - you were almost there!
as an alternative - if you are going to need to access this property often you may end up with a bunch of as casts in your code :) you may instead prefer to simply declare constants to hold those objects:
let levelFloor = SCNNode()
let levelFloorGeometry = SCNFloor()
levelFloor.geometry = levelFloorGeometry
levelFloorGeometry.reflectionFalloffEnd = 10
// and so on as this property of your SCNNode's geometry needs to change throughout your app

Related

Animating SCN Objects with CAKeyframeAnimation in Swift SceneKit

I'm writing an application that displays chemical reactions and molecules in 3D. I read in all the values and positions of each atom from a text file and I am creating each atom shape with SCNSpheres. I have all the other values I need read in properly, but I can't figure out how to add keyframe animations to each node object in my scene.
I set up the molecules like this in ViewController.swift
func makeAtom(atomName: String, coords: [Double], scene: SCNScene) {
guard let radius = atomRadii[atomName]?.atomicRadius else { return }
atoms.append(Atom(name: atomName, x: coords[0], y: coords[1], z: coords[2], radius: radius, positions: []))
let atomGeometry = SCNSphere(radius: CGFloat(radius))
let atomNode = SCNNode(geometry: atomGeometry)
atomNode.position = SCNVector3(coords[0], coords[1], coords[2])
scene.rootNode.addChildNode(atomNode)
atomNodes.append(atomNode)
}
I know that the CAKeyframeAnimations are supposed to be set up like this
let animation = CAKeyframeAnimation()
animation.keyPath = "position.y"
animation.values = [0, 300, 0]
animation.keyTimes = [0, 0.5, 1]
animation.duration = 2
animation.isAdditive = true
vw.layer.add(animation, forKey: "move")
I just don't know where I should be declaring these animations and how the layers factor into all this. What layer should I be adding the animations to? And how can I trigger them to play? I've been searching all over the internet for help with this but I can't find anything that just shows a simple implementation.
I can provide more code if need be, I'm pretty new to StackOverflow and want to make sure I'm doing this right.
You can do it different ways, but I like this method: 58001288 (my answer here) as you can pre-build some animations using scenekit and then run them as a sequence.
Per the comment.. needed more room.
A sequence is a fixed thing. You can start, repeat, and stop it. However, it's hard to interact with it during phases.
If you really need to do that, then one way is to break up your sequence into its parts and call the next one yourself after a completion handler of the current one. I keep an array and a counter so that I know where I am. So basically it's just a queue of actions that I manage - if I'm on a certain step and the button is pressed, then I can cancel all current actions, set the desired effect, and restart it.
Edit:
The completion handler calls itself at the end of the function and advances its own array count so that the next one in the list can be called. This is obviously a bit dangerous, so I would use sparingly, but that's how I did it. I started mine on a timer, then don't forget to clean it up. I had a global GAME_ACTIVE switch and within the code I checked for it before calling myself again.
Edit2: This is actually a moveTo, but it's still just a custom set of SCNActions that calls itself when complete based on duration so that it immediately goes to the next one without a delay.
func moveTo()
{
let vPanelName = moves[moveCount]
let vLaneNode = grid.gridPanels[vPanelName]!.laneNodes[lane]
let vAction = SCNAction.move(to: vLaneNode.presentation.position, duration: TimeInterval(data.getAttackSpeed(vGameType: gameType)))
node.runAction(vAction, completionHandler:
{
self.moveCount += 1
if(self.moveCount >= self.moves.count - 1)
{
self.killMe(vRealKill: false)
return
}
else
{
self.moveTo()
}
})
}

getBoundingBoxSize crashing sometimes

I have a strange problem with using getBoundingBoxSize on SCNText geometry - it sometimes causes a crash - EXC_BAD_ACCESS (code=1). Can't figure out why. I use it on main thread.
This happens on iOS 12. Can someone help me resolve this?
let node = node as! AugmentedRealityView3DObjectNode
let mediaNode = mediaNode as! SCNNode
let fontScalling: Float = 0.5
let marginScalling: Float = 0.9
let planeGeometry = mediaNode.geometry as! SCNPlane
let textNode = mediaNode.childNodes.first!
let mediaTextGeometry = textNode.geometry as! SCNText
mediaTextGeometry.containerFrame = CGRect(withSize: CGSize(fromSize3D: node.augmentedRealityView.sizeForMainNode(node: node)) / CGFloat(fontScalling * marginScalling), centeredInContainerOfSize: .zero)
let centerPoint = SCNVector3(getBoundingCenterPoint: mediaTextGeometry.boundingBox)
textNode.position = SCNVector3(-centerPoint.x, -centerPoint.y, -centerPoint.z) * fontScalling * marginScalling
textNode.scale = SCNVector3(qubicVector: fontScalling * marginScalling)
// TODO: This causes crashes sometines in iOS 12.
let boundingBoxSize = SCNVector3(getBoundingBoxSize: mediaTextGeometry.boundingBox) * fontScalling / marginScalling
planeGeometry.width = CGFloat(boundingBoxSize.x)
planeGeometry.height = CGFloat(boundingBoxSize.y)
This is still broken 2.5 years later in SceneKit in iOS 14.2. The best I can tell, you cannot invoke .boundingBox on a node with SCNText geometry until after the node has been rendered at least once. My guess is that something is not initialized until the render loop and boundingBox fails to check for it being uninitialized.
My workaround is the usual hack of putting the .boundingBox in a DispatchQueue.main.asyc {} block so (hopefully) the node & geometry have been initialized. Depending on your app, this may not be feasible.
You are crashing in the Objective-C memory management system, which usually indicates some kind of heap corruption. I would guess that some object has been prematurely deallocated, while references to it still exist. This is super common, and is often called a dangling pointer.
I'd suggest checking out the Zombies tool in Instruments. It can help find this kinds of errors. Good luck!

SpriteKit, copying a physics body does NOT copy the settings?

It seems almost incredible but,
let pb = model.physicsBody?.copy() as! SKPhysicsBody
print("orig .. \(model.physicsBody!.linearDamping)")
print("pb .. \(pb.linearDamping)")
they are NOT the same. No really.
This is so bizarre that I must be doing something wrong. What am I doing wrong?
Other than just manually copying all the properties, and maintaining the code forever as that evolves, how to do this??
Here's a dupe-qualities function if anyone needs it, to save typing. (This of 2017 - of course it has to be maintained forever as Apple add qualities.)
extension SKSpriteNode
{
func dupe() -> Any {
// annoyingly, Apple does not provide a dupe function as you would use
// when spawning from a model.
let c = self.copy() as! SKSpriteNode
c.physicsBody = self.physicsBody!.copy() as? SKPhysicsBody
c.physicsBody?.affectedByGravity = self.physicsBody!.affectedByGravity
c.physicsBody?.allowsRotation = self.physicsBody!.allowsRotation
c.physicsBody?.isDynamic = self.physicsBody!.isDynamic
c.physicsBody?.pinned = self.physicsBody!.pinned
c.physicsBody?.mass = self.physicsBody!.mass
c.physicsBody?.density = self.physicsBody!.density
c.physicsBody?.friction = self.physicsBody!.friction
c.physicsBody?.restitution = self.physicsBody!.restitution
c.physicsBody?.linearDamping = self.physicsBody!.linearDamping
c.physicsBody?.angularDamping = self.physicsBody!.angularDamping
c.physicsBody?.categoryBitMask = self.physicsBody!.categoryBitMask
c.physicsBody?.collisionBitMask = self.physicsBody!.collisionBitMask
c.physicsBody?.fieldBitMask = self.physicsBody!.fieldBitMask
c.physicsBody?.contactTestBitMask = self.physicsBody!.contactTestBitMask
return c
}
}
(Just one man's opinion, I feel it's better to not just override the NSCopy, since, the whole thing is a touchy issue and it's probably better to simply be explicit for the sake of the next engineer. It's very common to "dupe" the qualities of a game object to another, so, this is fine.)
It has to do with SKPhysicsBody being a wrapper class to PKPhysicsBody
Essentially what is going on is when you create a copy of SKPhysicsBody, it creates a new instance of PKPhysicsBody, not a copy of it.
To get around this, you need to write an extension that fills in the values for you:
extension SKPhysicsBody
{
override open func copy() -> Any {
guard let body = super.copy() as? SKPhysicsBody else {fatalError("SKPhysicsBody.copy() failed")}
body.affectedbyGravity = affectedByGravity
body.allowsRotation= allowsRotation
body.isDynamic= isDynamic
body.mass= mass
body.density = density
body.friction = friction
body.restitution = restitution
body.linearDamping = linearDamping
body.angularDamping = angularDamping
return body
}
}
Note, I typed this by hand, I do not have XCode available at this time to test if it does work.

Copying SCNParticleSystem doesn't seem to work well

I'm trying to use an SCNParticleSystem as a "template" for others. I basically want the exact same properties except for the color animation for the particles. Here's what I've got so far:
if let node = self.findNodeWithName(nodeName),
let copiedParticleSystem: SCNParticleSystem = particleSystemToCopy.copy() as? SCNParticleSystem,
let colorController = copiedParticleSystem.propertyControllers?[SCNParticleSystem.ParticleProperty.color],
let animation: CAKeyframeAnimation = colorController.animation as? CAKeyframeAnimation {
guard animation.values?.count == animationColors.count else {
return nil
}
// Need to copy both the animations and the controllers
let copiedAnimation: CAKeyframeAnimation = animation.copy() as! CAKeyframeAnimation
copiedAnimation.values = animationColors
let copiedController: SCNParticlePropertyController = colorController.copy() as! SCNParticlePropertyController
copiedController.animation = copiedAnimation
// Finally set the new copied controller
copiedParticleSystem.propertyControllers?[SCNParticleSystem.ParticleProperty.color] = copiedController
// Add the particle system to the desired node
node.addParticleSystem(copiedParticleSystem)
// Some other work ...
}
I copy not only the SCNParticleSystem, but also SCNParticlePropertyController and CAKeyframeAnimation just to be safe. I've found that I've had to manually do these "deep" copies "manually" since a .copy() on SCNParticleSystem doesn't copy the animation, etc.
When I turn on the copied particle system on the node it was added to (by setting the birthRate to a positive number), nothing happens.
I don't think the problem is with the node that I've added it to, since I've tried adding the particleSystemToCopy to that node and turning that on, and the original particle system becomes visible in that case. This seems to indicate to me that the the node I've added the copied particle system to is OK in terms of its geometry, rendering order, etc.
Something else perhaps worth mentioning: the scene is loaded from a .scn file and not created programmatically in code. In theory that shouldn't affect anything, but who knows ...
Any ideas on why this copied particle system doesn't do anything when I turn it on?
Do not use copy() method for particle systems !
copy() method does not allow copy particles' color (copied particles will be default white).
You can test it with the following code:
let particleSystem01 = SCNParticleSystem()
particleSystem01.birthRate = 2
particleSystem01.particleSize = 0.5
particleSystem01.particleColor = .systemIndigo // INDIGO
particleSystem01.emitterShape = .some(SCNSphere(radius: 2.0))
let particlesNode01 = SCNNode()
particlesNode01.addParticleSystem(particleSystem01)
particlesNode01.position.y = -3
sceneView.scene.rootNode.addChildNode(particlesNode01)
let particleSystem02 = particleSystem01.copy() // WHITE
let particlesNode02 = SCNNode()
particlesNode02.addParticleSystem(particleSystem02 as! SCNParticleSystem)
particlesNode02.position.y = 3
sceneView.scene.rootNode.addChildNode(particlesNode02)
Use clone() method for nodes instead !
clone() method works more consistently for 3d objects and particle systems and it can help you save particles' color but, of course, doesn't allow save a position for each individual particle.
let particlesNode02 = particlesNode01.clone() // INDIGO
particlesNode02.position.y = 3
sceneView.scene.rootNode.addChildNode(particlesNode02)

Create Array of SKShapeNodes. Swift > Xcode 6

In swift, the code below is called at a certain rate (2 times per second) from didMoveToView(). Every time this function is called, a new circlenode should appear on the screen. But instead of continually adding them, when it tries to add the second one it throws an error : attempted to add sknode which already has a parent. I figured out that you can't make a duplicate node on the same view. With that figured out, I came to the conclusion that I would need to make an array of SKShapeNodes so whenever the function is called, it would take one from the array and add it to the view. When a node has reached the bottom (y = -20) in my case, then it would need to remove the node, and make it available again to use.
So, my question: How to I make an array of SKShapeNodes, so when my function below is called it will take a new node and display it to the view? Also, when nodes exit the view, it will need to make the node available again to use.
let circlenode = SKShapeNode(circleOfRadius: 25) //GLOBAL
func thecircle() {
circlenode.strokeColor = UIColor.whiteColor()
circlenode.fillColor = UIColor.redColor()
let initialx = CGFloat(20)
let initialy = CGFloat(1015)
let initialposition = CGPoint(x: initialx, y: initialy)
circlenode.position = initialposition
self.addChild(circlenode)
let action1 = SKAction.moveTo(CGPoint(x: initialx, y: -20), duration: NSTimeInterval(5))
let action2 = SKAction.removeFromParent()
circlenode.runAction(SKAction.sequence([action1, action2]))
}
So you're right in that the problem the code above is that an SKNode can only have one parent.
You have 2 approaches you could take.
Create an array of your SKShapeNodes
Create a new SKShapeNode as needed
The former has the constraint that you need to keep tabs on your total amount of circles, else you'll exceed your bounds. If you remove items, this also means accounting for it. The latter has the overhead of generating the SKShapeNode when you need it. More than likely this will not be an issue.
To create the array you'd do something like (option 1):
var circleArray:[SKShapeNode] = [SKShapeNode]() // Property in your class
// Code in your init or wherever else you want, depending on what class this is.
// 10 is just an arbitrary number
for var i=0;i<10;i++ {
circleArray.append(SKShapeNode(circleOfRadius: 25))
}
When you want to add it in your thecircle, where you were calling self.addChild(circlenode) something like:
if numCircles < circleArray.count {
SKShapeNode *circlenode = circleArray[numCircles]
// Do other initialization here
self.addChild(circlenode)
numCircles++
}
Or you can do (option 2):
SKShapeNode *circlenode = SKShapeNode(circleOfRadius: 25)
// Do other initialization here
self.addChild(circle node)
I'd probably do option 2 in order to not have to deal with any of the management of number of outstanding circles in the array. This is particularly important if you ever remove circles and want to recycle them.