Randomizing node movement duration - swift

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.

Related

My SKSpriteNode speed values are not updating properly

What I'm trying to do is update my SKSpriteNodes so I can change their scrolling speeds dynamically, however they aren't really working consistently. I didn't include the code, but I have another method with a switch case that sets the value of platformSpeed whenever the state is changed (in this case, the switch case is changed with UIButtons). In my code I have an SKSpriteNode array and a platformSpeed property that includes didSet so my value is updated properly.
In my method to create the platforms, I grouped my SpriteNodes into platformGroup then looped through them with addChild(). Not sure why it's acting this way but here's a quick video of what it looks like in action:
demonstration clip
So with the buttons I'm changing the switch case, and as you can see, not all of the nodes speeds are updating properly and some get faster than others and eventually pass them. I need them to stay equal distance between each other.
Now here's my code:
class GameScene: SKScene, SKPhysicsContactDelegate {
var platformGroup = [SKSpriteNode]()
var platformSpeed: CGFloat = 1.0 {
didSet {
for platforms in platformGroup {
platforms.speed = platformSpeed
}
}
}
let platformTexture = SKTexture(imageNamed: "platform")
var platformPhysics: SKPhysicsBody!
func createPlatforms() {
let platformLeft = SKSpriteNode(texture: platformTexture)
platformLeft.physicsBody = platformPhysics.copy() as? SKPhysicsBody
platformLeft.physicsBody?.isDynamic = false
platformLeft.scale(to: CGSize(width: platformLeft.size.width * 4, height: platformLeft.size.height * 4))
platformLeft.zPosition = 20
let platformRight = SKSpriteNode(texture: platformTexture)
platformRight.physicsBody = platformPhysics.copy() as? SKPhysicsBody
platformRight.physicsBody?.isDynamic = false
platformRight.scale(to: CGSize(width: platformRight.size.width * 4, height: platformRight.size.height * 4))
platformRight.zPosition = 20
let scoreNode = SKSpriteNode(color: UIColor.clear, size: CGSize(width: frame.width, height: 32))
scoreNode.physicsBody = SKPhysicsBody(rectangleOf: scoreNode.size)
scoreNode.physicsBody?.isDynamic = false
scoreNode.name = "scoreDetect"
scoreNode.zPosition = 40
platformGroup = [platformLeft, platformRight, scoreNode]
let yPosition = frame.width - platformRight.frame.width
let max = CGFloat(frame.width / 4)
let xPosition = CGFloat.random(in: -80...max)
let gapSize: CGFloat = -50
platformLeft.position = CGPoint(x: xPosition + platformLeft.size.width - gapSize, y: -yPosition)
platformRight.position = CGPoint(x: xPosition + gapSize, y: -yPosition)
scoreNode.position = CGPoint(x: frame.midX, y: yPosition - (scoreNode.size.width / 1.5))
let endPosition = frame.maxY + (platformLeft.frame.height * 3)
let moveAction = SKAction.moveBy(x: 0, y: endPosition, duration: 7)
let moveSequence = SKAction.sequence([moveAction, SKAction.removeFromParent()])
for platforms in platformGroup {
addChild(platforms)
platforms.run(moveSequence)
}
platformCount += 1
}
func loopPlatforms() {
let create = SKAction.run { [unowned self] in
self.createPlatforms()
platformCount += 1
}
let wait = SKAction.wait(forDuration: 1.1)
let sequence = SKAction.sequence([create, wait])
let repeatForever = SKAction.repeatForever(sequence)
run(repeatForever)
}
I think I can see what's going wrong. When you change platformSpeed, it changes the speed of all the platforms in platformGroup. And createPlatforms() is being called multiple times. Now, each time it's called you create a pair of platforms and assign these to platformGroup. Since you call the function multiple times, it's overwriting any existing values in the array. That's why changing platformSpeed only updates the speed of the latest platforms you've created---the older platforms stay the same speed because they're not in platformGroup anymore.
To fix this, my advice would be to have platformGroup store all the platforms currently on the screen. You could do this by changing
platformGroup = [platformLeft, platformRight, scoreNode]
to something like
let newNodes = [platformLeft, platformRight, scoreNode]
platformGroup += newNodes
// Alternatively, platformGroup.append(contentsOf: newNodes)
Now you need to make sure you're 1) only adding the new nodes to the scene, and 2) removing the old nodes from platformGroup when they're removed from the parent. You could do this by changing
let moveAction = SKAction.moveBy(x: 0, y: endPosition, duration: 7)
let moveSequence = SKAction.sequence([moveAction, SKAction.removeFromParent()])
for platforms in platformGroup {
addChild(platforms)
platforms.run(moveSequence)
}
to something like
let moveAction = SKAction.moveBy(x: 0, y: endPosition, duration: 7)
for node in newNodes {
let moveSequence = SKAction.sequence([
moveAction,
SKAction.removeFromParent(),
SKAction.run {
self.platformGroup.remove(node)
}
])
addChild(node)
node.run(moveSequence)
}
Now that you're keeping a track of all platforms ever made, your speed changes should be applied consistently to every platform on the screen. Hope this works!

How to create a custom SKAction in 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)
}
}

How to add score to SpriteKit using SKAction

runAction(SKAction.repeatActionForever(
SKAction.sequence([
SKAction.runBlock(addScore),
SKAction.waitForDuration(1.0) ]) ))
func addScore() {
let scoreSprite = SKSpriteNode(imageNamed: "scoreSprite")
let actualY = random1(min: scoreSprite.size.height/2, max: size.height - scoreSprite.size.height/2)
scoreSprite.position = CGPoint(x: size.width + scoreSprite.size.width/2, y: actualY)
self.addChild(scoreSprite)
let actualDuration = random1(min: CGFloat(0.5), max: CGFloat(0.5))
let actionMove = SKAction.moveTo(CGPoint(x: -scoreSprite.size.width/2, y: actualY), duration: NSTimeInterval(actualDuration))
let actionMoveDone = SKAction.removeFromParent()
scoreSprite.runAction(SKAction.sequence([actionMove, actionMoveDone])) }
Okay, so I want my score to increase by 1 for every second the person plays the game. I looked into using NSTimeInterval but from what I could gather, the better option is to use SKAction. The only way I could think of was to use an almost clear sprite and to move it across the screen every second. What I want to know is how to make a score increase each time the node moves across the screen, or if this is no good what a better option would be. The code I am using to move the node is above. Thanks in advance.
Try adding this to your didMoveToView method
let wait = SKAction.waitForDuration(1.0)
let incrementScore = SKAction.runBlock ({
++self.score
self.scoreLabel.text = "\(self.score)"
})
self.runAction(SKAction.repeatActionForever(SKAction.sequence([wait,incrementScore])))
I fixed it using by adding this to the end of the scoreSprite function:
if scoreSprite.position.x > endOfScreenLeft {
updateScore()
}
Then did this to update the score:
func updateScore() {
score++
scoreLabel.text = String(score)
}
End of screen left is a CGFloat:
endOfScreenLeft = (self.size.width / 2) * CGFloat(-1)

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")]))}