I created a game in Swift that involves monsters appearing. Monsters appear, and disappear, based on timers using something like this:
func RunAfterDelay(_ delay: TimeInterval, block: @escaping ()->())
{
let time = DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
DispatchQueue.main.asyncAfter(deadline: time, execute: block)
}
and then I'd just call it like this (for example to spawn after 2 seconds):
///Spawn Monster
RunAfterDelay(2) {
[unowned self] in
self.spawnMonster()
}
I then do something similar for hiding (after x seconds, I despawn the monster).
So I created a settings icon at the top of the screen, and when you tap it, a giant rectangular window appears to change game settings, but naturally the problem is the monsters still spawn in the background. If I whisk the player away to another screen, I believe i'll lose all my game state and can't come back to it without starting all over (the player might be in the middle of their game).
Is there a way to tell all game timers I've created in the above, i.e.
DispatchQueue.main.asyncAfter(deadline: time, execute: block)
To pause and resume when I say so? I guess it's fine to do it with all timers (if there isn't a way to label and pause certain timers).
Thanks!
I will show a few things here for you, and some more for the future readers, so they will have a workable example by just copy-pasting this code. These few things are next:
1. Creating a timer using
SKAction
2. Pausing an action
3. Pausing a node itself
4. And as I said, a few things more :)
Note that all of these can be done in a different ways, even simpler than this (when it comes to pausing of actions and nodes) but I will show you detailed way, so you can chose works best for you.
Initial Setup
We have a hero node, and an enemy node. Enemy node will spawn every 5 seconds at the top of the screen and will go downwards, towards the player to poison him.
As I said, we are going to use only
SKActions
, noNSTimer
, not even theupdate:
method. Pure actions. So, here, the player will be static at the bottom of the screen (purple square) and the enemy (red square) will, as already mentioned, travel towards the player and will poison him.So lets see some code. We need to define usual stuff for all this to work, like setting up physics categories, initialization and positioning of nodes. Also we are going to set things like enemy spawning delay (8 seconds) and poison duration (3 seconds):
There is also variable called
isGamePaused
which I will comment more later, but as you can imagine, its purpose is to track if game is paused and its value changes when user taps big yellow square button.Helper Methods
I've made a few helper methods for node creation. I have a feeling that this is not required for you personally, because you looks like you have a good understandings of programming, but I will make it for completeness and for the future readers. So this is the place where you setup things like node's name , or its physics category... Here is the code:
Also, I separated creating of an enemy with its actual spawning. So creating here means create, setup, and return a node which will be later added to a node tree. Spawning means use previously created node add it to a scene, and run action (moving action) to it, so it can move towards the player:
I think that there is no need for going here into about spawning method because it is very simple. Lets go further to the spawning part:
SKAction Timer
Here is a method which will spawn enemies every x seconds. It will be paused every time we pause an action associated with a "spawning" key.
After the node is spawned, it will eventually collide (more precisely, it will make a contact) with a hero. And this is where physics engine comes into play...
Detecting contacts
While enemy is traveling, it will eventually reach the player, and we will register that contact:
Contact detection code is borrowed from here (from author Steve Ives).
I would not go into how contact handling in SpriteKit works, because I would go too much into off-topic that way. So when contact between hero and a projectile is registered, we are doing few things:
1. Stop all actions on projectile so it will stop moving. We could do this by stopping a moving action directly and I will show you later how to do that.
2. Removing a projectile from a parent, because we don't need it anymore.
3. Adding poisoning effect by adding emitter node (I made that effect in particle editor using Smoke template).
Here is the relevant method for the step 3:
As I said, I will mention some things that are not important for your question, but are crucial when doing all this in
SpriteKit
.SKEmitterNode
is not removed when emitting is done. It stays in a node tree and eat up resources (at some percent). That is why you have to remove it by yourself. You do this by defining action sequence of two items. First is anSKAction
which waits for a given time (until emitting is done) and second item would be an action which will remove an emitter from its parent when time comes.Finally - Pausing :)
The method responsible for pausing is called
togglePaused()
and it toggles game's paused state based onisGamePaused
variable when yellow button is tapped:What is happening here is actually simple: we stop spawning action by grabbing it using previously defined key (spawning), and in order to stop it we set action's speed to zero. To unpause it we will do the opposite - set actions speed to 1.0. This applies to the moving action as well, but because many nodes can be moved we enumerate through all of the nodes in a scene.
To show you a difference, I pause
SKEmitterNode
directly, so there is one more way for you to pause things in SpriteKit. When the node is paused, all its actions and actions of its children is paused as well.What is left to mention is that I detect in
touchesBegan
if button is pressed, and runtogglePaused()
method every time, but I think that code is not really needed.Video example
To make a better example I have recorded a whole thing. So when I hit the yellow button, all actions will be stopped. Means spawning, moving and poison effect if present will be frozen. By tapping again, I will unpause everything. So here is the result:
Here you can (clearly?) see that when an enemy hits a player, I pause the whole thing , say 1-1.5 seconds after the hit occurred. Then I wait for like 5 seconds or so, and I unpause everything. You can see that emitter continues with emitting for a second or two, and then it disappears.
Note that when an emitter is unpaused, it doesn't look like that it was really unpaused :), but rather looks like that particles were emitting even the emitter is paused (which actually true). This is a bug on iOS 9.1 and I am still on iOS 9.1 on this device :) So in iOS 10, it is fixed.
Conclusion
You don't need
NSTimer
for this kind of things in SpriteKit becauseSKActions
are meant for this. As you can see, when you pause the action, a whole thing will stop. Spawning is stopped, moving is stopped, just like you asked... I have mentioned that there is an easier way to do all this. That is, using a container node. So if all of your nodes were in one container, all nodes, actions and everything will be stopped just by pausing the container node. Simple as that. But I just wanted to show you how you can grab an action by a key, or pause the node, or change its speed... Hope this helps and make sense!I have solved this and would like to share my hours worth of research/coding in the conclusion below. To restate the problem more simply, I actually wanted to achieve this (not simply using the SpriteKit scene pause, which is quite easy):
Someone had mentioned to me that because I am using DispatchQueue.main.asyncAfter there is no way to pause/stop in the way I want (you can cancel but I digress). This makes sense, after all i'm doing an asyncAfter. But to actually get a timer going, you need to use NSTimer (now in Swift3 it is called Timer).
After researching, I see this actually not possible to pause/unpause so you "cheat" by creating a new timer (for each one) when you want to restart paused timers. My conclusion to do this is as follows:
Now because I had multiple timers, I decided I needed to create a dictionary in my AppDelegate (accessed via a service class) to retain all my active timers. Whenever a timer ended, I would remove it from the dictionary. I ended up making a special class that had properties for the timer, the initial delay, and the time it started. Technically I could've used an array and also put the timer key on that class, but I digress..
I created my own addTimer method that would create a unique key for each timer and then when the timer's code finished, it would self-remove as follows:
Note: block() is simply calling whatever block you wrap in your timer. For example I did something cool like this:
So addTimer would run the self.spawnMonster code (as block()) and then it would self-remove from the dictionary when done.
I got way more sophisticated later, and did things like keep repeating timers running and not self-removing, but it's just a lot of very specific code for my purposes and probably would consume way too much of this reply :)
Anyway I really hope this helps someone, and would love to answer any questions that anyone has. I spent a lot of time on this!
Thanks!