Create Array of SKShapeNodes. Swift > Xcode 6 - swift

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.

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

Making a node's sprite change as an animation (spriteKit)

im very new to swift, i have made a sprite kit game with a coin sprite. I want to make it spin so ive made 6 sprites in total. Im trying to get a continuous loop of spinning by quickly changing the sprites. I have tried to do this with the code below.
//This will hold all of the coin spinning sprites
let coinTextures : NSMutableArray = []
//There are 6 in total, so loop through and add them
for i in 0..<6 {
let texture : SKTexture = SKTexture(imageNamed: "Coin\(i + 1)")
coinTextures.insert(texture, at: i)
}
//When printing coinTextures, they have all been added
//Define the animation with a wait time of 0.1 seconds, also repeat forever
let coinAnimation = SKAction.repeatForever(SKAction.animate(with: coinTextures as! [SKTexture], timePerFrame: 0.1))
//Get the coin i want to spin and run the action!
SKAction.run(coinAnimation, onChildWithName: "coin1")
As i said im very new so im not sure what ive done wrong here.
Also the name of the coin i want to spin is "coin1" and the sprites so from coin1 to coin 6
You are almost there.
The problem is that your final line creates an action, but not running it on anything...
You got two alternatives:
1) Run your action on your scene
// Create an action that will run on a child
let action = SKAction.run(coinAnimation, onChildWithName: "coin1")
scene?.run(action)
or
2) Run the action directly on the child
// Assuming that you have a reference to coin1
coin1.run(coinAnimation)
As a sidenote, your array could be declared as var coinTextures: [SKTexture] = [], you can use append to add items to it and avoid the casting when you pass the textures to the action.
Or you can use a more compact form to construct your textures array:
let coinTextures = (1...6).map { SKTexture(imageNamed: "Coin\($0)") }
I hope that this makes sense

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)

updating an SKLabel to show the right integer

ok so first off I have an achievement Variable that is defined from a struct like so:
struct Achieve {
var node: SKSpriteNode?
var aName:String = ""
var aDes:String = ""
var aImage: String = "" {
didSet {
node?.texture = SKTexture(imageNamed: aImage)
}
}
var aAmount:Int = 0
var aRequired:Int = 0
var aStage:Int = 0
}
var Ach1 = Achieve(node: nil, aName: "Player", aDes: "Games Played", aImage: "locked", aAmount: 0, aRequired: 10, aStage: 1)
My problem is when I try and change the number of the aAmount property it isn't displayed on the SKLabel that displays the amount it just stays at 0 my sklabel is defined in a function like below:
func generateLabels(location: CGPoint, page:SKSpriteNode, text: String) -> SKLabelNode {
let node = SKLabelNode()
node.fontColor = UIColor.whiteColor()
node.fontName = "Helvetica"
node.position = location
node.fontSize = 15
node.text = text
page.addChild(node)
return node
}
func menu() {
_ = generateLabels(CGPointMake(0, -285), page:page1ScrollView, text: "\(Ach1.aAmount) / \(Ach1.aRequired)")
}
It seems to work if I make changes to aAmount when I stop running it and then change it and then run the game again. it also seems to make changes to the aAmount property during gameplay but it doesn't seem to update the label for some reason during the gameplay. Can someone please tell me why it won't update?
Also i'm updating the aAmount property like so:
Ach1.aAmount += 1
print("\(Ach1.Amount)")
In your Achieve struct, add a property called label:
var label: SKLabelNode?
And change the aAmount property to look something like this:
var aAmount: Int = 0 {
didSet {
label?.text = "\(aAmount) / \(aRequired)"
}
}
Now when you generate the labels, replace this:
_ = generateLabels(CGPointMake(0, -285), page:page1ScrollView,
text: "\(Ach1.aAmount) / \(Ach1.aRequired)")
with this
var label = _ = generateLabels(CGPointMake(0, -285), page:page1ScrollView,
text: "\(Ach1.aAmount) / \(Ach1.aRequired)")
Ach1.label = label
Now when you change the aAmount property of Ach1, the label's text should change as well.
Suggestions:
Judging from this and your last question about sprite node images not updating, I think your programming skills are not mature enough. In the long term, you should learn the basics first. Try playing around with UIKit and read more others' code. See how other people do things. You will definitely learn a lot. In the short term though, you shouldn't make each of all these properties binds to a node. That will make Achieve too dependent. What you should do is make Achieve a subclass of SKSpriteNode and add those other sprite nodes and label nodes as sub nodes of the Achieve node.
If I read your code, there isn't any part where you update your SKLabelNode.
If you call the function :
func menu() {
_ = generateLabels(CGPointMake(0, -285), page:page1ScrollView, text: "\(Ach1.aAmount) / \(Ach1.aRequired)")
}
for example to didMoveToView, it generate a label with your aAmount value when you call your scene (so 1 time only as didMoveToView works).
Instead, assuming that you call this menu function many times, it generate everytime a new label but there is no code written from you where you remove the previous label or labels added in past (see page.addChild(node)) and I don't understand why you want to return a label from your function if you don't use it, in fact you assign your result to _. Also you don't explain what is "page1ScrollView", are you using a scrollview?
Anyway, the best mode to update your label is simply to create 1 time only (for example on didMoveToView) and, everytime you want to update it, doing:
myNodeLabel.text = "\(Ach1.aAmount) / \(Ach1.aRequired)"
instead of re-create it everytime. I hope this helps to understand the dynamics that prevent your label to upgrade.
You have to reassign the text of the SKLabel. So when you call the function to change the number (or whatever it is), also run the code to make the label what you want it to be. My guess as to why it only works sometimes is that sometimes you call it in the update (or somewhere else that gets called repeatedly) and sometimes you call it somewhere like didMoveToView (only called once, so label doesn't get updated because of the way your code is).
Just whenever you update aAmount, add this code after it:
label.text = "\(aAmount)"

How can I create delay inside while loop in Swift 2?

I would need help with this while loop - what I'm trying to do is slow down the whole process of removing and adding new circles while radius is changing every time this happens. I'm becoming really desperate, I've tried using both dispatch_after and sleep inside the loop (which I found online) but neither of them is suitable, they basically stop the whole app. If I put them in the while loop, nothing happens. Thanks in advance!
while radius < 100 {
self.removeAllChildren()
addCircle()
radius++
print(radius)
}
Basically you just need to do few simple things:
Wait for a certain duration and add a node to the scene
Repeat this step forever (or certain number of times)
Here is the example of how you can do it. The important part is action sequence. You create the step above, and repeat it forever. Each time you check radius value and based on that you stop the action (remove it by the key). And that's it. You can change spawning speed by changing action's duration parameter.
Using NSTimer might seem like an easier solution, but NSTimer doesn't respect node's (or scene's or view's) paused state. So, imagine this situation:
You start spawning of nodes
User receive phone call and app automatically goes to background
Because NSTimer is not automatically paused, the nodes will continue with spawning. So you have to take an additional step and invalidate/restart timer by your self. When using SKAction, this is done automatically. There are some other flaws of using NSTimer in SpriteKit, search SO about all that, there are some posts which covering all this.
import SpriteKit
class GameScene: SKScene{
var radius:UInt32 = 0
override func didMoveToView(view: SKView) {
startSpawning()
}
func startSpawning(){
let wait = SKAction.waitForDuration(0.5)
// let wait = SKAction.waitForDuration(1, withRange: 0.4) // randomize wait duration
let addNode = SKAction.runBlock({
[unowned self] in //http://stackoverflow.com/a/24320474/3402095 - read about strong reference cycles here
if(self.radius >= 30){
if self.actionForKey("spawning") != nil {
self.removeActionForKey("spawning")
}
}
self.radius++
let sprite = self.spawnNode()
sprite.position = CGPoint(x: Int(arc4random()) % 300, y: Int(arc4random()) % 300) // change this to randomize sprite's position to suit your needs
self.addChild(sprite)
})
//wait & add node
let sequence = SKAction.sequence([wait, addNode])
//repeat forever
runAction(SKAction.repeatActionForever(sequence), withKey: "spawning")
}
func spawnNode()->SKSpriteNode{
let sprite = SKSpriteNode(color: SKColor.purpleColor(), size: CGSize(width: 50, height: 50))
//Do sprite initialization here
return sprite
}
}
The sleep stops the whole app because you are running the loop on the main thread.
You can solve this problem in one of two ways...
1) Use an NSTimer. Set it to the amount of time you want to delay. Put the body of the loop in the timer's trigger function and disable the timer when it has been called enough times. This is the best solution.
2) Wrap the loop in a dispatch async block to a background thread and put a sleep in the loop. If you do this though, you will have to wrap the UI code in another dispatch async block that comes back to the main thread.