I've read everything I could find on this topic and still cant figure out my issue. I have tried pausing my game in every area of appdelegate
func applicationWillResignActive(application: UIApplication!) {
NSNotificationCenter.defaultCenter().postNotificationName("pauseGameScene", object: self)
}
func applicationDidEnterBackground(application: UIApplication!) {
NSNotificationCenter.defaultCenter().postNotificationName("pauseGameScene", object: self)
}
func applicationWillEnterForeground(application: UIApplication!) {
NSNotificationCenter.defaultCenter().postNotificationName("pauseGameScene", object: self)
}
func applicationDidBecomeActive(application: UIApplication!) {
NSNotificationCenter.defaultCenter().postNotificationName("pauseGameScene", object: self)
}
In my controller:
override func viewDidLoad() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: "pauseGame:", name: "pauseGameScene", object: nil)
}
func pauseGame(){
self.skView.paused = true
self.skView.scene!.paused = true
}
i know pauseGame works because if i toggle it with a button in my scene, it will stop the game. Even if I pause my skview and scene directly after they are loaded in the controller.. the game will not be paused on launch. It's easy to pause the game when I'm in-game. but for some reason whenever i exit and resume the app, the game will un-pause itself.
i notice if i get hacky and use some kind of delay.. i can get it to work. but obviously this is very stupid.. i just need to know where the game is unpausing itself!
func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}
func pauseGame(sender: UIButton!){
delay(2) {
println("blah")
self.skView.paused = true
self.skView.scene!.paused = true
}
}
Here's a way to keep the view paused after returning from background mode.
Xcode 7 (see below for Xcode 8 instructions)
In the storyboard,
1) Change the class of the view to MyView
In the View Controller,
2) Define an SKView subclass with a boolean named stayPaused
class MyView: SKView {
var stayPaused = false
override var paused: Bool {
get {
return super.paused
}
set {
if (!stayPaused) {
super.paused = newValue
}
stayPaused = false
}
}
func setStayPaused() {
if (super.paused) {
self.stayPaused = true
}
}
}
3) Define the view as MyView
4) Add a notifier to set the stayPaused flag
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let scene = GameScene.unarchiveFromFile("GameScene") as? GameScene {
// Configure the view.
let skView = self.view as MyView
NSNotificationCenter.defaultCenter().addObserver(skView, selector:Selector("setStayPaused"), name: "stayPausedNotification", object: nil)
In the App Delegate,
5) Post a notification to set the stay paused flag when the app becomes active
func applicationDidBecomeActive(application: UIApplication) {
NSNotificationCenter.defaultCenter().postNotificationName("stayPausedNotification", object:nil)
}
Xcode 8
In the storyboard,
1) Change the class of the view from SKView to MyView
In the View Controller,
2) Define an SKView subclass with a boolean named stayPaused
class MyView: SKView {
var stayPaused = false
override var isPaused: Bool {
get {
return super.isPaused
}
set {
if (!stayPaused) {
super.isPaused = newValue
}
stayPaused = false
}
}
func setStayPaused() {
if (super.isPaused) {
self.stayPaused = true
}
}
}
3) Define the view as MyView
4) Add a notifier to set the stayPaused flag
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! MyView? {
NotificationCenter.default.addObserver(view, selector:#selector(MyView.setStayPaused), name: NSNotification.Name(rawValue: "stayPausedNotification"), object: nil)
In the App Delegate,
5) Post a notification to set the stay paused flag when the app becomes active
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "stayPausedNotification"), object: nil)
}
Here is an Obj-C example completely based on answer given by 0x141E, without subclassing a view. In my case, because I have internal game states, like finished, paused and started, I had to make an additional check in overridden setPaused: method. But this worked flawlessly for me and definitely it is the one possible way to go.
AppDelegate.m
- (void)applicationDidBecomeActive:(UIApplication *)application {
[[NSNotificationCenter defaultCenter] postNotificationName:#"stayPausedNotification" object:nil];
}
GameScene.m
#interface GameScene()
#property (nonatomic, assign) BOOL stayPaused;
#end
#implementation GameScene
//Use initWithCoder: if you load a scene from .sks file, because initWithSize is not called in that case.
-(instancetype)initWithSize:(CGSize)size{
if(self = [super initWithSize:size]){
_stayPaused = NO;
//register to listen for event
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(setStayPaused)
name:#"stayPausedNotification"
object:nil ];
}
return self;
}
-(void)setStayPaused{
self.stayPaused = YES;
}
-(void)setPaused:(BOOL)paused{
if (!self.stayPaused) {
[super setPaused:paused];
}
self.stayPaused = NO;
}
-(void)willMoveFromView:(SKView *)view{
[[NSNotificationCenter defaultCenter] removeObserver:self name:#"stayPausedNotification" object:nil];
}
#end
I recently encountered the same issue. The stayPaused solution works fine for me. Thanks guys:)
However, I think the mechanism you use to set stayPaused is NOT robust. Just by calling setStayPaused once on App becoming active is not enough, engineers in apple may change the code of Sprite Kit and call setPaused(false) MORE THAN ONCE on activating (In fact, I think they just did!). The second setPaused(false) will resume the game as you set stayPaused back to false on the first call.
My suggestion is to directly set the stayPaused property yourself in your pauseGame/resumeGame functions, and remove the "self.stayPaused = NO;" from setPaused function. In this way, the default behavior of SpriteKit can do whatever it likes, but the game will stay paused as long as we want.
Related
I'm creating a Swift project for a high school programming class. I can't seem to figure out this problem, and everyone else in my class doesn't seem to have any ideas.
To start, I created a new Swift project, and chose a game format.
I then used some basic code to make the first level for my game, a maze game where the maze moves around instead of the ball based on how the user tilts the device.
This is my GameScene.swift:
import SpriteKit
import CoreMotion
var accelupdateinterval = 0.1
var accelmultiplier = 15.0
class GameScene: SKScene {
let manager = CMMotionManager()
override func didMoveToView(view: SKView) {
manager.startAccelerometerUpdates()
manager.accelerometerUpdateInterval = accelupdateinterval
manager.startAccelerometerUpdatesToQueue(NSOperationQueue.mainQueue()){
(data, error) in
self.physicsWorld.gravity = CGVectorMake(CGFloat((data?.acceleration.x)!) * CGFloat(accelmultiplier), CGFloat((data?.acceleration.y)!) * CGFloat(accelmultiplier))
}
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
}
I want to have a main menu that the app opens into, which is mainMenu.storyboard:
I successfully have the app launching into the mainMenu.storyboard (and the level physics work well when I've tested the level1.sks), but I'm having trouble figuring out how to segue.
GOAL: I want people to be segued into the level1.sks (and the levels that I add later), when they tap the corresponding image in mainMenu.storyboard.
I can't use the method of adding a Storyboard Reference to segue it, as the Storyboard Reference won't let me choose level1.sks.
I'd also love to find out how to send users back to the main menu when the player icon touches the goal (the blue thing up near the top in this screenshot):
So to do this I think the best approach is to create another ViewController subclass, maybe named LauncherViewController, which will present your SKScene. Then in your storyboard add this viewController and have your menu segue to it on an image press.
Here is a start for the LauncherViewController
class LauncherViewController: UIViewController {
var gameScene: GameScene!
override func viewDidLoad() {
}
override func viewWillAppear() {
presentGameScene()
}
func presentGameScene(){
let skView = self.view as! SKView
skView.showsFPS = false
skView.showsNodeCount = false
skView.ignoresSiblingOrder = true
gameScene.size = skView.bounds.size
gameScene.scaleMode = .AspectFill
skView.presentScene(gameScene)
}
}
Where in your menu controller you have a prepareForSegue like this:
override func prepareForSegue(segue: UIStoryBoardSegue, sender: AnyObject?) {
if segue.identifier == "yourSegue" {
let destinationViewController = segue.destinationViewController as! LauncherViewController
destinationViewController.gameScene = GameScene()
}
to have your LauncherViewController dismiss the gameScene when the user finishes the maze, use a delegate pattern. So in GameScene add a protocol above your class
protocol GameDelegate {
func gameFinished()
}
have your LauncherViewController conform to this delegate and set the gameScene's delegate variable to self (see below)
class LauncherViewController: UIViewController, GameDelegate {
var gameScene: GameScene!
override func viewDidLoad() {
gameScene.delegate = self
}
override func viewWillAppear() {
presentGameScene()
}
func presentGameScene(){
let skView = self.view as! SKView
skView.showsFPS = false
skView.showsNodeCount = false
skView.ignoresSiblingOrder = true
gameScene.size = skView.bounds.size
gameScene.scaleMode = .AspectFill
skView.presentScene(gameScene)
}
func gameFinished(){
// this forces LauncherViewController to dismiss itself
dismissViewControllerAnimated(true, completion: nil)
}
}
add a variable in GameScene to hold the delegate (LauncherViewController) and add a function that calls the delegate function. You will also need to add the logic to know when the game is over as I haven't done that.
class GameScene: SKScene {
let manager = CMMotionManager()
var delegate: GameDelegate!
override func didMoveToView(view: SKView) {
manager.startAccelerometerUpdates()
manager.accelerometerUpdateInterval = accelupdateinterval
manager.startAccelerometerUpdatesToQueue(NSOperationQueue.mainQueue()){
(data, error) in
self.physicsWorld.gravity = CGVectorMake(CGFloat((data?.acceleration.x)!) * CGFloat(accelmultiplier), CGFloat((data?.acceleration.y)!) * CGFloat(accelmultiplier))
}
}
func gameOver(){
delegate.gameFinished()
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
// it's probably easiest to add the logic for a gameOver here
if gameIsOver {
gameOver()
}
}
}
There will probably be some mistakes in here as I wrote this on my phone so just comment below for anything you are unsure about or doesn't work.
I feel like I've looked everywhere and I can't figure it out. I'm trying to show a banner ad at the bottom of my menu screen like so:
But after I display my "game over" screen (which also has an ad on it) The main menu looks like this:
Any help would be greatly appreciated. I call the ads by using the NSNotificationCenter and the GameOver SKScene calls presentScene to get back to the Main Menu. I assume it has something to do with frame size in that call, but I do not know how to fix it.
Inside my GameViewController:
override func viewDidLoad() {
//code
loadAds()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "showiAdBanner", name: "showiAdBanner", object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "hideiAdBanner", name: "hideiAdBanner", object: nil)
}
func bannerViewDidLoadAd(banner: ADBannerView!) {
adBannerView.hidden = false
}
func bannerView(banner: ADBannerView!, didFailToReceiveAdWithError error: NSError!) {
adBannerView.hidden = true
}
func loadAds(){
adBannerView = ADBannerView(frame: CGRect.zeroRect)
adBannerView.center = CGPoint(x: adBannerView.center.x, y: view.bounds.size.height - adBannerView.frame.size.height / 2)
adBannerView.delegate = self
adBannerView.hidden = true
adBannerView.layer.zPosition = 10
view.addSubview(adBannerView)
}
func showiAdBanner() {
if adBannerView.bannerLoaded {
adBannerView.hidden = false
}
}
func hideiAdBanner() {
adBannerView.hidden = true
}
Inside SpriteKit "Start" Scene:
override func didMoveToView(view: SKView) {
//code
NSNotificationCenter.defaultCenter().postNotificationName("showiAdBanner", object: nil)
}
I ended up figuring out a solution for my situation. I simply have a different SKScene load up, wait a second, then calls my other one. This way, the menu always looks the same.
I have the following code to pause game scene:
class GameProcessScene: SKScene {
...
var onPause: Bool = false {
willSet {
self.paused = newValue
self.view?.paused = newValue
}
}
...
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch in touches {
let location = touch.locationInNode(self)
let possiblePauseNode = self.nodeAtPoint(location)
if (possiblePauseNode.name == "pauseButton") {
if (self.paused) {
onPause = false
} else {
onPause = true
}
return
}
...
}
}
It works to pause by pressing button. But i also want to pause game when it appears from background. I am trying to do this from ViewController class:
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "pauseGameScene", name: UIApplicationWillEnterForegroundNotification, object: nil)
}
func pauseGameScene() {
let view = self.view as SKView
let scene = view.scene
switch scene {
case is MainMenuScene:
println("MainMenu")
case let game as GameProcessScene:
game.onPause = true
default:
println("default")
}
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
But it doesnt work when game appears from background.
Can anybody help? Thanks!
UPD1: Also tried UIApplicationWillResignActiveNotification, UIApplicationDidEnterBackgroundNotification but game.onPause = true still doesnt work. It changes "SKScene.paused" property to true, but the actions are still executing when game enters foreground, even game scene was paused before it goes background.
UPD2: I moved observers and selectors from ViewController class to GameProcessScene: SKScene class - same effect, "paused" property changes, but in fact gameplay is continuing.
I solved my problem by creating a property called isPaused and overriding the paused property to always follow the isPaused property. Note : This disables the SKSCene from automatically pausing when going to background mode. You have to manually set the isPaused property. So only use it in scenes where you actually need the pause mode on resume.
class GameScene: SKScene,SKPhysicsContactDelegate {
// Initializers
var isPaused : Bool = false {
didSet {
self.paused = isPaused
}
}
override var paused : Bool {
get {
return isPaused
}
set {
super.paused = isPaused
}
}
}
In ViewController
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "pauseGameScene", name: UIApplicationWillResignActiveNotification, object: nil)
}
func pauseGameScene() {
println("will become inactive")
let view = self.view as SKView
let scene = view.scene
switch scene {
case let game as GameScene:
game.isPaused = true
default:
println("default")
}
}
Did you try put your code into
override func viewWillAppear(animated: Bool) {
//Put your pauseGameScene(); code here
}
This method called before your view appears every times. The viewDidLoad() run only once after your instance is initialized.
I have been creating a game in sprite kit using swift and have encountered a problem. I have two view controllers, each with one scene and one transitions to the other modally. This all works perfectly first time round, but then when i return to the first view controller and then go to the second again, i have using double the memory. This gives me the impression that nothing is being deallocated, but the objects are rather reallocated every time I transition to the scene. I ran the app in instruments and got the same result. In the below image i moved from one scene to the next, and then back to the first one again and yet it appears to reallocate the first scene and yet not clear any memory. As the dealloc method is unused now, i don't see how i can fix this. I will post the code to the first view controller below so you can have a look at it. Thanks a lot.
import UIKit
import SpriteKit
class SelectionViewController: UIViewController {
var selectionScene:SelectionScene?
var currentRocketName = ""
#IBOutlet var playButton: UIButton
override func viewDidLoad() {
super.viewDidLoad()
if let selectionScene = SelectionScene.unarchiveFromFile("SelectionScene") as? SelectionScene {
// Configure the view.
let skView = self.view as SKView
skView.showsFPS = true
skView.showsNodeCount = true
skView.multipleTouchEnabled = false
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
selectionScene.scaleMode = .ResizeFill
selectionScene.viewController = self
skView.presentScene(selectionScene)
NSNotificationCenter.defaultCenter().addObserver(selectionScene, selector: "spinnerChanged", name: "spinnerValueChanged", object: nil)
NSNotificationCenter.defaultCenter().addObserver(selectionScene, selector: "productBought", name: "ProductBought", object: nil);
NSNotificationCenter.defaultCenter().addObserver(selectionScene, selector: "manageErrorInPurchase", name: "ErrorOccured", object: nil)
}
}
override func shouldAutorotate() -> Bool {
return true
}
override func viewDidAppear(animated: Bool) {
}
#IBAction func playButtonPressed(sender: UIButton) {
self.performSegueWithIdentifier("moveToGame", sender: nil)
}
override func prefersStatusBarHidden() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> Int {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return Int(UIInterfaceOrientationMask.AllButUpsideDown.toRaw())
} else {
return Int(UIInterfaceOrientationMask.All.toRaw())
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override func viewDidUnload() {
NSNotificationCenter.defaultCenter().removeObserver(selectionScene)
}
override func viewDidDisappear(animated: Bool) {
NSNotificationCenter.defaultCenter().removeObserver(selectionScene)
}
override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
if segue.identifier == "moveToGame" {
let destController = segue.destinationViewController as GameViewController
destController.rocketTexture = SKTexture(imageNamed: self.currentRocketName)
}
}
}
Both selectionScene and currentRocketName are passed to the viewController as soon as they are loaded into the view
I'm not familiar with Swift yet, so I'll give you examples in Objective-C.
Create an IBOutlet for skView. When you are going to present another ViewController, remove skView from it's superview and nil it out:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Need to deallocate GameScene (if game is not paused)
[self.skView removeFromSuperview];
self.skView = nil;
....
}
Don't forget to add skView back to the ViewController's view, when ViewController is getting loaded:
if (!self.skView.window) {
[self.view addSubview:self.skView];
}
To easily check if SKScene was deallocated or not, add this method to it:
- (void)dealloc {
NSLog(#"GAME SCENE DEALLOCATED");
}
I had a similar issue.
Turns out I had created a strong reference by having an SKScene instance as a delegate in another class. After declaring each property of SKScene type or UIView type as weak my issue was resolved:
weak var skScene:SKScene!
Is it possible to enable/disable rotation parameter for current screen or this property is for all application?
Sure:
- (BOOL)shouldAutorotate
{
return YES;
}
- (NSUInteger)supportedInterfaceOrientations
{
//Choose your available orientation, you can also support more tipe using the symbol |
//e.g. return (UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight)
return (UIInterfaceOrientationMaskPortrait);
}
If you have multiple ViewControllers within a NavigationController, and you wish to disable rotation in one of them only, you need to set and control rotation within ApplicationDelegate. Here is how to do it in Swift...
In AppDelegate.swift:
class AppDelegate: UIResponder, UIApplicationDelegate {
var blockRotation: Bool = false
func application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) -> Int {
if (self.blockRotation) {
println("supportedInterfaceOrientations - PORTRAIT")
return Int(UIInterfaceOrientationMask.Portrait.rawValue)
} else {
println("supportedInterfaceOrientations - ALL")
return Int(UIInterfaceOrientationMask.All.rawValue)
}
}
In the ViewController that you want to block rotation, add UIApplicationDelegate to your class...
class LoginViewController: UIViewController, UITextFieldDelegate, UIApplicationDelegate {
and then create a reference to the AppDelegate...
var appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
In viewDidLoad, set appDelegate.blockRotation = true:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
appDelegate.blockRotation = true
}
In viewWillAppear, set the orientation to force the device to the chosen orientation (Portrait in this example):
override func viewWillAppear(animated: Bool) {
let value = UIInterfaceOrientation.Portrait.rawValue
UIDevice.currentDevice().setValue(value, forKey: "orientation")
}
Then in viewWillDisappear, or in prepareForSegue, set appDelegate.blockRotation = false:
override func viewWillDisappear(animated: Bool) {
appDelegate.blockRotation = false
}
This will block rotation in the one view controller within a Navigation Controller that contains multiple ViewControllers. Hope this helps.