How to change wait for duration SKAction ran by an SKScene? - ios

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.

Related

Spawning Sprite problems in swift3 xCode

Im having some issues, I have sprites spawn on the right of the screen and work there way left then when they go off the screen they are removed from the scene and the process restarts. I want every spawn to get quicker by like 0.1seconds.
Problem is i am calling my spawn function when the game starts and its on a constant loop so i cant then update the delay.
Spawn Code:
func spawnBirdRL() {
let spawn = SKAction.run({ () -> Void in
self.createEnemyBird()
})
let delay = SKAction.wait(forDuration: (TimeInterval(spawnDuration)))
let sequence = SKAction.sequence([spawn, delay])
self.run(SKAction.repeatForever(sequence), withKey: "Spawn")
}
Which is being called in the TouchesBegan function.
It creates the enemy bird and runs the movement loop over and over
I have changed the question slightly with more information...

How do I only allow one instance of an SKSpriteNode on screen at one time?

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.

iOS SpriteKit - countdown before game starts?

Just getting into SpriteKit on iOS using Swift. I have a 'Breakout' game from a tutorial and I want to implement a countdown before every ball launches, by putting an SKLabel in the middle of the screen which counts down from 5 to 1 before removing itself and the game starting. Whilst the countdown is going on, you can see the full games screen with the wall, stationary ball etc.
I can't work out where in the game loop to do this. If I do the countdown in didMoveToView (where I create the wall and initialise the ball and paddle), I never see it, but I see my debugging messages in the log. I guess that didMoveToView is called before the SKScene is presented.
I tried to use a flag to call the countdown function the first time 'Update' is called, but again I saw the countdown executed before anything appeared on screen - i think 'update' is called initially before the scene is rendered.
I could implement a 'Tap screen to start' screen in a another SKScene, but I really wanted a countdown on the screen with the wall and the (stationary) ball ready to go. I could create this countdown scene using a background image of the game screen, but this seems awkward.
Any suggestions gratefully received,
Steve
Added these functions and a call to countdown(5) at the end of didMoveToView
func countdown(count: Int) {
countdownLabel.horizontalAlignmentMode = .Center
countdownLabel.verticalAlignmentMode = .Baseline
countdownLabel.position = CGPoint(x: size.width/2, y: size.height*(1/3))
countdownLabel.fontColor = SKColor.whiteColor()
countdownLabel.fontSize = size.height / 30
countdownLabel.zPosition = 100
countdownLabel.text = "Launching ball in \(count)..."
addChild(countdownLabel)
let counterDecrement = SKAction.sequence([SKAction.waitForDuration(1.0),
SKAction.runBlock(countdownAction)])
runAction(SKAction.sequence([SKAction.repeatAction(counterDecrement, count: 5),
SKAction.runBlock(endCountdown)]))
}
func countdownAction() {
count--
countdownLabel.text = "Launching ball in \(count)..."
}
func endCountdown() {
countdownLabel.removeFromParent()
ball.physicsBody!.applyImpulse(CGVectorMake(20, 20))
}
So I create and set up the text of the countdown and then create and run an SKAction that waits for 1 second before decrementing the countdown and updating the label. It repeats this 5 times and then removes the countdown label before finally giving the ball an impulse to start it moving and so the game proper can start.
Seems to work ok...
This is where NSTimers really come in handy. An NSTimer basically activates a function every so often (you specify how often).
Update: Sometimes it's better not to use an NSTimer; see here: https://stackoverflow.com/a/24950982/5700898
Here's a code sample, using NSTimer:
class ViewController: UIViewController {
var countdownTime = 5
var countdownTimer = NSTimer()
// above, setting your timer and countdown time as global variables so they can be accessed in multiple functions.
func PlayerPressesStart() {
countdownTime = 5
countdownTimer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "Countdown", userInfo: nil, repeats: true) // calls the function "Countdown" every second.
}
func Countdown() { // this is the function called by the timer every second, which causes your "countdownTime" to go down by 1. When it reaches 0, it starts the game.
countdownTime--
if countdownTime > 0 {
countdownTimer.invalidate()
placewhereyoudisplaycountdown.text = String(countdownTime)
}
if countdownTime == 0 {
// call the function where game action begins here, or call the function that makes the game begin here.
}
} // closing bracket for the View Controller, don't include this if you're copying and pasting to your already existing View Controller.

Why is my SpriteKit update() function creating multiple NSTimers?

I'm creating a SpriteKit game that updates based on the amount of time passed. The game spawns enemies using an NSTimer and its scheduledTimerWithTimeInterval method, calling the spawnEnemy function every 2.0 seconds.
When 5 seconds have passed there should be a very brief intermission, preventing new enemies from spawning in order to show a level change animation.
When the initial 5 seconds has been reached, everything works well up until the conditional where self.nextLevelDelayTicker == 100. Once this conditional is met, the "YOLO" string is only fired once in the console. However, I'm assuming multiple instances of NSTimer are being created and stored within self.timer since a massive amount of enemies are spawned after self.resumeGame() is called to create a new scheduled timer.
Any ideas on why this is happening even though I have flags set up within my conditional to only call the self.resumeGame() function once?
func resumeGame() {
// Start game timer
// Need a way to access ib action of pause button
self.timer = NSTimer.scheduledTimerWithTimeInterval(2.0, target: self, selector: "spawnEnemy", userInfo: nil, repeats: true)
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
if gameTicker.isActive == true {
gameTicker.increment()
}
// If gameTicker is equal to 5 seconds, increase enemy amount
if gameTicker.getTimePassed() % 500 == 0 {
self.enemyAmount += 1
self.timer?.invalidate()
levelCount += 1
gameTicker.isActive = false
}
// If level has been completed and last ghost has been killed, activate next level scene
if gameTicker.isActive == false && enemyArray.count == 0 {
self.nextLevelDelayTicker.increment()
if self.nextLevelDelayTicker.getTimePassed() == 100 {
print("YOLO")
self.gameTicker.isActive = true
self.nextLevelDelayTicker.reset()
self.resumeGame()
}
}
}
Trying to follow your code.. but I think your approach here isn't great for spritekit. It's probably making things way more complicated than it needs to be.
You can keep track of time using your update method directly. It would probably be worth rewriting this part of your code. Would work better within spritekit and be less prone to bugs.
All you really need is delta time.
scene properties
// time values
var delta = NSTimeInterval(0)
var last_update_time = NSTimeInterval(0)
// some time youre trying to keep track of
var timeLimit = NSTimeInterval(5)
var timeLimitMax = NSTimeInterval(5)
your scene's update method
func update(currentTime: NSTimeInterval) {
if last_update_time == 0.0 {
delta = 0
} else {
delta = currentTime - last_update_time
}
last_update_date = currentTime
// now we can keep track of time
timeLimit -= self.delta
if timeLimit <= 0 {
// do something and reset timer
timeLimit = timeLimitMax
}
}
Now if you're going to be consistently spawning something every number of seconds then we dont even need to bother with update to do this. Just put this in your viewDidLoad
Now we're running this code every two seconds forever. The best part is this will pause and resume with your game automatically. You don't have to manage SKAction too much. spritekit does it for you :)
let spawnAction = SKAction.repeatActionForever(
SKAction.sequence([
SKAction.waitForDuration(2),
SKAction.runBlock({
[unowned self] in
self.spawnEnemy()
})
])
)
runAction(spawnAction)

How to repeat action while some condition is true?

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

Resources