Performing actions in order (not asynchronously) on different nodes - swift

Here is an illustration of what I am trying to do:
Here is my code so far:
import SpriteKit
class GameScene: SKScene {
let mySquare1 = SKShapeNode(rectOfSize:CGSize(width: 50, height: 50))
let mySquare2 = SKShapeNode(rectOfSize:CGSize(width: 50, height: 50))
override func didMoveToView(view: SKView) {
mySquare1.position = CGPoint(x: 100, y:100)
mySquare2.position = CGPoint(x: 300, y:100)
mySquare1.fillColor = SKColor.blueColor()
mySquare2.fillColor = SKColor.blueColor()
self.addChild(mySquare1)
self.addChild(mySquare2)
let moveAction1 = SKAction.moveTo(CGPoint(x:250, y:100), duration: 1)
mySquare1.runAction(moveAction1)
let moveAction2 = SKAction.moveTo(CGPoint(x:300, y:350), duration: 1)
mySquare2.runAction(moveAction2)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
}
override func update(currentTime: CFTimeInterval) {
}
}
My problem is, I am trying to move the rectangles synchronously (not asynchronously). That is, I want my first rectangle start moving, finish its movement, stop. And then, start my second rectangle moving, finish its movement and stop.
Currently what happens is that, when I run my program, they both start moving at the same time.
I also found SKAction.sequence for actions to play in order, however, I only can use this for actions on the same object. Not in different objects like in my example.

If you want to move the two rectangles sequentially (not in parallel), you could use the completion property of the first action like this (apple docs):
import SpriteKit
class GameScene: SKScene {
let mySquare1 = SKShapeNode(rectOfSize:CGSize(width: 50, height: 50))
let mySquare2 = SKShapeNode(rectOfSize:CGSize(width: 50, height: 50))
override func didMoveToView(view: SKView) {
mySquare1.position = CGPoint(x: 100, y:100)
mySquare2.position = CGPoint(x: 300, y:100)
mySquare1.fillColor = SKColor.blueColor()
mySquare2.fillColor = SKColor.blueColor()
self.addChild(mySquare1)
self.addChild(mySquare2)
let move1 = SKAction.moveToX(250, duration: 1.0)
let move2 = SKAction.moveToY(250, duration: 1.0)
self.mySquare1.runAction(move1,completion: {
self.mySquare2.runAction(move2)
})
}
}

You could use the SKAction for running blocks. Then sequence them from the scene.
For example:
func example() {
let firstRectMove = SKAction.runBlock({
let move1 = SKAction.moveToX(20, duration: 1.0)
let move2 = SKAction.moveToY(20, duration: 1.0)
rec1.runAction(SKAction.sequence([move1,move2]))
})
let actionWait = SKAction.waitForDuration(2.0)
let secondRectMove = SKAction.runBlock({
let move1 = SKAction.moveToX(20, duration: 1.0)
let move2 = SKAction.moveToY(20, duration: 1.0)
rec2.runAction(SKAction.sequence([move1,move2]))
})
//self is an SKScene or any other node really...
self.runAction(SKAction.sequence([firstRectMove,actionWait,secondRectMove]))
}

As Alessandro Ornano suggests, a way to accomplish this is run your first action with a completion block, where a block of code runs after the completion of the original action. A downside to this approach is that it can create a pyramid of doom when you need to chain together more than two actions at a time.
An alternative way to avoid to the "pyramid" is to define a method that calls itself recursively for each subsequent action in an array:
func runInSequence(actions:[(node:SKNode,action:SKAction)], index:Int) {
if index < actions.count {
let node = actions[index].node
let action = actions[index].action
node.runAction(action) {
// Avoid retain cycle
[weak self] in
self?.runInSequence(actions, index: index+1)
}
}
}
To use this, define/create an array that stores the nodes and actions you want to run in sequence, and call the method with the starting index:
let nodesAndActions:[(node:SKNode,action:SKAction)] = [
(mySquare1,moveAction1),
(mySquare2,moveAction2)
]
runInSequence(nodesAndActions, index: 0)

Related

spawning random enemies with an array

I am currently making a game where I need random enemies from my array, to spawn in a random location on repeat. This code seems to work okay other than the fact that it can only rotate through each Enemy once. It comes up with an error saying "Attemped to add a SKNode which already has a parent". Any help? Here is my current code:
func random() -> CGFloat {
return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}
func random(min: CGFloat, max: CGFloat) -> CGFloat {
return random() * (max - min) + min
}
func spawnEnemy() {
let EnemyArray = [Enemy1, Enemy2, Enemy3, Enemy4, Enemy5, Enemy6]
let randomElement = EnemyArray.randomElement()!
self.addChild(randomElement)
var moveEnemy = SKAction.moveTo(y: -800, duration: 4.0)
let deleteEnemy = SKAction.removeFromParent()
let EnemySequence = SKAction.sequence([moveEnemy, deleteEnemy])
randomElement.run(EnemySequence)
}
func runEnemy() {
run(SKAction.repeatForever(SKAction.sequence([SKAction.run(spawnEnemy), SKAction.wait(forDuration: 2.0)])))
}
as jnpdx suggested, you should spawn new instances of your Enemy class rather than starting with an array of them. you can introduce randomness inside the Enemy class -- for example a random start position or a random color. i would also put your movement and removeFromParent code inside the class as well. You didn't post your Enemy code, but it might look something like this
class Enemy:SKNode {
var shape:SKShapeNode?
override init() {
super.init()
//how ever you want to graphically represent your enemy... using a SKShapeNode for demo
shape = SKShapeNode(ellipseOf: CGSize(width: 20, height: 40))
shape?.fillColor = .blue
addChild(shape ?? SKNode())
//randomize starting x position
position.x = CGFloat.random(in: -200...200)
position.y = 200
move()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//move and remove this node using SKAction
func move() {
let move = SKAction.moveTo(y: -200, duration: 4.0)
let delete = SKAction.removeFromParent()
let sequence = SKAction.sequence([move, delete])
self.run(sequence)
}
}
then you would simply activate your spawn point from didMove(to view: SKView) like this
override func didMove(to view: SKView) {
runSpawnPoint()
}
func runSpawnPoint() {
run(SKAction.repeatForever(SKAction.sequence([SKAction.run(spawnEnemy), SKAction.wait(forDuration: 2.0)])))
}
func spawnEnemy() {
let enemy = Enemy() //a brand new Enemy object each time
addChild(enemy)
}
optional: save your spawned Enemy objects in an array if you want to access them later. alternately you can simply query self.children from your SKScene since they're all stored there as well. in which case you don't need an additional array for storage.
I have found an answer. So originally my problem was trying to spawn multiple, different-looking enemies, at random. I realized that I could solve the same issue by changing the texture of the Enemy, instead of creating many different Enemy Nodes. In order to spawn enemies at random with an array of textures, it would look something like this:
var enemy1 = SKTexture(imageNamed: "Enemy1")
var enemy2 = SKTexture(imageNamed: "Enemy2")
var enemy3 = SKTexture(imageNamed: "Enemy3")
var enemy4 = SKTexture(imageNamed: "Enemy4")
var enemy5 = SKTexture(imageNamed: "Enemy5")
var enemy6 = SKTexture(imageNamed: "Enemy6")
let EnemyArray = [Enemy1, Enemy2, Enemy3, Enemy4, Enemy5, Enemy6]
let randomElement = EnemyArray.randomElement()!
let enemy = SKSpriteNode(imageNamed: "")
enemy.name = "Enemy"
enemy.texture = randomElement
enemy.size = CGSize(width: 30, height: 30)
enemy.zPosition = 2
self.addChild(enemy)
var moveEnemy = SKAction.moveTo(y: -800, duration: 4.0)
let deleteEnemy = SKAction.removeFromParent()
let EnemySequence = SKAction.sequence([moveEnemy, deleteEnemy])
enemy.run(EnemySequence)
}
func runEnemy() {
run(SKAction.repeatForever(SKAction.sequence([SKAction.run(spawnEnemy), SKAction.wait(forDuration: 2.0)])))
}
Thanks everyone for the help

Is there a way I can add more than one sprite in SpriteKit globally?

I am struggling with one issue. Global declaration of my sprite so that I can interact with it. In this game, I have created a local sprite called enemy featured below:
func spawnEnemy() {
let enemy = SKSpriteNode(imageNamed: "as")
let yPosition = CGFloat(frame.maxY - enemy.size.height)
let getXvalue = GKRandomDistribution(lowestValue: Int(frame.minX + enemy.size.width), highestValue: Int(frame.maxX - enemy.size.width))
let xPosition = CGFloat(getXvalue.nextInt())
enemy.position = CGPoint(x: xPosition, y: yPosition)
enemy.name = "asteroid"
enemy.zPosition = 100
addChild(enemy)
let animationDuration:TimeInterval = 6
var actionArray = [SKAction]()
actionArray.append(SKAction.move(to: CGPoint(x: xPosition, y: 0), duration: animationDuration))
actionArray.append(SKAction.self.removeFromParent())
enemy.run(SKAction.sequence(actionArray))
}
I want to tap the enemy to make it disappear from the screen. The variable is declared locally and not globally so the touchesBegan function does not "see" enemy. However, when I move the statement:
let enemy = SKSpriteNode(imageNamed: "as")
outside of local declaration and into global. It works until the code tries to spawn in another enemy and i get an error of "Tried to add an SKNode who already has a parent" This is the code I have running in my view did load:
run(SKAction.repeatForever(SKAction.sequence([SKAction.run{self.spawnEnemy()
}, SKAction.wait(forDuration: 1.0)])))
Every time it spawns a new enemy it crashes and says that the SKNode already has a parent which i understand. However, for my game to function I need the player to be able to touch the individual instance of that enemy and remove it. Hence my code for
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
if let location = touch?.location(in:self) {
let nodesArray = self.nodes(at:location)
if nodesArray.first?.name == "asteroid" {
print("Test")
enemy.removeFromParent()
print("Test Completed")
}
}
}
Now the error says unresolved use of "enemy" because the enemy is not global. I have been going back and forth on this issue for quite some time. If anyone has any potential solution or work around I would be very grateful, and thank you for your help.
Move your enemies to their own class and handle the touch for each of those enemies in their own class. This cleans up your GameScene and keeps your code more organized. You can now add as many instances of enemy as you want.
FYI not related to this question but somethings to consider after you get this working
when game over or level change or win make sure you have a clean up function to remove all enemies
you should strongly consider recycling your objects vs creating them on the fly...better performance
try to separate as much code to your objects class as possible
class enemy: SKSpriteNode {
init() {
super.init(texture: nil, color: .clear, size: CGSize.zero)
setup()
}
func setup() {
isUserInteractionEnabled = true
name = "asteroid"
zPosition = 100
let image = SKSpriteNode(imageNamed: "as")
imagine.zPosition = 1
addChild(image)
self.size = image.size
animate()
}
func animate() {
let animationDuration: TimeInterval = 6
let move = SKAction.move(to: CGPoint(x: xPosition, y: 0), duration: animationDuration)
let remover = SKAction.self.removeFromParent()
run(SKAction.sequence(move, remover))
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
removeFromParent()
}
}
class GameScene: SKScene {
override func didMove(to view: SKView) {
let sequence = SKAction.sequence([SKAction.run{ self.spawnEnemy()
}, SKAction.wait(forDuration: 1.0)])
run(SKAction.repeatForever(sequence))
}
func spawnEnemy() {
let enemy = Enemy()
let yPosition = CGFloat(frame.maxY - enemy.size.height)
let getXvalue = GKRandomDistribution(lowestValue: Int(frame.minX + enemy.size.width), highestValue: Int(frame.maxX - enemy.size.width))
let xPosition = CGFloat(getXvalue.nextInt())
enemy.position = CGPoint(x: xPosition, y: yPosition)
addChild(enemy)
}
}

SKLabelNode Moves. But action does not repeat

I have created a Test Scene to practice some basic Swift 3 and SpriteKit. I'm trying to learn by understanding the basics before moving on to more complex goals.
Here I have a SKLabelNode that is created and then moves to the left. I have created a sequence to repeat the action but it does not work. Please could you help me understand where it fails. NodeCount notes that there is only 1 node.
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var testShape = SKLabelNode()
override func didMove(to view: SKView) {
func createShape() {
testShape = SKLabelNode(text: "TEST")
testShape.position = CGPoint(x: 0.5, y: 0.5)
testShape.zPosition = 1
addChild(testShape)
}
let moveTestShape = SKAction.moveBy(x: -500, y: 0, duration: 5)
func repeater() {
createShape()
testShape.run(moveTestShape)
}
let delay = SKAction.wait(forDuration: 2)
let repeatingAction = SKAction( repeater() )
let sequence = SKAction.sequence([ delay, repeatingAction ] )
run(SKAction.repeatForever(sequence))
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
}
override func update(_ currentTime: TimeInterval) {
}
}
Are you not getting compiler errors?
Why are you creating methods in didMoveToView?
Your code should look more like this
class GameScene: SKScene {
var testShape = SKLabelNode()
override func didMove(to view: SKView) {
let delay = SKAction.wait(forDuration: 2)
let repeatingAction = SKAction.run(repeater)
let sequence = SKAction.sequence([ delay, repeatingAction ] )
run(SKAction.repeatForever(sequence))
}
func createShape() {
testShape = SKLabelNode(text: "TEST")
testShape.position = CGPoint(x: 0.5, y: 0.5)
testShape.zPosition = 1
addChild(testShape)
}
func repeater() {
createShape()
let moveTestShape = SKAction.moveBy(x: -500, y: 0, duration: 5)
testShape.run(moveTestShape)
}
}
This is how you call functions/code blocks in SKActions.
let repeatingAction = SKAction.run(repeater)
or
let repeatingAction = SKAction.run {
repeater()
}
Also remember our are only repeating the spawn action for new labels. The actual action to move the labels is non repeating. So what you should see is 1 label created and moved once, than 2 seconds later a new label gets created and moved once etc
Hope this helps

How do you trigger actions with NSTimer in Spritekit?

I'm trying to get my "Player" (A circle in the middle) to increase in size once the screen is touched by running a timer.
Once the timer is over 0 seconds, it increases in size. Once the timer is over 3 seconds, it decreases to its original scale size and once the timer is over 7 seconds, it resets and this repeats forever.
What am I doing wrong?
import SpriteKit
class GameScene: SKScene {
var Center = SKSpriteNode()
var Player = SKSpriteNode()
var timer = NSTimer()
var seconds = 0
override func didMoveToView(view: SKView) {
Center = SKSpriteNode(imageNamed: "Center")
Center.size = CGSize(width: 80, height: 80)
Center.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
self.addChild(Center)
Player = SKSpriteNode(imageNamed: "Player")
Player.size = CGSize(width: 80, height: 80)
Player.position = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
self.addChild(Player)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
timer = NSTimer.scheduledTimerWithTimeInterval(4.0, target: self, selector: #selector(GameScene.playerScaleUp), userInfo: nil, repeats: true)
}
func playerScaleUp(){
if seconds > 0{
Player.runAction(SKAction.scaleBy(4, duration: 2))
}
}
func playerScaleDown(){
if seconds > 3{
Player.runAction(SKAction.scaleBy(-4, duration: 2))
}
}
func resetScale(){
if seconds > 7{
timer.invalidate()
}
}
override func update(currentTime: CFTimeInterval) {
}
}
This can be done in a few different ways:
1) Using SKActions (which my example uses)
2) Using update: method and its passed currentTime parameter
3) Using NSTimer which I wouldn't recommend because it is not affected by scene's, view's or node's paused state, so it can lead you into troubles in the future. Read more in this StackOverflow answer
4) Well, probably some more, but I will stop here.
My example uses SKAction. This means that I don't really check how much time is passed, but rather I organize actions into sequences (where actions are executed sequentially) and into groups (where actions are organized parallel). Means I use SKActions like I am playing with LEGO's :)
Here is the code ...I left debugging code intentionally, because it can help you to learn how you can use SKActions in different situations.
class GameScene: SKScene, SKPhysicsContactDelegate {
let player = SKSpriteNode(color: .blackColor(), size: CGSize(width: 50, height: 50))
var timePassed = 0.0 {
didSet {
self.label.text = String(format: "%.1f",timePassed)
}
}
let label = SKLabelNode(fontNamed: "ArialMT")
override func didMoveToView(view: SKView) {
/* Debugging - Not really needed, I added it just because of a better example */
let wait = SKAction.waitForDuration(0.1)
let updateLabel = SKAction.runBlock({[unowned self] in self.timePassed += wait.duration})
self.label.position = CGPoint(x: frame.midX, y: frame.midY+100.0)
addChild(self.label)
let timerSequence = SKAction.sequence([updateLabel, wait])
self.runAction(SKAction.repeatActionForever(timerSequence), withKey: "counting")
//This is used later, at the end of scaleUpAndDownSequence to reset the timer
let resetTimer = SKAction.runBlock({[unowned self] in self.timePassed = 0.0})
/* End Debugging */
self.player.position = CGPoint(x: frame.midX, y: frame.midY)
addChild(self.player)
let waitBeforeScaleToOriginalSize = SKAction.waitForDuration(3.0)
let waitBeforeRestart = SKAction.waitForDuration(4.0)
let scaleUp = SKAction.scaleTo(2.0, duration: 1)
let scaleDown = SKAction.scaleTo(1.0, duration: 1)
let scaleUpGroup = SKAction.group([waitBeforeScaleToOriginalSize, scaleUp])
let scaleDownGroup = SKAction.group([scaleDown, waitBeforeRestart])
//One cycle, scale up, scale down, reset timer
let scaleUpAndDownSequence = SKAction.sequence([scaleUpGroup, scaleDownGroup, resetTimer])
let loop = SKAction.repeatActionForever(scaleUpAndDownSequence)
self.player.runAction(loop, withKey: "scalingUpAndDown")
}
}
So here, I have two groups of actions:
1) scaleUpGroup
2) scaleDownGroup
scaleUpGroup group of actions has two actions in it: a scaleUp action, and an action which says how much to wait before the scale down action should occur. Because we want scaling up to happen immediately, we run it in parallel with the waitBeforeScaleToOriginalSize.
Same logic goes for scaleDownGroup. When scaleUpGroup is finished (its duration is determined by the longest action in the group) we start scaleDownGroup which scales down the player to its default size, and waits certain amount of time to repeat the whole thing.
Here is the result:
I start an animation on touch ( I've removed that code) and as you can see, scale up animation starts immediately, then after 3 seconds the player get scaled down to its original size, and after 4 seconds the whole animation repeats (player gets scaled up again etc).

Why do I keep getting this error in Swift? 'Attemped to add a SKNode which already has a parent'

I'm trying to get this rectangle to repeat but I keep getting this error message saying this. I tried everything but I cant seem to figure it out. Can someone please help Its so frustrating I cant get pass this part. I ve been stuck on this error for a couple of days now. If you need more info please message me.
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: Attemped to add a SKNode which already has a parent: >name:'(null)' texture:[ 'longrec'
class GameScene: SKScene, SKPhysicsContactDelegate {
let longRec = SKSpriteNode(imageNamed: "longrec")
override func didMoveToView(view: SKView) {
repeatlongRec()
}
func longRec() {
let moveToTheLeft = SKAction.moveByX(-self.size.width, y: 0, duration: 2.0)
let repeatMoveToLeft = SKAction.repeatActionForever(moveToTheLeft)
let removeFromScene = SKAction.removeFromParent()
let sequence = SKAction.sequence([repeatMoveToLeft, removeFromScene])
longRec.position = CGPointMake(self.size.width / 0.5, self.size.height / 2)
longRec.zPosition = 15
longRec.runAction(sequence)
addChild(longRec)
}
func repeatlongRec() {
let generateLongRec = SKAction.sequence([
SKAction.runBlock(self.longRec),
SKAction.waitForDuration(2.0)])
let endlessAction = SKAction.repeatAction(generateLongRec, count: 4)
runAction(endlessAction)
}
}
Try the following code:
import SpriteKit
class GameScene: SKScene, SKPhysicsContactDelegate {
let longRec = SKSpriteNode(imageNamed: "rectangle.png")
override func didMoveToView(view: SKView) {
/* Setup your scene here */
let makeLongRec = SKAction.sequence([SKAction.runBlock(generateLongRec), SKAction.waitForDuration(3)])
runAction(SKAction.repeatAction(makeLongRec, count: 4))
}
func generateLongRec() {
longRec.position = CGPointMake(self.size.width / 2, self.size.height / 2)
longRec.zPosition = 15
self.addChild(longRec)
let moveToTheLeft = SKAction.moveByX(-self.size.width, y: 0, duration: 2.0)
let removeFromScene = SKAction.removeFromParent()
longRec.runAction(SKAction.sequence([moveToTheLeft, removeFromScene]))
}
The issue with your code is that, you are adding it too fast. Like #Ace Green said, you are adding the node before you even remove it.
Two other things you should be aware of:
try not to use the same name for both func and your node.
if you are planning to use only one node and repeat adding it. The wait time in waitforDuration(2) in didMoveToView should always be larger than the duration in let moveToTheLeft = SKAction.moveByX(-self.size.width, y: 0, duration: 2.0), otherwise you will still have the same Attemped to add a SKNode which already has a parent issue. One way to resolve this is to declare an array of node.
After you do addChild(longRec) you never remove it
So when you execute func repeatlongRec() it crashes because the node is still there.
try to restructure both functions so that you remove the node whenever you don't are done with it then repeat the process
I would try doing it this way:
class GameScene: SKScene, SKPhysicsContactDelegate {
let longRec = SKSpriteNode(imageNamed: "longrec")
override func didMoveToView(view: SKView) {
var timer = NSTimer .scheduledTimerWithTimeInterval(2, target: self, selector: Selector("longRec") , userInfo: nil, repeats: true)
}
func longRec() {
let moveToTheLeft = SKAction.moveByX(-self.size.width, y: 0, duration: 2.0)
let removeFromScene = SKAction.removeFromParent()
let sequence = SKAction.sequence([moveToTheLeft, removeFromScene])
let repeatSequence = SKAction.repeatActionForever(sequence)
longRec.position = CGPointMake(self.size.width / 0.5, self.size.height / 2)
longRec.zPosition = 15
longRec.runAction(repeatSequence)
addChild(longRec)
}
}