Keep SpriteKit scene paused after app becomes active - ios

When returning to my App after closing it the applicationDidBecomeActive(application: UIApplication) automatically fires in AppDelegate.swift.
This fires a method that handles the paused status of the app:
GameViewController().pause(true)
The method looks like this:
func pause(paused: Bool) {
if paused == true {
scene?.paused = true
print("paused")
} else if paused == false {
scene?.paused = false
print("unparsed")
}
}
When first launching the app the Game is automatically paused which is exactly what should happen. When returning to the app it unpauses though. Still, the Console prints "paused".
I have also tried using scene?.view?.paused instead of scene?.paused. This does work, but leads to lag in the animations running on the scene.
Any help would be highly appreciated
EDIT
I managed to solve the problem by calling the pause() method in the update(currentTime: NSTimeInterval) function but I don't like this solution as it means the method is called once per frame. Other solutions would be highly appreciated

This code makes no sense
GameViewController().pause(true)
because you are creating a new instance of GameViewController rather than accessing the current one.
Rather than pausing the whole scene you should just pause the nodes that you would liked paused. Usually you create some kind of worldNode in your game scene (Apple also does this in DemoBots)
class GameScene: SKScene {
let worldNode = SKNode()
// state machine, simple bool example in this case
var isPaused = false
....
}
than add it to the scene in DidMoveToView
override func didMoveToView(view: SKView) {
addChild(worldNode)
}
Than all nodes that you need paused you add to the worldNode
worldNode.addChild(YOURNODE1)
worldNode.addChild(YOURNODE2)
Than your pause function should look like this
func pause() {
worldNode.paused = true
physicsWorld.speed = 0
isPaused = true
}
and resume like this
func resume() {
worldNode.paused = false
physicsWorld.speed = 1
isPaused = false
}
Lastly to make sure the game is always paused when in paused add this to your update method. This ensures that you game does not resume by accident e.g due to app delegate, iOS alerts etc.
override func update(currentTime: CFTimeInterval) {
if isPaused {
worldNode.paused = true
physicsWord.speed = 0
return
}
// Your update code
...
}
To call these from your AppDelegate you should use delegation or NSNotificationCenter as has been mentioned in one of the comments.
In gameScene create the NSNotifcationObserver in didMoveToView
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(pause), name: "Pause", object: nil) // in your app put the name string into a global constant property to avoid typos when posting
and in appDelegate post it at the correct spot
NSNotificationCenter.defaultCenter().postNotificationName("Pause", object: nil)
The main benefit with the worldNode approach is that you can easily add pause menu sprites etc while the actual game is paused. You also have more control over your game, e.g having the background still be animated while game is paused.
Hope this helps.

Related

SpriteKit animation not executed reliably

I'm working on an iOS game based on SpriteKit/Swift using xCode, currently experimenting around with animations.
I've created a class PlayerSprite as a subclass of SKSpriteNode and defined a method moveRight running the following action:
run(
SKAction.moveBy(x: 32.0, y: 0.0, duration: 0.25), completion: {
debugPrint("Action completed.")
}
)
There's only one player instance of class PlayerSprite which is part of the node tree (SKScene -> SKTileMap -> PlayerSprite). The point is, that the mentioned action is not executed reliably:
When I start the app "for the first time" it's executed.
When I stop the app and start it again it's NOT executed.
When I press "pause" in the debugger and than "play" again, the action is executed!
This holds for the simulator as well as for starting the app on a connected iPhone. Stopping the app refers to pressing the stop button in xCode and starting to running it again from xCode.
The update loop is part of the SKScene subclass (called LevelSceneView in this case) and looks like this (just playing around so far):
override func update(_ currentTime: TimeInterval) {
debugPrint("Update called")
if !player.hasActions() {
player.moveRight()
}
}
The update loop is called correctly, but the action is not executed (according to the log console).
Has anyone experienced such a behavior yet? Any ideas would be very much appreciated. I hope I've described the issue adequately. In case of any questions don't hesitate to ask.
So If I understand correctly, the animation doesn't always run? I did run into a problem like this a while ago using xCode 9. The way I fixed it was to toggle the isPaused properties of the scene. So at the end of my update call I have two lines of code that read like so:
self.isPaused = true
self.isPaused = false
This way each time update() is called the scene is paused then paused. Using this I have never had a problem with running animations since. Hopefully, that helps get around the problem.
Where is your animation located?
If it's in didMove function then your action is going to run only when your app is loaded/loading.
I think the problem could be that when you stop your app it's going to stay in phone memory for some time.
That will probably cause the problem, I would recommend to do not put any kinds of actions in didMove function if it's possible.
I use the simple subclass from the SKSpriteNode and found no issue. Maybe you can restart from here.
class MySpriteNode: SKSpriteNode {
func moveRight(){
run(
SKAction.moveBy(x: 3, y: 0.0, duration: 0.25), completion: {
debugPrint("Action completed.")
}
)
}
}
import SpriteKit
import GameplayKit
class GameScene: SKScene {
// private var label : SKLabelNode?
// private var spinnyNode : SKShapeNode?
private var myNode : MySpriteNode?
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
debugPrint("Update called")
if !(myNode?.hasActions())! {
myNode?.moveRight()
}
}
override func didMove(to view: SKView) {
myNode = MySpriteNode.init(texture: nil, color: UIColor.red, size: CGSize.init(width: 200, height: 200))
self.addChild(myNode!)
}}

how to stop func update(_ currentTime: TimeInterval) in Spritekit

override func update(currentTime: NSTimeInterval) {
// ...
}
how to stop currentTime in update method?
I want to calculate time since game starts.
when player clicks pause button, current scene is paused successfully.
but, currentTime in update is moving.
When the scene is paused, update will not be called, try this code:
class GameScene: SKScene {
override func didMove(to view: SKView) {
let wait = SKAction.wait(forDuration: 2)
let pause = SKAction.run { self.isPaused = true }
self.run(SKAction.sequence([wait, pause]))
}
override func update(_ currentTime: TimeInterval) {
print("update")
}
}
"update" will be printed for the first 2 seconds only. This proves that pausing the scene will stop update. You are probably not pausing the scene, but a node in the scene on which all actions are run.
In addition to that, implementing a pause screen by pausing the scene isn't such a good idea because the user can't leave the pause screen by tapping if the scene is paused. Also, you can't show cool animations in the pause screen.
What I usually do is to have a background node. Every game sprite are added as a child of the background node. In the pause screen, the background pauses but the scene is still running. I then add the pause screen spirte as a direct child of the scene so that you can still interact with the pause screen.
You don't really need to "stop" the update method. You just need to check whether the game is paused in the method. If it is, return immediately.
If you want to measure how much time has passed since the player started the game, you can also use Date objects. Create a Date at the start of the game and another Date when the user pauses. Call the timeIntervalSince method and there you go!
I've implemented pause like the following, I think the code is self-explenatory but I've added some comments:
// BaseScene inherits from SKScene but adds some methods, e.g. for dealing with
// controller input.
class GameScene : BaseScene {
// The previous update time is used to calculate the delta time.
// The delta time is used to update the Game state.
private var lastUpdateTime: NSTimeInterval = 0
override func update(currentTime: CFTimeInterval) {
// NOTE: After pausing the game, the last update time is reset to
// the current time. The next time the update loop is entered,
// a correct delta time can then be calculated using the current
// time and the last update time.
if lastUpdateTime <= 0 {
lastUpdateTime = currentTime
} else {
let deltaTime = currentTime - lastUpdateTime
lastUpdateTime = currentTime
Game.sharedInstance.update(deltaTime)
}
}
// A method on BaseScene that is called when the player presses pause
// on controller.
override func handlePausePress(forPlayer player: PlayerIndex) {
paused = true
lastUpdateTime = 0
}
}
My Game singleton only makes use of the delta time (time difference since last call) when updating the game state.

How to properly manage groups of sprites onscreen

I'm building my first game in Swift and I wanted to know how to go about handling multiple on screen sprites at once. My game pushes sprites on to screen with addChild continuously, so there are many active at once. I realized that I didn't have a proper way of simultaneously affecting all of them- like if I wanted to affect a physics property of all enemy sprites at once. So far I created an empty array var enemySprites = [enemyType1]() at the begining of GameScene and have been appending the sprite instances to it instead of using addChild to draw them directly to the scene. However, I'm not able to simply loop through and draw them to screen with:
for enemy in enemySprites{
addChild(enemy)
}
this bit of code is in the override func update(currentTime: CFTimeInterval) function, so maybe I'm just misplacing it? Any help on how to go about this would be great!
Sam,
Here's some sample code to update enemies when your lives reach 0:
First, we set a property observer on the lives property so we can call a function when you lose all lives:
var lives = 3 {
didSet {
if lives == 0 {
updateEnemies()
}
}
And then a function to enumerate over all the enemies and change each one's velocity to (0, 0):
func update enemies() {
enumerateChildNodesWithName("type1") {
node, stop in
let enemy = node as! SKSpriteNode
enemy.physicsBody?.velocity = CGVector(dx: 0, dy: 0)
}
}
Instead of use update method, you could use a timer. From sources:
public class func scheduledTimerWithTimeInterval(ti: NSTimeInterval, target aTarget: AnyObject, selector aSelector: Selector, userInfo: AnyObject?, repeats yesOrNo: Bool) -> NSTimer
So if you follow Apple guide, it will be for example:
NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: Selector("spawnAlien:"), userInfo: myParameter, repeats: true)
func spawnAlien(timer : NSTimer) {
if let myUserInfo = timer.userInfo {
print(myUserInfo) // a parameters passed to help you to the alien creation
}
timer.invalidate()
}
BUT according to Whirlwind I agree with him and with LearnCocos2d work, sprite-kit don't work well with timers (as explained in the link by LearnCocos2d) and the better way, especially as you say you develop your first game, it's to use SKAction, a combination of actions to achieve the similar behavior obtained by NSTimer.
I've think about a function or an extension, let me know if it's work as expected:
extension SKAction {
class func scheduledTimerWithTimeInterval(time:NSTimeInterval, selector: Selector, repeats:Bool)->SKAction {
let call = SKAction.customActionWithDuration(0.0) { node, _ in
node.performSelector(selector)
}
let wait = SKAction.waitForDuration(time)
let seq = SKAction.sequence([wait,call])
let callSelector = repeats ? SKAction.repeatActionForever(seq) : seq
return callSelector
}
}
Usage:
let spawn = SKAction.scheduledTimerWithTimeInterval(time, selector: #selector(GenericArea.spawnAlien), repeats: true)
self.runAction(spawn,withKey: "spawnAlien")

SpriteKit: run action while scene is paused

I have a button to pause the game on my code. What I want is that pausing the game with that button makes a message that says "Paused" to appear. However, since the scene is paused, the message does not appear.
What I have right now is a SKLabelNode with the alpha on 0.0 at the beginning and when the user pauses the game, it changes to 1.0 with fadeInWithDuration(). Then when the user presses the button again, it changes back to 0.0 with fadeOutWithDuration(). The problem is that the SKAction with fadeInWithDuration() does not run when the scene is paused.
How could I achieve this?
The best way, one Apple also uses in "DemoBots", is to create a world node that you pause instead of the scene.
Create a worldNode property
class GameScene: SKScene {
let worldNode = SKNode()
}
add it to the scene in didMoveToView
addChild(worldNode)
and than add everything you need paused to the worldNode. This includes actions that are normally run by the scene (eg. timers, enemy spawning etc)
worldNode.addChild(someNode)
worldNode.run(someSKAction)
Than in your pause func you say
worldNode.isPaused = true
physicsWorld.speed = 0
and in resume
worldNode.isPaused = false
physicsWorld.speed = 1
You can also add an extra check in your Update function if you have stuff there that you want to ignore when paused.
override func update(_ currentTime: CFTimeInterval) {
guard !worldNode.isPaused else { return }
// your code
}
This way it's much easier to add your paused label or other UI when your game is paused because you haven't actually paused the scene. You can also run any action you want, unless that action is added to the worldNode or to a child of worldNode.
Hope this helps
Instead of pausing the scene, you could layer some nodes your scene like this
SKScene
|--SKNode 1
| |-- ... <--place all scene contents here
|--SKNode 2
| |-- ... <--place all overlay contents here
Then when you want to pause the game, you pause only SKNode 1.
This allows node SKNode 2 to continue to run, so you can do things like have animations going, and have a button that unpauses the scene for you, without having the need to add some non Sprite Kit object into the mix.
A quick workaround would be to pause your game after the SKLabelNode appears on screen:
let action = SKAction.fadeOutWithDuration(duration)
runAction(action) {
// Pause your game
}
Another option would be to mix UIKit and SpriteKit and inform the ViewController back that it needs show this label.
class ViewController: UIViewController {
var gameScene: GameScene!
override func viewDidLoad() {
super.viewDidLoad()
gameScene = GameScene(...)
gameScene.sceneDelegate = self
}
}
extension ViewController: GameSceneDelegate {
func gameWasPaused() {
// Show your Label on top of your GameScene
}
}
protocol GameSceneDelegate: class {
func gameWasPaused()
}
class GameScene: SKScene {
weak var sceneDelegate: GameSceneDelegate?
func pauseGame() {
// Pause
// ...
sceneDelegate?.gameWasPaused()
}
}
So you want to pause the game AFTER the action execution has completed.
class GameScene: SKScene {
let pauseLabel = SKLabelNode(text: "Paused")
override func didMoveToView(view: SKView) {
pauseLabel.alpha = 0
pauseLabel.position = CGPoint(x: CGRectGetMaxY(self.frame), y: CGRectGetMidY(self.frame))
self.addChild(pauseLabel)
}
func pause(on: Bool) {
switch on {
case true: pauseLabel.runAction(SKAction.fadeInWithDuration(1)) {
self.paused = true
}
case false:
self.paused = false
pauseLabel.runAction(SKAction.fadeOutWithDuration(1))
}
}
}
I would add the label with
self.addChild(nameOfLabel)
and then pause the game with
self.scene?.paused = true
This should all go in the if pauseButton is touched portion of your code.

Sprite moves two places after being paused and then unpaused

I am having trouble figuring out the solution to this and am starting to get very frustrated with it.
I have a pause button and an unpause button in my game scene to allow the player to pause the game, which is the following code
else if (node == pauseButton) {
pauseButton.removeFromParent()
addChild(unpauseButton)
addChild(restartButton)
self.runAction (SKAction.runBlock(self.pauseGame))
}
func pauseGame(){
pauseButton.hidden = true
unpauseButton.hidden = false
scene!.view!.paused = true // to pause the game
}
Problem
The problem is that when I pause the game then unpause the game my player sprite seems to move two spaces forward automatically
I also have a tap and swipe gesture that allows me to move the player up left right and down when I tap anywhere on the screen.
func tapUp(){
let amountToMove:CGFloat = levelUnitHeight
let move:SKAction = SKAction.moveByX(0, y: amountToMove, duration: 0.1)
menubutton.hidden = true
settingsButton.hidden = true
highscoreLabel.hidden = true
pauseButton.hidden = false
thePlayer.runAction(move)
}
func swipedRight(){
let amountToMove:CGFloat = levelUnitHeight
let move:SKAction = SKAction.moveByX(amountToMove, y: 0, duration: 0.1)
thePlayer.runAction(move) // links the action with the players
}
As the member above me said the player is not really moving 2 spaces.
Also you should maybe change your strategy when pausing your game, because pausing the scene.view makes it very hard to add SpriteKit elements afterwards.
I think a better way is to create a worldNode in your GameScene and add all the sprites that need to be paused to that worldNode. It basically gives you more flexibility pausing a node rather than the whole scene.
First create a world node property
let worldNode = SKNode()
and add it to the scene in ViewDidLoad
addChild(worldNode)
Than add all the sprites you need paused to the worldNode
worldNode.addChild(sprite1)
worldNode.addChild(sprite2)
...
Create a global enum for your game states
enum GameState {
case Playing
case Paused
case GameOver
static var current = GameState.Playing
}
Than make a pause and resume func in your game scene
func pause() {
GameState.current = .Paused
// show pause menu etc
}
func resume() {
GameState.current = .Playing
self.physicsWorld.speed = 1
worldNode.paused = false
}
And finally add the actual pause code to your updateMethod. This way it will not resume the game even if spriteKit itself tries to resume (e.g. reopened app, dismissed alert etc)
override func update(currentTime: CFTimeInterval) {
if GameState.current == .Paused {
self.physicsWorld.speed = 0
worldNode.paused = true
}
}
In regards to your tapGesture recogniser, in the method that gets called after a tap you can add this before the rest of the code
guard GameState.current != .Paused else { return }
....
Hope this helps

Resources