SpriteKit SKEmitterNode particleAction not working in Xcode 8 / iOS 10 - sprite-kit

I have an iOS 10 SpriteKit project where I'm trying to put actions on particles from a basic particle emitter created from the "snow" particle template in Xcode 8:
let snowPath = Bundle.main.path(forResource: "Snow", ofType: "sks")!
snowEmitter = NSKeyedUnarchiver.unarchiveObject(withFile: snowPath) as! SKEmitterNode
snowEmitter.position = CGPoint(x: 0, y: size.height / 2)
snowEmitter.particlePositionRange = CGVector(dx: size.width, dy: 0)
snowEmitter.particleAction = SKAction.scale(to: 3, duration: 3)
effectLayer.addChild(snowEmitter) // effectLayer is a SKNode on the scene
The emitter works as it should, but no matter what kind of SKAction I set particleAction to it gets ignored. Has anyone else experienced this?
Update: Doesn't work with Xcode 7 and iOS 9 either.

I think this still might be a leftover iOS 9 bug, not 100% sure. I just tried myself and I cannot get it to work as well.
SKEmitterNode particleAction not working iOS9 Beta
Can you not achieve the same effect using the particles settings directly in snow.sks in the inspector on the right?
You are probably looking at †hose two settings and its subsettings.
1) Particle life cycle (start, range)
2) Particle scale (start, range, speed)
This article has a nice description of each setting.
http://www.techotopia.com/index.php/An_iOS_8_Sprite_Kit_Particle_Emitter_Tutorial#Particle_Birthrate
As a general tip
Your code is not very safe in the first 2 lines because you force unwrapped the snow particle.
If you ever change the name and forget about it or the file becomes corrupted than you will crash. You should change it to something like this
guard let snowPath = Bundle.main.path(forResource: "Snow", ofType: "sks") else { return } // or if let snowPath = ...
snowEmitter = NSKeyedUnarchiver.unarchiveObject(withFile: snowPath) as? SKEmitterNode
...
You can also simply this code a lot by simple saying this where you define your snowEmitter property
let snowEmitter = SKEmitterNode(fileNamed: "Snow")
This will return an optional as well, just like your old code. Than in your method where you set up the emitter say something like this (dont use !)
if let snowEmitter = snowEmitter {
snowEmitter.position =
...
}
Hope this helps

There's another way to achieve the goal.
Make two or more particle systems.
Create two or more noise fields.
Bitmask match one each of the particle systems to one of the noise fields.
Put the noise fields down the bottom, where you want the wiggles to happen
Adjust the noise fields to taste.

As a 2021 datapoint, whilst adding particle emitters to Touchgram, I spent a couple of days exploring this. I came to the conclusion that they were broken in iOS9 and never fixed.

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

PhysicsBody: Could not create physics body

I have an entire working game that has been up on the App Store, but since iOS 13 it simply does not work. I've installed the game onto my device via Xcode and I'm getting a lot of errors coming up saying:
PhysicsBody: Could not create physics body.
I've been creating my SKSpriteNodes like this:
let bird = SKSpriteNode(texture: SKTextureAtlas(named:"player").textureNamed("bird0001"))
bird.physicsBody = SKPhysicsBody(texture: bird.texture!,
size: bird.size)
Based on some research, this is possibly an ongoing bug with iOS and Xcode. Could someone please confirm if this is the case, as this seems to be like a major problem for Games on the app store that create their SKSpriteNodes using textures?
Is there a fix to this where textures are needed?
OK, here's a test of different approaches for avoiding this bug as of iOS 13.3 (edit also now tried on 13.3.1) and Xcode version 11.3. Full source of the test at this link:
https://github.com/bg2b/bugtest
Relevant code:
func addShip(_ texture: SKTexture, how: String) {
let sprite = SKSpriteNode(texture: texture)
sprite.position = CGPoint(x: x, y: 0)
sprite.zRotation = .pi / 4
x += 100
sprite.physicsBody = SKPhysicsBody(texture: texture, size: sprite.size)
if sprite.physicsBody == nil {
print("\(how) failed")
} else {
print("\(how) worked")
}
addChild(sprite)
}
override func didMove(to view: SKView) {
// The atlas version of a texture
addShip(SKTexture(imageNamed: "ship_blue"), how: "simple atlas reference")
// From an atlas, but call size() to force loading
let texture = SKTexture(imageNamed: "ship_blue")
_ = texture.size()
addShip(texture, how: "atlas force load")
// Reconstruct via CGImage (size would be wrong because of 2x)
let cgTexture = SKTexture(cgImage: texture.cgImage())
addShip(cgTexture, how: "reconstruct via cgImage")
// Re-render using view
let renderedTexture = view.texture(from: SKSpriteNode(texture: texture))!
addShip(renderedTexture, how: "re-render using view")
// Non-atlas texture
addShip(SKTexture(imageNamed: "nonatlas_ship_blue"), how: "not in atlas")
}
Summary:
Simply referencing the texture from an atlas and making the physics body may fail
Force-loading the texture by calling size() before making the body fails
Trying to make a new texture by going through cgImage() fails (the image itself is broken, probably related to the same bug)
Rendering to a texture using a view and then making the physics body from that new texture works
Making the physics body from a non-atlas copy of the texture works
Console output from the test program showing what works and what does not:
2020-02-01 06:23:51.872975-0500 bugtest[14399:9898087] PhysicsBody: Could not create physics body.
simple atlas reference failed
2020-02-01 06:23:51.886387-0500 bugtest[14399:9898087] PhysicsBody: Could not create physics body.
atlas force load failed
2020-02-01 06:23:51.913927-0500 bugtest[14399:9898087] PhysicsBody: Could not create physics body.
reconstruct via cgImage failed
re-render using view worked
not in atlas worked
Here's a screen shot showing the effect of the different approaches. You have to look a bit closely, but only the last two have valid physics bodies.

SceneKit: Rotate node but define target angles

EDIT: I have solved the problem and will post the solution in the next couple of days.
I'm building 3D achievements similar to Apple's Activity app.
I've already loaded my 3D model (a scene with a single node), can show it, and can tap on it to apply a rotational force:
#objc func objectTapped(_ gesture: UITapGestureRecognizer) {
let tapLocation = gesture.location(in: scnView)
let hitResults = scnView.hitTest(tapLocation, options: [:])
if let tappedNode = (hitResults.first { $0.node === badgeNode })?.node {
let pos = Float(tapLocation.x) - tappedNode.boundingBox.max.x
let tappedVector = SCNVector4(x: 0, y: pos, z: 0, w: 0.1)
tappedNode.physicsBody?.applyTorque(tappedVector,
asImpulse: true)
}
}
This works fine. Now to the tricky part:
I want the node to rotate until it either shows its front or backside (like in the Activity app), where it then should stop. It should stop naturally, which means it can overshoot a bit and then return.
To describe it with pictures - here I am holding the node in this position...
...and if I let go of the node, it will rotate to show the front side, which includes a little bit of overshooting. This is the ending position:
Since I'm quite new to SceneKit, I have troubles figuring out how to achieve this effect. It seems like I can achieve that by using SceneKit objects like gravity fields, without having to calculate a whole lot of stuff by myself, or at least that's what I'm hoping for.
I don't necessarily ask for a full solution, I basically just need a point in the right direction. Thanks in advance!

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!

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)