Passing data between SpriteKit scenes (Swift) - ios

I have been following an old Ray Wenderlich tutorial: https://www.raywenderlich.com/1514-introduction-to-the-sprite-kit-scene-editor and have encountered a problem. I have run up against this problem numerous times and even found a "solution" on the stackoverflow site: Passing Data Between Scenes (SpriteKit)
This is the same code as the tutorial (with a few tweaks as it was written in Swift 2). I feel it a very simply elegant solution and not full of code using NSdefaults etc.
This is the call to the new scene, please note the soundToPlay object:
func gameOver(didWin: Bool) {
let menuScene = MenuScene(size: self.size)
menuScene.scaleMode = .aspectFill
menuScene.soundToPlay = didWin ? "fear_win.mp3" : "fear_lose.mp3"
let transition = SKTransition.flipVertical(withDuration: 1.0)
self.view?.presentScene(menuScene, transition: transition)
}
And this is the scene I am calling:
import SpriteKit
class MenuScene: SKScene {
var soundToPlay: String!
override func sceneDidLoad() {
self.backgroundColor = SKColor(red: 0, green:0, blue:0, alpha: 1)
// Setup label
let label = SKLabelNode(fontNamed: "AvenirNext-Bold")
label.text = "Press anywhere to play again!"
label.fontSize = 55
label.horizontalAlignmentMode = .center
label.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
addChild(label)
// Play sound
if let soundToPlay = soundToPlay {
run(SKAction.playSoundFileNamed(soundToPlay, waitForCompletion: false))
}
}
The data is never passed and the object has a value of nil
I suspect it's something that has changed in Swift as the tutorial is very old as is the suggested "solution" on the previous stackoverflow question.
If that is the case, what is now the best way to achieve this?

The problem with your sound is that you are setting the variable after you call sceneDidLoad (this happens during init), so the sound will not play.
To fix this, do this code in your didMove method:
override func didMove(to view: SKView) {
guard let soundToPlay = soundToPlay else {fatalError("Unable to find soundToPlay")}
run(SKAction.playSoundFileNamed(soundToPlay, waitForCompletion: false))
}
The actual issue to your crashing is not visible here.
SKTransitions are bugged when you use SKLightNode, so by calling a scene with a transition, it is going to crash. SKTransitions have always been bugged with the introduction of Metal, so the only thing you can do is report it to apple and pray they do not tell you it is intentional.
As of now, keep track of all your light sources, and when you transition, set the isEnabled property to false.
You may need to create a screen shot of your scene prior to disabling the light source to not have it look silly.

What you are doing is, as you reload the scene (let menuScene = MenuScene(size: self.size)) you are resetting all the variables that were defined.
What you could do is use UserDefaults:
func gameOver(didWin: Bool) {
let menuScene = MenuScene(size: self.size)
menuScene.scaleMode = .aspectFill
let soundToPlay = didWin ? "fear_win.mp3" : "fear_lose.mp3"
UserDefaults.standard.set(soundToPlay, forKey: "soundToPlay")
let transition = SKTransition.flipVertical(withDuration: 1.0)
self.view?.presentScene(menuScene, transition: transition)
}
And:
import SpriteKit
class MenuScene: SKScene {
var soundToPlay: String!
override func sceneDidLoad() {
self.backgroundColor = SKColor(red: 0, green:0, blue:0, alpha: 1)
// Setup label
let label = SKLabelNode(fontNamed: "AvenirNext-Bold")
label.text = "Press anywhere to play again!"
label.fontSize = 55
label.horizontalAlignmentMode = .center
label.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
addChild(label)
// Play sound
if let soundToPlay = UserDefaults.standard.string(forKey: "soundToPlay") {
run(SKAction.playSoundFileNamed(soundToPlay, waitForCompletion: false))
}
}
Then, put this in your AppDelegate's applicationWillTerminate function:
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
UserDefaults.standard.removeObject(forKey: "soundToPlay")
}
If you do not put that in the AppDelegate then soundToPlay will remain after closing the app altogether.
I don't know if UserDefaults is the best way to do this, but I've struggled with this when I was making my iOS app and I just did this. So if anyone else has a better answer, please notify me as well!

Related

Memory increase when returning to GameScene

I'm creating a game which consists of multiple scenes but my problem is in the game scene. When I start playing and then return to the menu scene I notice that memory isn't being freed, instead memory keeps increasing every time I go to the game scene and eventually it crashes.
I've already tried to remove all actions and children from self in the function 'willMove', like this:
override func willMove(from view: SKView) {
self.removeAllActions()
self.removeAllChildren()
}
But it did nothing.
I believe my problem is that I have too many animations made with SKActions like these:
//example 1, whiteCircle is an SKShapeNode
whiteCircle.run(.sequence([.scale(to: 1.5, duration: 0.5), .removeFromParent()]))
//example 2, SKAction.colorize doesn't work with SKLabels so I did this
let color1 = SKAction.run {
label.fontColor = .red
}
let color2 = SKAction.run {
label.fontColor = .yellow
}
let color3 = SKAction.run {
label.fontColor = .blue
}
let color4 = SKAction.run {
label.fontColor = .green
}
let wait = SKAction.wait(forDuration: 0.2)
label.run(SKAction.repeatForever(SKAction.sequence([color1, wait, color2, wait, color3, wait, color4, wait])))
//example 3, this is to refresh the label's text and change it's color
coinLabel.run(SKAction.sequence([.wait(forDuration: 3.25), .run {
self.coinLabel.fontColor = .yellow
self.coinLabel.text = "\(UserDefaults.standard.integer(forKey: "coins"))"
}]))
I'm using lots of images as SKTextures too, used like this:
coinIcon.texture = SKTexture(image: UIImage(named: "coinImage")!)
My variables are all declared like this in the GameScene class:
var ground = SKSpriteNode()
var ceiling = SKSpriteNode()
var character = SKSpriteNode()
var scoreLabel = SKLabelNode()
var coinLabel = SKLabelNode()
var coinIcon = SKLabelNode()
I think I may be creating a strong reference cycle, but I don't know where or how to identify it, I'm new to SpriteKit so sorry if my question seems dumb. Any help is very much appreciated.
It's hard to actually give you a solution not being able to debug with you and see how you are managing references in your Game, but I've encoutered this issue and I was able to solve it using those two things:
deinit {
print("(Name of the SKNode) deinit")
}
Using this, I could find which objects the arc could not remove reference.
let color4 = SKAction.run { [weak self] in
self?.label.fontColor = .green
}
This one helped me clean all strong references in my Game. For many of us, it’s best practice to always use weak combined with self inside closures to avoid retain cycles. However, this is only needed if self also retains the closure. More Information.

SpriteKit Game Losing FPS after main scene restart

I'm creating a game using Sprite Kit, and I have an issue where the game slowly loses fps with every reiteration of the my main scene.
I've tried to use Xcode's Instruments, but have gotten no closer to the root of my issue.
This is my function to end the game and present the Game Over Scene:
func endGame(){
let endGameScene = SKScene(fileNamed: "EndGameScene" ) as! EndGameScene
endGameScene.score = score
//let transition = SKTransition.push(with: SKTransitionDirection.right, duration: 0.5)
self.view?.presentScene(endGameScene)
transitionNode = SKSpriteNode(color: UIColor.white, size: self.frame.size)
transitionNode.zPosition = 5
self.addChild(transitionNode)
transitionNode.run(SKAction.fadeIn(withDuration: 0.3))
gameOver = true
}
This is my function to present the Game Scene as a result of the
restart button being tapped:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
if let location = touch?.location(in: self) {
let nodeArray = self.nodes(at: location)
if(nodeArray.first?.name == "replayButton"){
self.view?.presentScene(gameScene)
transitionNode = SKSpriteNode(color: UIColor.white, size: self.frame.size)
transitionNode.zPosition = 5
self.addChild(transitionNode)
transitionNode.run(SKAction.fadeIn(withDuration: 0.3))
I also checked for leaks using instruments, but none come up. Similarly, the heaviest stack trace log shows no areas in my class file that took the most time, as shown here: (wasn't able to upload an image for some reason, so here's the link)
https://puu.sh/wtrMZ/1bb8c6542a.png
The game starts at a solid 60 fps, and then drops to 58, then to 57, then to 55, then to 54, and so on. I tried the deinit {} test, and the scene deallocates fine. I used the debugger to check my memory usage, and while it does go up ever so slightly, it barely breaks 50 mb in a single sitting. I also don't think it's a problem of removing unused nodes, because I make sure to remove them in every instance, but I could be wrong:
deinit
deinit {
self.removeAllChildren()
}
I am genuinely stumped, as I don't even know how to begin to fix this. Any help or constructive criticism is very much appreciated, thanks.
EDIT:
I never invalidated the two main timers in my Game Scene class. I simply invalidated them in my endGame function:
`func endGame(){
let endGameScene = SKScene(fileNamed: "EndGameScene" ) as! EndGameScene
endGameScene.score = score
//let transition = SKTransition.push(with: SKTransitionDirection.right, duration: 0.5)
self.view?.presentScene(endGameScene)
transitionNode = SKSpriteNode(color: UIColor.white, size: self.frame.size)
transitionNode.zPosition = 5
self.addChild(transitionNode)
transitionNode.run(SKAction.fadeIn(withDuration: 0.3))
pillarTimer.invalidate()
durationTimer.invalidate()
gameOver = true
}`
Fixed my issue perfectly.

How to make enemy have independent lives?

PLEASE HELP!!! I have been trying to figure this out for along time. I have searched the internet and i cannot find anything that will help me.
I am currently making a game in which you are a space ship in the middle and enemy ships are moving towards you and you have to shoot them. some enemies have different lives. for example: a red ship takes one shot to explode, the blue ship takes 3, etc. I have everything to work only the lives. for example: whenever a blue ship is called on to the screen i shoot it once so its life goes down to 2. but whenever a another blue ship is called the first blue ship has its life reset back to 3 again. Is there anyway I can make it so that whenever a ship looses lives it remains that way even if other ships are called ?
this is my ship function that gets called and adds enemy space ships onto the screen:
func VillainRight(){
let TooMuch = self.size.width
let point = UInt32(TooMuch)
life = 3
let VillainR = SKSpriteNode(imageNamed: "BlueVillain")
VillainR.zPosition = 2
VillainR.position = CGPoint(x: self.frame.minX,y: CGFloat(arc4random_uniform(point)))
//This code makes the villain's Zposition point towards the SpaceShip
let angle = atan2(SpaceShip.position.y - VillainR.position.y, SpaceShip.position.x - VillainR.position.x)
VillainR.zRotation = angle - CGFloat(M_PI_2)
let MoveToCenter = SKAction.move(to: CGPoint(x: self.frame.midX, y: self.frame.midY), duration: 15)
//Physics World
VillainR.physicsBody = SKPhysicsBody(rectangleOf: VillainR.size)
VillainR.physicsBody?.categoryBitMask = NumberingPhysics.RightV
VillainR.physicsBody?.contactTestBitMask = NumberingPhysics.Laser | NumberingPhysics.SpaceShip
VillainR.physicsBody?.affectedByGravity = false
VillainR.physicsBody?.isDynamic = true
VillainR.run(MoveToCenter)
addChild(VillainR)
}
This is the code that calls this function:
_ = Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(Level1.VillainRight), userInfo: nil, repeats: true)
I am using the spritekit in Swift.
Thank You Very Much in advance!
That is happening because life variable is declared as a property of a scene and it is not local to a specific node (enemy ship). You can solve this in a few ways... First way would be using node's userData property:
import SpriteKit
let kEnergyKey = "kEnergyKey"
class GameScene: SKScene, SKPhysicsContactDelegate {
override func didMove(to view: SKView) {
let blueShip = getShip(energy: 3)
let greenShip = getShip(energy: 2)
let redShip = getShip(energy: 1)
if let blueShipEnergy = blueShip.userData?.value(forKey: kEnergyKey) as? Int {
print("Blue ship has \(blueShipEnergy) lives left")
//hit the ship
blueShip.userData?.setValue(blueShipEnergy-1, forKey: kEnergyKey)
if let energyAfterBeingHit = blueShip.userData?.value(forKey: kEnergyKey) as? Int {
print("Blue ship has \(energyAfterBeingHit) lives left")
}
}
}
func getShip(energy:Int)->SKSpriteNode{
//determine which texture to load here based on energy value
let ship = SKSpriteNode(color: .purple, size: CGSize(width: 50, height: 50))
ship.userData = [kEnergyKey:energy]
return ship
}
}
This is what the docs say about userData property:
You use this property to store your own data in a node. For example,
you might store game-specific data about each node to use inside your
game logic. This can be a useful alternative to creating your own node
subclasses to hold game data.
As you can see, an alternative to this is subclassing of a node (SKSpriteNode):
class Enemy:SKSpriteNode {
private var energy:Int
//do initialization here
}

linking GameViewController.swift to GameScene.swift

I have created a UI elements on main.storyboard which i require to be hidden until the game is over and the once the player tap the screen to dismiss. Main.storyboard is linked to GameViewController therefor all my IBOutlets and IBActions are in there and all my game code is in GameScene. How can i link the view controller to the scene for that the popup image and buttons only appear when it is game over. Would greatly appreciate some help, I have been stuck on this for quite some time now.
This seems to be quite a common problem people have with SpriteKit games so lets go through the difference between SpriteKit games and UIKit apps.
When you make a regular UIKit app, e.g. YouTube, Facebook, you would use ViewControllers, CollectionViews, Views etc for each screen/menu that you see (Home screen, Channel screen, Subscription channel screen etc). So you would use UIKit APIs for this such as UIButtons, UIImageViews, UILabels, UIViews, UICollectionViews etc. To do this visually we would use storyboards.
In SpriteKit games on the other hand it works differently. You work with SKScenes for each screen that you see (MenuScene, SettingsScene, GameScene, GameOverScene etc) and only have 1 ViewController (GameViewController). That GameViewController, which has a SKView in it, will present all your SKScenes.
So we should add our UI directly in the relevant SKScenes using SpriteKit APIs such as SKLabelNodes, SKSpriteNodes, SKNodes etc. To do this visually we would use the SpriteKit scene level editor and not storyboards.
So the general logic would be to load your 1st SKScene as usual from the GameViewController and than do the rest from within the relevant SKScenes. Your GameViewController should basically have next to no code in it beyond the default code. You can also transition from 1 scene to another scene very easily (GameScene -> GameOverScene).
If you use GameViewController for your UI it will get messy really quickly if you have multiple SKScenes because UI will be added to GameViewController and therefore all SKScenes. So you would have to remove/show UI when you transition between scenes and it would be madness.
To add a label in SpriteKit it would be something like this
class GameScene: SKScene {
lazy var scoreLabel: SKLabelNode = {
let label = SKLabelNode(fontNamed: "HelveticaNeue")
label.text = "SomeText"
label.fontSize = 22
label.fontColor = .yellow
label.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
return label
}()
override func didMove(to view: SKView) {
addChild(scoreLabel)
}
}
To make buttons you essentially create a SKSpriteNode and give it a name and then look for it in touchesBegan or touchesEnded and run an SKAction on it for animation and some code after.
enum ButtonName: String {
case play
case share
}
class GameScene: SKScene {
lazy var shareButton: SKSpriteNode = {
let button = SKSpriteNode(imageNamed: "ShareButton")
button.name = ButtonName.share.rawValue
button.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
return button
}()
override func didMove(to view: SKView) {
addChild(shareButton)
}
/// Touches began
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
let node = atPoint(location)
if let nodeName = node.name {
switch nodeName {
case ButtonName.play.rawValue:
// run some SKAction animation and some code
case ButtonName.share.rawValue:
let action1 = SKAction.scale(to: 0.9, duration: 0.2)
let action2 = SKAction.scale(to: 1, duration: 0.2)
let action3 = SKAction.run { [weak self] in
self?.openShareMenu(value: "\(self!.score)", image: nil) // image is nil in this example, if you use a image just create a UIImage and pass it into the method
}
let sequence = SKAction.sequence([action1, action2, action3])
node.run(sequence)
default:
break
}
}
}
}
}
To make this even easier I would create a button helper class, for a simple example have a look at this
https://nathandemick.com/2014/09/buttons-sprite-kit-using-swift/
You can also check out Apple's sample game DemoBots for a more feature rich example.
This way you can have things such as animations etc in the helper class and don't have to repeat code for each button.
For sharing, I would actually use UIActivityController instead of those older Social APIs which might become deprecated soon. This also allows you to share to multiple services via 1 UI and you will also only need 1 share button in your app. It could be a simple function like this in the SKScene you are calling it from.
func openShareMenu(value: String, image: UIImage?) {
guard let view = view else { return }
// Activity items
var activityItems = [AnyObject]()
// Text
let text = "Can you beat my score " + value
activityItems.append(text as AnyObject)
// Add image if valid
if let image = image {
activityItems.append(image)
}
// Activity controller
let activityController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
// iPad settings
if Device.isPad {
activityController.popoverPresentationController?.sourceView = view
activityController.popoverPresentationController?.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
activityController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection.init(rawValue: 0)
}
// Excluded activity types
activityController.excludedActivityTypes = [
UIActivityType.airDrop,
UIActivityType.print,
UIActivityType.assignToContact,
UIActivityType.addToReadingList,
]
// Present
view.window?.rootViewController?.present(activityController, animated: true)
}
and then call it like so when the correct button was pressed (see above example)
openShareMenu(value: "\(self.score)", image: SOMEUIIMAGE)
Hope this helps
create reference of GameViewController in GameScene class like this way
class GameScene: SKScene, SKPhysicsContactDelegate {
var referenceOfGameViewController : GameViewController!
}
in GameViewController pass the reference like this way
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
// Load the SKScene from 'GameScene.sks'
if let scene = GameScene(fileNamed: "GameScene") {
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFill
scene.referenceOfGameViewController = self
// Present the scene
view.presentScene(scene)
}
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
}
by using this line you can pass reference to GameScene class
scene.referenceOfGameViewController = self
Now In GameScene class you can access all the variable of GameViewController like this way
referenceOfGameViewController.fbButton.hidden = false
referenceOfGameViewController.gameOverPopUP.hidden = false

How to create NSUserdefaults to image button in spritekit

I have Sound button (On/Off) in main scene and its works perfectly
but I have problem , off the background music playing when I was disqualified I return to the main screen, but the image of the button changes the music is turned on and stays on the image of the music is off.
Main Scene :
var SoundOnOff = SKSpriteNode()
override func didMoveToView(view: SKView) {
backgroundColor = UIColor(red:0.09, green:0.63, blue:0.52, alpha:1.0)
//Main Scene:
SoundOnOff.texture = SKTexture(imageNamed:"Sound-on.png")
SoundOnOff.position = CGPoint(x: self.size.width/2 - 40 , y: self.size.height/2 - 500)
SoundOnOff.size = CGSizeMake(60 , 60)
SoundOnOff.runAction(SKAction.moveToY(140, duration: 0.5))
SoundOnOff.removeFromParent()
addChild(SoundOnOff)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
let location = touch!.locationInNode(self)
if(SoundOnOff.containsPoint(location)) {
// ---------------------------------------------
// Play Background Music
// ---------------------------------------------
if ((NSUserDefaults.standardUserDefaults().objectForKey("onoroff")) !== true)
{
NSUserDefaults.standardUserDefaults().setBool(true, forKey: "onoroff")
Singleton.sharedInstance().pauseBackgroundMusic()
SoundOnOff.texture = SKTexture(imageNamed:"Sound-off.png")
}else {
NSUserDefaults.standardUserDefaults().setBool(false, forKey: "onoroff")
Singleton.sharedInstance().resumeBackgroundMusic()
SoundOnOff.texture = SKTexture(imageNamed:"Sound-on.png")
}
}
} }
}
Basically, you have to check in your Menu scene if music is off or on, and based on that to show appropriate texture. Right now, it seems that you are not making that check inside of a menu scene.
HINT: Obviously, your Singleton class has been implemented as a singleton. And if everything is done correctly, it is instantiated once and alive through the whole app's life time. So it has an info about music on/off state. What I wanted to point is that, when the app is closed, this singleton gets deallocated. So before that happen, you might store the info about music on/off into persistent storage, like NSUserDefaults, so the next time when user open up the app, he has his settings saved. Just a though... This really depends on you and what you want to offer in your app.

Resources