I am trying to repeat a set of actions by running them through a loop. My loop is fine when running something else, but it seems to have trouble running the actions. My code is as follows:
let pulse1 = SKAction.scaleTo(2.0, duration: 1.0)
let pulse2 = SKAction.scaleTo(0.5, duration: 1.0)
var i = 0
override func didMoveToView(view: SKView) {
for var i = 0; i <= 100; i++ {
self.sun.runAction(pulse1)
self.sun.runAction(pulse2)
}
This will cause the node to pulse1 and pulse2 each once but never again. If I add
println("")
to the loop, it runs whatever text properly, but for some reason doesn't run the actions like it runs the text. Or maybe it does and I don't understand how SKAction works? Either way, the loop is executing properly, I believe. I am not quite sure what's wrong with the SKAction call in the loop.
scaleTo simply changes the node's scale. Once pulse1 goes to 2.0 and pulse2 gets to 0.5, runAction runs repeatedly but you never change the scale for either pulse ever again.
That's why you're only seeing it do work the first time.
Instead of using a for loop, try something like this:
override func didMoveToView(view: SKView) {
if (i % 2 == 0) {
let pulse = SKAction.scaleTo(2.0, duration: 1.0)
} else {
let pulse = SKAction.scaleTo(0.5, duration: 1.0)
}
[self.sun runAction:pulse completion:^{
if( i < 100 )
{
didMoveToView(view);
}
}];
}
Maybe you can use
class func repeatAction(_ action: SKAction,
count count: Int) -> SKAction
Put as many single actions in a sequence an run repeatAction for x times.
Related
Title says it all. I've got two SKSpriteNodes [ leftTrap, rightTrap] with two separate SKActions [ rotateSequenceLeft, rotateSequence] that need to run at the same time but need to do it randomly.
SKSpriteNodes with attached SKActions
Need to run these two in parallel at same random intervals.
leftTrap.run(SKAction.repeatForever(rotateSequenceLeft))
rightTrap.run(SKAction.repeatForever(rotateSequence))
What I have tried
I have tried to group the leftTrap node and action rotateSequenceLeft with a wait duration range action. But it seems that the created group never even runs the wait duration action.
let randomPivotInterval = SKAction.wait(forDuration: 1.0, withRange: 5.0)
let leftGroup = SKAction.group([rotateSequenceLeft, randomPivotInterval])
let rightGroup = SKAction.group([rotateSequence, randomPivotInterval])
leftTrap.run(SKAction.repeatForever(leftGroup))
rightTrap.run(SKAction.repeatForever(rightGroup))
If both flippers need to be in sync at all times, just apply the action to the left flipper, and do this for the right:
func didEvaluateActions(){
rightTrap.angle = -leftTrap.angle
}
This will ensure both flippers are always at the opposite angle
You can try something like this
class GameScene: SKScene {
var leftTrap: SKSpriteNode! // TODO: populate this
var rightTrap: SKSpriteNode! // TODO: populate this
func start() {
let rotateTraps = SKAction.run { [weak self] in
guard let self = self else { return }
self.rotate(trap: self.leftTrap, clockwise: .random())
self.rotate(trap: self.rightTrap, clockwise: .random())
}
let wait = SKAction.wait(forDuration: 5)
let sequence = SKAction.sequence([rotateTraps, wait])
run(.repeatForever(sequence))
}
private func rotate(trap: SKSpriteNode, clockwise: Bool) {
// TODO: put rotation code here
}
}
How does it work?
The start() method creates a rotateTraps action which, each time it is executed, call self?.rotate on the left trap passing a random bool for the clockwise param and then does the same for the right trap.
Then the rotateTraps action is wrapped into a sequence and repeated forever every 5 seconds.
If for whatever reason you don't want to add a post-evaluate phase like in Knight0fDragon's answer, then you can probably do something like this:
func operateTrap() {
run(.wait(forDuration: .random(in: 1.0 ... 6.0)) { // On completion...
// I assume both sequences take the same time.
leftTrap.run(rotateSequenceLeft)
rightTrap.run(rotateSequenceRight) { self.operateTrap() }
}
}
I try to run some actions on array of sprites, and need to run this actions in sequence for the sprites, the problem is when I use for loop its apply the action at once and there is not a period of time between them.
func setColors() {
for color in boyColors {
color.run(.wait(forDuration: 0.3))
color.run(.fadeIn(withDuration: 0.5))
}
}
When call this function its should fade in the colors
of boyColors array with some time period between them, but its fade in all the colors ate once
If you want all 4 to fade in concurrently after a delay:
func setColors() {
let seq = [SKAction.wait(forDuration: 0.3),SKAction.fadeIn(withDuration: 0.5)]
for color in boyColors {
color.run(SKAction.sequence(seq))
}
}
If you need them to fade in sequentially:
func setColors() {
var seq = [SKAction]()
let wait3 = SKAction.wait(forDuration: 0.3)
let wait5 = SKAction.wait(forDuration: 0.5)
let fadeIn = SKAction.fadeIn(withDuration: 0.5)
for color in boyColors {
let colorFadeIn = SKAction.run({color?.run(fadeIn)})
let group = [wait5,colorFadeIn]
seq.append(wait3)
seq.append(SKAction.group(group))
}
scene.run(SKAction.sequence(seq))
}
What this does is allow your scene (or any node you want to fire the actions) to control when a fade event starts. This assumes that all nodes are running at the same speed. If you need the nodes to run at individual speeds, you are going to need something a lot more complex like SomaMen proposes that chains your next colors action to the end of your current color. You will have to also check if color exists in this regards, because if you do a chain like RGBY, and you remove G, the only color that will fire is R.
There is a variety of SKActions available to you, including a run with completion, which you could use. One possible approach could be -
var colors = boyColors // so we can remove each item as we've faded it in
var waitAndFadeIn: (() -> ())?
waitAndFadeIn = {
guard let boyColor = colors.first else { return } // stops when we run out of items
let wait: SKAction = .wait(forDuration: 0.3)
let fadeIn: SKAction = .fadeIn(withDuration: 0.5)
let sequence: SKAction = .sequence([wait, fadeIn])
boyColor.run(sequence, completion: {
colors.remove(at: 0)
waitAndFadeIn?() // do it again with the next boyColor
})
}
waitAndFadeIn?()
This would fade in the objects, one after the other. I think the code will run, but it's back-of-the-envelope sort of stuff which I haven't been able to test.
I'm animating a clock hand that takes a CGFloat value from 0 to 1. While I have the animation, I would like it to be a lot smoother. The total animation takes 5 seconds, as part of an input variable. How can I make this a lot smoother?
Ideally, I'd like to get all the values from 0 to 1 in 5 seconds...
The clock hand does a complete 360 but is a little choppy
#IBAction func start(_ sender: Any) {
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(launchTimer), userInfo: nil, repeats: true)
launchTimer()
}
func launchTimer() {
guard seconds < 4.9 else {
timer.invalidate()
seconds = 0
return
}
seconds += 0.1
clockView.currentPressure = CGFloat(seconds / 5)
clockView.setNeedsDisplay()
}
EDIT
import UIKit
class GaugeView: UIView {
var currentPressure : CGFloat = 0.0
override func draw(_ rect: CGRect) {
StyleKitName.drawGauge(pressure: currentPressure)
}
}
Timer is not appropriate for animations on this scale. 100ms isn't a good step in any case, since it's not a multiple of the frame rate (16.67ms). Generally speaking, you shouldn't try to hand-animate unless you have a specialized problem. See UIView.animate(withDuration:...), which is generally how you should animate UI elements, allowing the system to take care of the progress for you.
For a slightly more manual animation, see CABasicAnimation, which will update a property over time. If you need very manual control, see CADisplayLink, but you almost never need this.
In any case, you must never assume that any timer is called precisely when you ask it to be. You cannot add 0.1s to a value just because you asked to be called in 0.1s. You have to look at what time it really is. Even hard-real-time systems can't promise something will be called at a precise moment. The best you can possibly get is a promise it will be within some tolerance (and iOS doesn't even give you that).
To animate this with UIView (which I recommend), it'll probably be something like:
#IBAction func start(_ sender: Any) {
self.clockView.currentPressure = 0
UIView.animate(withDuration: 5, animations: {
self.clockView.currentPressure = 1
})
}
With a CABasicAnimation (which is more complicated) it would be something like:
currentPressure = 1 // You have to set it to where it's going or it'll snap back.
let anim = CABasicAnimation(keyPath: "currentPressure")
anim.fromValue = 0
anim.toValue = 1
anim.duration = 5
clockView.addAnimation(anim)
Make the time interval smaller to make the animation smoother. That way it will seem like it's gliding around instead of jumping between values.
You can also use spritekit:
import SpriteKit
let wait = SKAction.wait(forDuration: 0.01)
let runAnim = SKAction.run {
launchTimer()
}
let n = SKNode()
n.run(SKAction.repeat(SKAction.sequence([wait, runAnim]), count: 500))
I'm adding a SKErmitterNode from the update() function. I want to remove the SKErmitterNode after 2 seconds, therefore I made a sequence but in the sequence, I can´t add Nodes. And if I add the Node outside of the sequence it gets added over and over again(because I´m doing all of this in the update function) Does someone know a better way to do this?
Here is my Code from the update function:
override func update(_ currentTime: CFTimeInterval) {
if player.position.y <= player.size.height / 2{
self.player.removeFromParent()
if let particles = SKEmitterNode(fileNamed: "MyParticle.sks") {
particles.position = player.position
let addParticle = addChild(particles)
let wait = SKAction.wait(forDuration: 2.0)
let removeParticle = SKAction.removeFromParent()
let particleSequence = SKAction.sequence([addParticle, wait, removeParticle]) //Error ->Cannot convert value of type 'Void' to expected element type 'SKAction'
self.run(SKAction.run(particleSequence))
}
}
So what I recommend for you to do is to create a function like the following
func myExplosion (explosionPosition: CGPoint){
let explosion = SKEmitterNode(fileNamed: "MyParticle")// borrowed this from you
explosion?.position = explosionPosition
explosion?.zPosition = 3
self.addChild(explosion!)
self.run(SKAction.wait(forDuration: 2)){//you can always change the duration to whatever you want
explosion?.removeFromParent()
}
}
then when it is time to use this function, use it like so
myExplosion(explosionPosition: player.position)
Hope this can help you out.
I am wondering if its possible to increase withForDuration( ) with a variable. Heres how I am trying to do it in short. This is also SpriteKit, and this is my first time with it so I am still a little unsure. While this snippet of code works to change the float it doesn't actually change the waitForDuration(difficulty)
var timer: NSTimer!
var difficulty = 1.0
override func didMoveToView(view: SKView) {
backgroundColor = SKColor.whiteColor()
player.position = CGPoint(x: size.width * 0.5, y: size.height * 0.25)
player.physicsBody = SKPhysicsBody(circleOfRadius: player.size.width/2)
player.physicsBody?.dynamic = true
addChild(player)
physicsWorld.gravity = CGVectorMake(0, 0)
physicsWorld.contactDelegate = self
var timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "increaseDifficulty", userInfo: nil, repeats: true)
runAction(SKAction.repeatActionForever(
SKAction.sequence([
SKAction.runBlock(addEnemy),
SKAction.waitForDuration(difficulty)
])
))
}
func increaseDifficulty() {
difficulty -= 0.1
}
One way to do this is to use the completionHandler of the runAction function. For example.
func addEnemyWithChangingDifficulty() {
let waitAction = SKAction.waitForDuration(difficulty)
let addEnemyAction = SKAction.runBlock { () -> Void in
self.addEnemy()
}
runAction(SKAction.sequence([addEnemyAction,waitAction]), completion: { () -> Void in
self.addEnemyWithChangingDifficulty()
})
}
Another way would be to use the update function to track the waitDuration.
var lastAddedTime : CFTimeInterval = 0
override func update(currentTime: CFTimeInterval) {
if currentTime - lastAddedTime > CFTimeInterval(difficulty) {
addEnemy()
lastAddedTime = currentTime
}
}
You're not getting your intended effect because the value of 'difficulty' is captured in the first declaration of your SKAction and it never refers to the outside declaration again.
Two ways to solve this:
Instead of doing SKAction.repeatActionForever(), dump the SKAction.sequence() in increaseDifficulty. You'll have to tweak the numbers a bit to make it work, but NSTimer is already running on repeat, you can use that instead.
The second way (not 100% sure about this) is to put the '&' symbol in front of difficulty. This passes difficulty by reference rather by value, which should give you the intended effect. I'm just not sure if Swift allows this, but in C++ that's what we can do.