Is there a way to pause certain action in an SKSpriteNode while running other actions on the same sprite ?
You can run action with key, like this:
Objective-C
[yourNode runAction:yourAction withKey:#"aKey"];
Then you can access that particular action like this:
SKAction *action = [yourNode actionForKey:#"aKey"];
if(action){
action.speed = 0; //pause action
}
Swift
To run an action with key:
yourNode.runAction(yourAction , withKey: "aKey")
To pause the action:
if let action = ball.actionForKey("aKey"){
action.speed = 0
}
Related
This is my first ever post - I have searched for a long time and could not find the answer.
I am making a game with SpriteKit and want the player to be able to only launch one bomb at a time- i.e they can't fire again until the previous bomb has exploded or gone off screen. Currently when the player taps the screen, they can launch as many bombs as they want.
Any help would be greatly appreciated!
Thanks,
Iain
Steve's idea works out well and is better than mine, but here is a more novice-friendly explanation IMO... Put this in your gamescene :)
var canFireMissile = true
func fireMissile() {
guard canFireMissile else { return }
canFireMissile = false // So you can't fire anymore missiles until 0.5secs later
let wait = SKAction.wait(forDuration: 0.5) // the duration of the missile animation (example)
let reset = SKAction.run { canFireMissile = true } // lets us refire the missile
let sequence = SKAction.sequence([wait, reset])
run(sequence)
}
override func mouseDown(with event: NSEvent) { // touchesBegan on iOS
fireMissile()
}
Create a SKSpriteNode property for your misssile.
Create an SKAction for the movement of the missile and give the action a key so you can refer to it by name).
When the fire button is pressed, check to see if the named action is already running; if it is, do nothing, otherwise run the ‘fireMissile’ action.
I want to be able to run multiple animations, one after another, in SceneKit. I've implemented a function which runs one animation like so:
fileprivate func animateMove(_ move: Move) {
print("Animate move started " + move.identifier)
// I am creating rotateNode
let rotateNode = SCNNode()
rotateNode.eulerAngles.x = CGFloat.pi
scene.rootNode.addChildNode(rotateNode)
// Then I am selecting nodes which I want to rotate
nodesToRotate = ...
// Then I am adding the nodes to rotate node
_ = nodesToRotate.map { rotateNode.addChildNode($0) }
SCNTransaction.begin()
SCNTransaction.animationDuration = move.animationDuration
SCNTransaction.completionBlock = {
rotateNode.enumerateChildNodes { node, _ in
node.transform = node.worldTransform
node.removeFromParentNode()
scene.rootNode.addChildNode(node)
}
rotateNode.removeFromParentNode()
print("Animate move finished " + move.identifier)
}
SCNTransaction.commit()
}
And then I've tried to run multiple serial animations like so:
func animateMoves(_ moves: [Move]) {
for (index, move) in moves.enumerated() {
perform(#selector(animateMove(_:)),
with: move,
afterDelay: TimeInterval(Double(index) * move.duration)
}
}
Everything is animating but the animations don't run in a serial manner. Animations start and finish in unpredictable time.
Sample logs from debugger:
Animate move started 1
Animate move started 2
Animate move finished 1
Animate move finished 2
Animate move started 3
Animate move finished 3
I realise that my approach isn't the best, but only in that way I was able to achieve almost working animations.
I know that there is a SCNAction class available. Maybe I should make many actions within one transaction? If so, could someone explain to me how exactly SCNTransactions work and why the completion SCNTransaction's completion block fires in unpredictable time?
Try to use SCNAction.sequence():
class func sequence([SCNAction])
Creates an action that runs a collection of actions sequentially
let sequence = SCNAction.sequence([action1, action2, action3]) // will be executed one by one
let node = SCNNode()
node.runAction(sequence, completionHandler:nil)
Following #Oleh Zayats' answer I've tried to implement my case using SCNAction.sequence(_:) method, but the problem was that I needed completion handler to be fired after every completed subaction in order to be able to remove the nodes from rotationNode.
After a couple of hours of struggling I've ended up with quite a nice solution and it worked like a charm.
Namely:
I've made a function rotateAction which looks something like this:
func rotateAction(with move: Move, from rotateNode: SCNNode) -> SCNAction {
let preAction = SCNAction.run { (rotateNode) in
// all the pre action setup like choosing nodes to rotate
}
let action = SCNAction.rotate(by: -move.angle, around: vector, duration: move.animationDuration)
let postAction = SCNAction.run { (rotateNode) in
// completion handler for each action
}
return SCNAction.sequence([preAction, action, postAction])
}
Then I was able to write a function to be able to run multiple animations one after another:
func animateRotateMoves(_ moves: [Move]) {
let rotateNode = SCNNode()
scene.rootNode.addChildNode(rotateNode)
var actions: [SCNAction] = []
for move in moves {
let action = rotateAction(with: move, from: rotateNode)
actions.append(action)
}
actions.append(SCNAction.removeFromParentNode())
let sequence = SCNAction.sequence(actions)
rotateNode.runAction(sequence)
}
var action = SKAction.sequence([
SKAction.waitForDuration(1),
SKAction.runBlock({
//Some code
})
])
I want the above action to keep repeating while some condition is true. How can I do this?
I know I can run the action once with runAction(action) or repeat forever using runAction(SKAction.repeatActionForever(action)). How do I only repeat it while some condition is true though?
If this isn't possible using actions, is there an alternative to how I can repeat these steps while some condition is true (obviously, on a separate thread, I don't want this to freeze my app):
1) Wait for a second
2) Execute a block of code
3) Check if the condition is true. If it is stop repeating. Else, repeat.
I'm hesitant to use sleep() because that sounds like a bad solution, and something Apple wouldn't allow for apps in their store.
Alternative Solution using Swift:
runAction(
SKAction.repeatActionForever (
SKAction.sequence([
SKAction.waitForDuration(1),
SKAction.runBlock({
//Code you want to execute
if conditionIsTrue {
self.removeActionForKey("New Thread")
}
})
])
),
withKey: "New Thread"
)
You can use SKAction's
(SKAction *)runBlock:(dispatch_block_t)block
queue:(dispatch_queue_t)queue
You would want to create a block. Inside just check for the variable and then apply your logic accordingly, once the condition is no longer true you can remove it from the object.
If you want to modify the variable add __block in front of its declaration.
SKAction *logic = [SKAction runBlock:^{
if (myvar){
NSLog(#"hello world!");}
else
{
//remove actions
}} queue : dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
SKAction *delay = [SKAction waitForDuration: 0.5];
SKAction *mySequence = [SKAction sequence:#[logic,delay,nil]];
[myNode runAction:mySequence];
easy as apple pie my boy
I know this may sound weird, but I'm wondering if there is a way I can set a method for is my sprite doesn't make contact with anything. For example; if my sprite doesn't touch an object the game is over. I'm very sorry I can't provide any sample codes because I have absolutely no idea how to do what I just described. Usually for my CollisionTests I would simply import this:
didBeginContact(contact: SKPhysicsContact) {
let firstnode = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
switch(firstnode){
case Body.faceMask.rawValue | Body.redPoint.rawValue:
//gameOver = true
sampleMethod()
default:
print("a")
}
I don't think it's possible to set an else method inside there because I tried and it gave me errors.
This should really be a comment but I don't have enough rep yet!
What is your sprite trying to do? For example is it like a projectile that gets fired from somewhere? And if it misses you loose?
When you give your sprite an action, can you give it an action to complete after that?
For example:
let actionMove = SKAction.moveTo(CGPoint(10, 10), duration: 1.0)
let actionMoveDone = SKAction.rubBlock() {
// Code here to run after the object finishes moving
}
sprite.runAction(SKAction.sequence([actionMove, actionMoveDone]))
In my SpriteKit project, I have a spawnEnemy method. I want this method to be called over and over again so that enemies continue to spawn throughout the game.
What I did to achieve this is to put the spawnEnemy method in an SKAction runBlock and have my SKScene run that action forever with a delay in between calls (to prevent overloading the app).
Below is a snippet of the relevant code:
var _spawnSpeedSeconds: Double = 2.0
func startSpawning()
{
let waitForXSeconds = SKAction.waitForDuration(self._spawnSpeedSeconds)
let spawn = SKAction.runBlock({ () -> Void in
self.spawnEnemy()
})
self.runAction(SKAction.repeatActionForever(SKAction.sequence([spawn, waitForXSeconds])), withKey: "spawnEnemy")
}
func spawnEnemy()
{
...
}
Now after a certain time (for example when the player gets 5 points or something), I decrease the _spawnSpeedSeconds to make the enemies spawn more in a shorter amount of time to increase difficulty.
The problem is, even if I decrease my _spawnSpeedSeconds variable, the spawn action's delay being ran by the SKScene is still the same.
The only way I can think of resolving this issue is to remove the action, and then re-add the action with a new delay/spawn rate. Is there a better way to approach this issue?
Thanks
don't use an action in this case. They don't really work in a dynamic way. Once you set them, they're stuck. just use your update method.
I'll show you how I'm periodically launching missles:
first set two timers in your class
var missleTimer:NSTimeInterval = NSTimeInterval(2)
var missleInterval:NSTimeInterval = NSTimeInterval(2)
now in our update method we count down the time and spawn missles
// subtract time from our timer
self.missleTimer -= self.delta
// when our timer reaches zero
if self.missleTimer <= 0 {
// run your spawning code
self.launchMissle()
// reset timer
self.missleTimer = self.missleInterval
}
this is better than using an action in this case because I can set missleInterval anywhere in my code and the change will always be reflected.
-(void)recursiveMethod
{
if(shouldSpawnEnemy)
{
_spawnSpeedSeconds -= 0.01;
SKAction *wait = [SKAction waitForDuration: 0.5];
SKAction *action = [SKAction performSelector:#selector(recursiveMethod) onTarget:self];
SKAction *sequence = [SKAction sequence:#[wait,action]];
[self repeatActionForever:sequence];
}
}
Remember to call the recursiveMethod when you want to start spawning enemies.
Hope that helps.
Providing swift solution without the need for timers, using recursion with a circuit breaker flag:
class YourScene: SKScene {
var gameOver: Bool = false
func spawnEnemies() {
let waitAction = SKAction.wait(forDuration: yourDynamicDurationHere)
self.run(SKAction.sequence([SKAction.run(self.spawnEnemy), waitAction])) {
if !gameOver {
spawnEnemies()
}
}
}
}
This solution leverages the completion handler of run method to recursively schedule another execution of the sequence. Since each individual SKSequence is created just before it is executed, we can pass in a different wait time for each iteration.
To stop the recursive loop, just set gameOver = true.