Swift 3 (SpriteKit): Stopping a forever looping SKAction has a delay - swift

I have been trying to make a forever running SKAction that I can stop whenever I want to. I have done it like this:
override func didMove(to view: SKView) {
run(SKAction.repeatForever (
SKAction.sequence ([
SKAction.run(drawFrame),
SKAction.wait(forDuration: 0.01),
])), withKey: "frameDrawing"
)
}
Then in the drawFrame function I stop the SKAction like this:
func drawFrame() {
//(code)
if stop {
removeAction(forKey: "frameDrawing")
}
}
For some reason the SKAction only stops when it has run 3 or 4 more times after stop became true. I want it to stop instantly when stop is set to true, not after 3 or 4 more repeats.
If anyone knows how to fix this, please tell me because I've tried many things and they never fix the issue. Thanks!
Edit: Here is my code:
var drawingFrame = SKAction()
class GameScene: SKScene, SKPhysicsContactDelegate {
override func didMove(to view: SKView) {
drawingFrame = SKAction.repeatForever (
SKAction.sequence ([
SKAction.run(drawFrame),
SKAction.wait(forDuration: 0.03),
]))
run(drawingFrame)
}
func drawFrame() {
//(code)
if stop {
drawingFrame.speed = 0.0
}
}
If you're wondering why I have set the SKAction drawingFrame to an empty SKAction at the start, it is because I needed to define the SKAction before both functions. Otherwise, it would be not defined for both functions.
EDIT FOR ANYONE WITH THE SAME PROBLEM: I have fixed the problem using my own thinking and #appzYourLife's solution. The most efficient way which works every time is to only run the code if stop equals false. But, make sure that the if statement that stops the program is outside of that bracket so the SKAction will eventually stop. This is the working code:
var drawingFrame = SKAction()
class GameScene: SKScene, SKPhysicsContactDelegate {
override func didMove(to view: SKView) {
drawingFrame = SKAction.repeatForever (
SKAction.sequence ([
SKAction.run(drawFrame),
SKAction.wait(forDuration: 0.03),
]))
run(drawingFrame)
}
func drawFrame() {
if stop = false {
//(code)
}
if stop {
removeAllActions()
}
}
You may use an if else statement for the stop = false statement if you prefer that.

I don't have an exact explanation why drawFrame() is called multiple times at the moment (but I will try to discover that :D)... Anyways, try this code:
import SpriteKit
class GameScene: SKScene, SKPhysicsContactDelegate {
var stop = false
override func didMove(to view: SKView) {
run(SKAction.repeatForever (
SKAction.sequence ([
SKAction.run({[unowned self] in
if self.stop {
self.action(forKey: "frameDrawing")?.speed = 0.0
}else{
self.drawFrame()
}
}),
SKAction.wait(forDuration:0.03),
])), withKey: "frameDrawing"
)
}
func drawFrame() {
//(code)
print("drawFrame")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
stop = !stop
print(stop ? "stopped": "running")
if !stop {
self.action(forKey: "frameDrawing")?.speed = 1.0
}
}
}
Use touchesBegan to toggle paused mode.

You can remove all the actions running on the current node. This will remove immediately not only the sequence action but also the embedded actions.
func drawFrame() {
if stop {
removeAllActions()
}
}

Try creating a reference to the SKAction, and calling a change of speed (to 0.0) on that, when you want it to stop. Both of these will be quicker than the need to look up the name of the action and then remove it. But at 0.01 you're already repeating it faster than the frame rate (0.01666), so you're always going to get at least one extra iteration of the action, no matter how well you stop it.
something like:
let myAction = SKAction.repeatForever (
SKAction.sequence ([
SKAction.run(drawFrame),
SKAction.wait(forDuration: 0.01),
]))
//when you want to run it
run(myAction)
// when you want to stop it:
myAction.speed = 0.0

Related

Remove SKAction and restore node state

Desired behavior is: when an action is removed from a node (with removeAction(forKey:) for instance) it stops to animate and all the changes caused by action are discarded, so the node returns back to pervious state. In other words, I want to achieve behavior similar to CAAnimation.
But when a SKAction is removed, the node remains changed. It's not good, because to restore it's state I need to know exactly what action was removed. And if I then change the action, I also will need to update the node state restoration.
Update:
The particular purpose is to show possible move in a match-3 game. When I show a move, pieces start pulsating (scale action, repeating forever). And when the user moves I want to stop showing the move, so I remove the action. As the result, pieces may remain downscaled. Later I would like to add more fancy and complicated animations, so I want to be able to edit it easily.
Thanks to the helpful comment and answer I came to my own solution. I think the state machine would be bit too heavy here. Instead I created a wrapper node, which main purpose is run the animation. It also has a state: isAimating property. But, first of all, it allows to keep startAnimating() and stopAnimating() methods close to each other, incapsulated, so it's more difficult to mess up.
class ShowMoveAnimNode: SKNode {
let animKey = "showMove"
var isAnimating: Bool = false {
didSet {
guard oldValue != isAnimating else { return }
if isAnimating {
startAnimating()
} else {
stopAnimating()
}
}
}
private func startAnimating() {
let shortPeriod = 0.2
let scaleDown = SKAction.scale(by: 0.75, duration: shortPeriod)
let seq = SKAction.sequence([scaleDown,
scaleDown.reversed(),
scaleDown,
scaleDown.reversed(),
SKAction.wait(forDuration: shortPeriod * 6)])
let repeated = SKAction.repeatForever(seq)
run(repeated, withKey: animKey)
}
private func stopAnimating() {
removeAction(forKey: animKey)
xScale = 1
yScale = 1
}
}
Usage: just add everything that should be animated to this node. Works well with simple animations, like: fade, scale and move.
As #Knight0fDragon suggested, you would be better off using the GKStateMachine functionality, I will give you an example.
First declare the states of your player/character in your scene
lazy var playerState: GKStateMachine = GKStateMachine(states: [
Idle(scene: self),
Run(scene: self)
])
Then you need to create a class for each of these states, in this example I will show you only the Idle class
import SpriteKit
import GameplayKit
class Idle: GKState {
weak var scene: GameScene?
init(scene: SKScene) {
self.scene = scene as? GameScene
super.init()
}
override func didEnter(from previousState: GKState?) {
//Here you can make changes to your character when it enters this state, for example, change his texture.
}
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
return stateClass is Run.Type //This is pretty obvious by the method name, which states can the character go to from this state.
}
override func update(deltaTime seconds: TimeInterval) {
//Here is the update method for this state, lets say you have a button which controls your character velocity, then you can check if the player go over a certain velocity you make it go to the Run state.
if playerVelocity > 500 { //playerVelocity is just an example of a variable to check the player velocity.
scene?.playerState.enter(Run.self)
}
}
}
Now of course in your scene you need to do two things, first is initialize the character to a certain state or else it will remain stateless, so you can to this in the didMove method.
override func didMove(to view: SKView) {
playerState.enter(Idle.self)
}
And last but no least is make sure the scene update method calls the state update method.
override func update(_ currentTime: TimeInterval) {
playerState.update(deltaTime: currentTime)
}

Cannot disable, then reenable touch, after an SKAction animation

I am working on an interactive, animated scene. I want all touches on the scene to be disabled on entry. Then, once the objects (which are subclassed nodes) in the scene finish rotating/moving, I want to re-enable all touches on the screen to allow interaction. I have disabled user interaction using this code:
override func didMove(to view: SKView) {
setupNodes()
view?.isUserInteractionEnabled = false
spinLocations()
}
This is the code, within the scene file, for spinLocations:
func spinLocations() {
var allLocationArrays = [[String : CGPoint]]()
var previousArray = hiddenLocationPositions
for _ in 0...SearchConstant.numSpins {
let freshArray = generateNewLocationArray(previous: previousArray)
allLocationArrays.append(freshArray)
previousArray = freshArray
}
for (item, _) in hiddenLocationPositions {
let node = fgNode.childNode(withName: item) as! LocationNode
node.spin(position: allLocationArrays) // this is function below
}
hiddenLocationPositions = previousArray
}
This is the code for the animations in the node class:
func spin(position: [[String : CGPoint]]) {
var allActions = [SKAction]()
for array in position {
let action = SKAction.move(to: array[self.name!]!, duration: 2.0)
allActions.append(action)
}
let allActionsSeq = SKAction.sequence(allActions)
self.run(SKAction.sequence([SKAction.wait(forDuration: 5.0), allActionsSeq, SKAction.run {
self.position = position[position.count - 1][self.name!]!
},]))
}
This is the code for passing back the touches to the main scene from this class:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let parent = self.parent else { return }
}
As you can see, touch is not disabled here.
I do not want to add a "waitForDuration" SKAction to the runBlock to change the view status after the previous action; I want the program to determine when the animations are finished executing and then re-enable touches.
In order to do this, I theorised using a completion handler might work, but it only re-enables touches immediately (e.g. handling a handler to spin causes the touches to be detected again). Previously, I also tried to disable the view in the runBlock, but of course, that is run instantaneously. How do I ensure that the touches are re-detected following the animation without using "waitForDuration."?
So, this is a simple example that shows how you can:
1) Disable touches completely
2) Spin a node
3) When node is done with spinning, to enable touches
Here is the code (you can copy/paste it to try how it works):
class Object:SKSpriteNode{
func spin(times:Int,completion:#escaping ()->()) {
let duration = 3.0
let angle = CGFloat(M_PI) * 2.0
let oneRevolution = SKAction.rotate(byAngle: angle , duration: duration)
let spin = SKAction.repeat(oneRevolution, count: times)
let sequence = SKAction.sequence([spin,SKAction.run(completion)])
run(sequence, withKey:"spinning")
}
}
class WelcomeScene: SKScene {
override func didMove(to view: SKView) {
view.isUserInteractionEnabled = false
print("Touches Disabled")
let object = Object(texture: nil, color: .purple, size: CGSize(width: 200, height: 200))
addChild(object)
object.spin(times: 3, completion: {[weak self] in
self?.view?.isUserInteractionEnabled = true
print("Touches Enabled")
})
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("touch detected")
}
deinit {
print("Welcome scene deinited")
}
}
Here, you disable touches when scene is loaded, start spinning the object, and you pass a completion block to it... That block of code is used here:
let sequence = SKAction.sequence([spin,SKAction.run(completion)])
So after spinning, that block will be executed. Now, there are different ways to do this...Personally, I would use delegation, but I thought this can be less confusing... I can write an example for delegation too if needed, but basically, what you would do, is to set a scene as a delegate of your custom node, and notify it about spinning is done, so the scene can tell the view to re-enable the touches.

Integer value not updating

I am creating a game and I am trying to keep a record of all enemy's killed but my SKLabel node is not updating. Here's how I'm implementing it
class GameScene: SKScene, SKPhysicsContactDelegate {
var Enemy1KillCounter:Int = 0
var Enemy1KillCounterLabel = SKLabelNode ()
override func didMoveToView(view: SKView) {
createEnemyKilledLabel()
}
func createEnemyKilledLabel() {
Enemy1KillCounterLabel.text = "\(Enemy1KillCounter)"
Enemy1KillCounterLabel.fontSize = 65
Enemy1KillCounterLabel.fontColor = SKColor .blackColor()
Enemy1KillCounterLabel.position = CGPointMake(400, 400)
self.addChild(Enemy1KillCounterLabel)
}
func updateEnemy1KillCounter() {
Enemy1KillCounter = Enemy1KillCounter + 1
print(Enemy1KillCounter)
}
// I use the next method because i call this method in my enemy class
when the enemy is "killed"
func Enemy1DieG () {
updateEnemy1KillCounter()
}
Does anybody know why my label is not being updated?
When you update Enemy1KillCounter, you also need to update the Enemy1KillCounterLabel.text with the new value. Besides, I don't see where your createEnemyKilledLabel() is called. Make sure it is called somewhere.
A side note - variable names typically start with lowercase, like enemy1KillCounterLabel. Following the standards makes the code easier to read by others...
Update your label text after updating your Enemy1KillCounter variable.
func updateEnemy1KillCounter() {
Enemy1KillCounter = Enemy1KillCounter + 1
Enemy1KillCounterLabel.text = "\(Enemy1KillCounter)"
print(Enemy1KillCounter)
}

Varying value inside runAction() not changing

I have a run action loop that spawns an enemy then waits and spawns another. The intention is that the higher the score the quicker the enemies spawn.
But the current code has the enemies spawn at the same rate no matter what. I update the rate in the override func update(currentTime: NSTimeInterval) method all the time, so I don't know what's wrong.
override func update(currentTime: NSTimeInterval){
spawnRate = 2.0-(0.1*(Double)(((Double)(Score))/(10.0)))
if(spawnRate < 0.5){
spawnRate = 0.5
}
}
override func didMoveToView(view: SKView) {
runAction(SKAction.repeatActionForever(
SKAction.sequence([
SKAction.runBlock(addEnemy),
SKAction.waitForDuration(spawnRate)
])
))
}
What is happening currently is that you've:
stored duration paramter inside an action
reused that action within sequence over and over again
So nothing actually changes. To solve this, one way would be to use recursive call and change duration parameter each time:
import SpriteKit
class GameScene: SKScene {
let shape = SKSpriteNode(color: UIColor.redColor(), size: CGSize(width: 20, height: 20))
var delay:NSTimeInterval = 1.0
override func didMoveToView(view: SKView) {
recursiveMethod()
}
func recursiveMethod(){
let recursive = SKAction.sequence([
SKAction.waitForDuration(delay),
SKAction.runBlock({self.recursiveMethod();println("delay \(self.delay)")})
])
runAction(recursive)
}
override func update(currentTime: NSTimeInterval) {
if(delay > 0.2){delay = delay - 0.001}
}
}
The problem is that This:
SKAction.sequence([
SKAction.runBlock(addEnemy),
SKAction.waitForDuration(spawnRate)
])
is made one's and then just repeated forever.
try to put the code in a
runAction(SKAction.repeatActionForever(runblock(spawnAndWait()))
and then make a seperate func.
spawnAndWait() { // put here the code for spawning the enemy and waiting spawnrate duration.
b.t.w
Put the code that updates the spawnRate where the score is increased. Because that is more efficient. then in the update method because that is constantly called.

Sprite Kit physicsBody.resting behavior

I am using Swift and Sprite Kit to develop a game on XCode Beta 6.
In order to detect if all nodes are sleeping, i check their physicsBody.resting property.
In update method i print out the result.
import SpriteKit
class GameScene: SKScene, SKPhysicsContactDelegate {
var hero:SKSpriteNode!
override func didMoveToView(view: SKView) {
self.physicsWorld.gravity = CGVectorMake(0, 0)
self.physicsWorld.contactDelegate = self
self.physicsBody = SKPhysicsBody(edgeLoopFromRect:self.frame)
hero = SKSpriteNode(imageNamed: "Spaceship")
hero.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame))
hero.zPosition = 10.0
hero.physicsBody = SKPhysicsBody(circleOfRadius: hero.size.width/2)
hero.physicsBody.allowsRotation = false
hero.physicsBody.linearDamping = 0.5
self.addChild(hero)
}
override func update(currentTime: CFTimeInterval) {
if hero.physicsBody.resting {
println("resting")
} else {
println("moving")
}
}
}
To my surprise, the results are:
moving
resting
moving
(n times the same)
moving
resting
So why the hero is moving, although i didn't do anything. The node moves N times and takes a break(resting), after that goes on moving.
Can anyone explain that behaviour? Is that a bug or do i miss something? Thanks in advance.
If you examine the velocity of a physics body, you'll see that it is indeed moving but at a rate that is not perceivable. That's why the resting property is not set. A more reliable way to check if a SKPhysicsBody is at rest is to test if its linear and angular speeds are nearly zero. Here's an example of how to do that:
func speed(velocity:CGVector) -> Float {
let dx = Float(velocity.dx);
let dy = Float(velocity.dy);
return sqrtf(dx*dx+dy*dy)
}
func angularSpeed(velocity:CGFloat) -> Float {
return abs(Float(velocity))
}
// This is a more reliable test for a physicsBody at "rest"
func nearlyAtRest(node:SKNode) -> Bool {
return (self.speed(node.physicsBody.velocity)<self.verySmallValue
&& self.angularSpeed(node.physicsBody.angularVelocity) < self.verySmallValue)
}
override func update(_ currentTime: TimeInterval) {
/* Enumerate over child nodes with names starting with "circle" */
enumerateChildNodesWithName("circle*") {
node, stop in
if (node.physicsBody.resting) {
println("\(node.name) is resting")
}
if (self.nearlyAtRest(node)) {
println("\(node.name) is nearly resting")
}
}
}