How to create a custom SKAction in swift - swift

The idea is that I am creating blocks that are falling out of the sky.
To do this I need an custom action that does four things:
Create an node with my block class
Set the position of that node
add the node to the scene
after an delay go to point one
I am wondering if you actually can create a SKAction.customActionWithDuration to do this things.
Thanks in advance

The following method creates an SKAction that should fit your needs.
func buildAction() -> SKAction {
return SKAction.runBlock {
// 1. Create a node: replace this line to use your Block class
let node = SKShapeNode(circleOfRadius: 100)
// 2. Set the position of that node
node.position = CGPoint(x: 500, y: 300)
// 3. add the node to the scene
self.addChild(node)
// 4. after a delay go to point one
let wait = SKAction.waitForDuration(3)
let move = SKAction.moveTo(CGPoint(x: 500, y: 0), duration: 1)
let sequence = SKAction.sequence([wait, move])
node.runAction(sequence)
}
}

Thanks to #appsYourLife.
I've made a couple of changes below:
I've adjusted for swift 3
I've added a parameter called parent so you can either use buildAction(parent: self) or if you want to attach the node to some otherNode you can use buildAction(parent: otherNode)
func buildAction(parent: SKNode) -> SKAction {
return SKAction.run {
// 1. Create a node: replace this line to use your Block class
let node = SKShapeNode(circleOfRadius: 100)
// 2. Set the position of that node
node.position = CGPoint(x: 500, y: 300)
// 3. add the node to the scene
parent.addChild(node)
// 4. after a delay go to point one
let wait = SKAction.wait(forDuration: 3)
let move = SKAction.move(to: CGPoint(x: 500, y: 0), duration: 1)
let sequence = SKAction.sequence([wait, move])
node.run(sequence)
}
}

Related

ARkit Scene Kit Animations

I'm building an app with ARkit and I'd like the user to have the ability to start and stop animations in the scene with the use of a button in the viewcontroller. However, I am having trouble finding an example of this code. Any pointers would be appreciated. For reference, my code is as follows for animating the dae file. I had a version working where I could stop the animation, but could not get it to restart. Thanks in advance.
func loadModels(atom: String) {
// Create a new scene
let scene = SCNScene(named: "art.scnassets/" + atom + ".scn")!
// Set the scene to the view
sceneViewAr?.scene = scene
let mainnode = scene.rootNode.childNode(withName: "mainnode", recursively: true)
mainnode?.runAction(SCNAction.rotateBy(x: 10, y: 0, z: 0, duration: 0))
let orbit = scene.rootNode.childNode(withName: "orbit", recursively: true)
orbit?.runAction(SCNAction.rotateBy(x: 0, y: 2 * 50, z: 0, duration: 100))
if let orbittwo = scene.rootNode.childNode(withName: "orbit2", recursively: true) {
orbittwo.runAction(SCNAction.rotateBy(x: 0, y: -2 * 50, z: 0, duration: 100))
}
if let orbitthree = scene.rootNode.childNode(withName: "orbit3", recursively: true) {
orbitthree.runAction(SCNAction.rotateBy(x: 0, y: 2 * 50, z: 0, duration: 100))
}
}
I dont think there is a way to pause and stop in the way that you want per se.
Having said this you can make use of the following SCNAction functions:
func action(forKey key: String) -> SCNAction?
func removeAction(forKey key: String)
Essentially you are going to have to stop and then recreate the actions again.
As such you could do something like so:
Create a variable under your class declaration for your SCNActions so you dont have to rewrite them every time e.g:
let rotateXAction = SCNAction.rotateBy(x: 10, y: 0, z: 0, duration: 10)
Then Create 2 functions one to add and the other to remove the actions e.g:
/// Adds An SCNAction To A Specified Node With A Key
///
/// - Parameters:
/// - action: SCNAction
/// - node: SCNNode
/// - key: String
func addAction(_ action: SCNAction, toNode node: SCNNode, forKey key: String){
node.runAction(action, forKey: key)
}
/// Removes An SCNAction For A Specified Node With A Key
///
/// - Parameters:
/// - action: SCNAction
/// - node: SCNNode,
/// - key: String
func removeActionFromNode(_ node: SCNNode, forKey key: String){
node.removeAction(forKey: key)
}
Which you could then test like so:
/// Tests Whether The `SCNActions` Can Be Stated & Stopped
func testActions(){
//1. Create An Array Of Colours
let colours: [UIColor] = [.red, .green, .blue, .orange, .purple, .black]
//2. Create An Array Of Face Materials
var faceArray = [SCNMaterial]()
//3. Create An SCNNode With An SCNBox Geometry
let firstNode = SCNNode(geometry: SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0))
for faceIndex in 0..<colours.count{
let material = SCNMaterial()
material.diffuse.contents = colours[faceIndex]
faceArray.append(material)
}
firstNode.geometry?.materials = faceArray
//4. Add It To The Scene
self.augmentedRealityView.scene.rootNode.addChildNode(firstNode)
firstNode.position = SCNVector3(0 , 0, -1)
//5. Run The Action
addAction(rotateXAction, toNode: firstNode, forKey: "rotateX")
//6. Stop It After 4 Seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
self.removeActionFromNode(firstNode, forKey: "rotateX")
}
//7. Start It Again After 8
DispatchQueue.main.asyncAfter(deadline: .now() + 8) {
self.addAction(self.rotateXAction, toNode: firstNode, forKey: "rotateX")
}
}
In your context you can easily tweak these functions to apply for a specific IBAction etc.
This is just a starter and in no way a refactored or refined example, however it should point you in the right direction...

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).

Randomizing node movement duration

I am making a game in SpriteKit and I have a node that is moving back and forth across the screen and repeating using the code:
let moveRight = SKAction.moveByX(frame.size.width/2.8, y: 0, duration: 1.5)
let moveLeft = SKAction.moveByX(-frame.size.width/2.8, y: 0, duration: 1.5)
let texRight = SKAction.setTexture(SKTexture(imageNamed: "Drake2"))
let texLeft = SKAction.setTexture(SKTexture(imageNamed: "Drake1"))
let moveBackAndForth = SKAction.repeatActionForever(SKAction.sequence([texRight, moveRight, texLeft, moveLeft,]))
Drake1.runAction(moveBackAndForth)
I am trying to figure out what method I can use to randomize the duration. Every time moveBackandForth runs, I want it to rerun using a different duration, (within games, not between). If someone could give me some example code to try I would really appreciate it.
Also arc4Random works fine, but it doesn't randomize within the game, only between games.
When you run actions like from your example and randomize duration parameter with something like arc4Random this is actually happening:
Random duration is set and stored in action.
Then action is reused in a sequence with a given duration.
Because the action is reused as it is, duration parameter remains the same over time and moving speed is not randomized.
One way to solve this (which I prefer personally) would be to create a "recursive action", or it is better to say, to create a method to run desired sequence and to call it recursively like this :
import SpriteKit
class GameScene: SKScene {
let shape = SKSpriteNode(color: UIColor.redColor(), size: CGSize(width: 20, height: 20))
override func didMoveToView(view: SKView) {
shape.position = CGPointMake(CGRectGetMidX(self.frame) , CGRectGetMidY(self.frame)+60 )
self.addChild(shape)
move()
}
func randomNumber() ->UInt32{
var time = arc4random_uniform(3) + 1
println(time)
return time
}
func move(){
let recursive = SKAction.sequence([
SKAction.moveByX(frame.size.width/2.8, y: 0, duration: NSTimeInterval(randomNumber())),
SKAction.moveByX(-frame.size.width/2.8, y: 0, duration: NSTimeInterval(randomNumber())),
SKAction.runBlock({self.move()})])
shape.runAction(recursive, withKey: "move")
}
}
To stop the action, you remove its key ("move").
I don't have any project where I can try right now.
But you might want to try this :
let action = [SKAction runBlock:^{
double randTime = 1.5; // do your arc4random here instead of fixed value
let moveRight = SKAction.moveByX(frame.size.width/2.8, y: 0, duration: randTime)
let moveLeft = SKAction.moveByX(-frame.size.width/2.8, y: 0, duration: randTime)
let texRight = SKAction.setTexture(SKTexture(imageNamed: "Drake2"))
let texLeft = SKAction.setTexture(SKTexture(imageNamed: "Drake1"))
let sequence = SKAction.sequence([texRight, moveRight, texLeft, moveLeft])
Drake1.runAction(sequence)
}]; 
let repeatAction = SKAction.repeatActionForever(action)
Drake1.runAction(repeatAction)
Let me know if it helped.

How to make a spritekit node change from one image to another

I have an SKSpriteNode image with the code:
let Drake1 = SKSpriteNode(imageNamed: "Drake1")
Drake1.position = CGPoint(x:self.frame.size.width/3, y:self.frame.size.height - 230)
Drake1.zPosition = 2
addChild(Drake1)
//drake movement
let moveRight = SKAction.moveByX(frame.size.width/2.8, y: 0, duration: 2)
let moveLeft = SKAction.moveByX(-frame.size.width/2.8, y: 0, duration: 2)
let moveBackAndForth = SKAction.repeatActionForever(SKAction.sequence([moveRight, moveLeft]))
Drake1.runAction(moveBackAndForth)
What I want to do is, when the image is moving to the right, I want to replace the image with a different SKSpriteNode image, and when it moves back left, I want to use the original image, and repeat this forever. I am struggling with what the code should be for this.
SpriteKit comes with a SKAction, setTexture, to instantaneously change a sprite's texture with relative ease. You can create an inline SKTexture object of each images, use them in SKActions, and add them to your sequence loop, like this:
let moveRight = SKAction.moveByX(frame.size.width/2.8, y: 0, duration: 2)
let moveLeft = SKAction.moveByX(-frame.size.width/2.8, y: 0, duration: 2)
let texRight = SKAction.setTexture(SKTexture(imageNamed: "Drake1r"))
let texLeft = SKAction.setTexture(SKTexture(imageNamed: "Drake1l"))
let moveBackAndForth = SKAction.repeatActionForever(SKAction.sequence([texRight, moveRight, texLeft, moveLeft]))
Drake1.runAction(moveBackAndForth)
Hopefully this works for you! Please note that if the textures are different sizes, you must add resize: Bool to setTexture's arguments.

Wait until function is completed

I have to function for adding two sprites and moving, called Space1 and Space2. I need to make Space1 move Right to Left, after Space 1 is finished the moving. Then, Space2 will move Left to Right.
I can make them move by 2 functions Move1() and Move2(); but they started the same time.
How can I make the Space2 wait until the function Move1 of Space1 is completed and then Space2 start moving?
This is my code
// Create sprite
let space1 = SKSpriteNode(imageNamed: "Spaceship")
space1.position = CGPoint(x: 0, y: frame.height/2)
space1.xScale = 0.3;
space1.yScale = 0.3;
space1.name = "space1";
self.addChild(space1);
let space2 = SKSpriteNode(imageNamed: "Spaceship")
space2.position = CGPoint(x: frame.width, y:0)
space2.xScale = 0.3;
space2.yScale = 0.3;
space2.name = "space2";
self.addChild(space2);
let actualDuration = random(min: CGFloat(1), max: CGFloat(3))
// Create the actions
let actionMove1 = SKAction.moveTo(CGPoint(x: frame.width, y: frame.height/2), duration: NSTimeInterval(actualDuration))
let actionMove2 = SKAction.moveTo(CGPoint(x: 0, y: 0), duration: NSTimeInterval(actualDuration))
self.runAction(SKAction.sequence([SKAction.runAction(actionMove1, onChildWithName: "space1"),SKAction.runAction(actionMove2, onChildWithName: "space2")]))
You can use the completion handler of runAction function to get what you need. Start the next SKAction inside the completion handler of the first runAction function.
space1.runAction(actionMove1, completion: { () -> Void in
space2.runAction(actionMove2)
})
SpriteKit provides something called SKAction.sequence. You can give it an array of actions which will be started after the one before has finished:
func startAction(){
var space1 = SKSpriteNode()
var space2 = SKSpriteNode()
var move1 = SKAction()
var move2 = SKAction()
space1.name = "space1"
space2.name = "space2"
self.runAction(SKAction.sequence([SKAction.runAction(move1, onChildWithName: "space1"), SKAction.runAction(move2, onChildWithName: "space2")]))
}
As you see here, I add names to the SKSpriteNodes. That's important because as you can see in the sequence, you have to specify, on which node the action is getting called.
Also important to say is, that if you use sub-sequences, you need to work with SKAction.waitForDuration(sec: NSTimeInterval). For example:
self.runAction(SKAction.sequence([SKAction.runAction(move1, onChildWithName: "space1"), SKAction.waitForDuration(move1.duration), SKAction.runAction(move2, onChildWithName: "space2")]))}