(Apple) GameplayKit State Machine implementation with SKScene - ios

I've read up on the GameplayKit State Machine and the documentation explicitly mention Game UI as an example of usage. Im fully understanding the concept but I am unsure how to implement it while using SpriteKit.
Lets say I want 3 states. Menu, Gameplay and Game Over - All which display content to the SKScene.
GameScene class extends SKScene. So it's impossible to turn GameScene into a state machine since we only can extend one class.
Should GameScene have a variable for a state machine with a reference to the SKScene, or is there a better solution? Earlier I've used protocols for state machines which makes it easy, but I'd like to try the functionality of GameplayKit.

You're on the right track. Add the state machine variable in GameScene and initialise its starting state.
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var stateMachine: GKStateMachine!
override func didMove(to view: SKView) {
self.stateMachine = GKStateMachine(states: [
StartState(scene: self),
PlayingState(scene: self),
PausedState(scene: self),
GameOverState(scene: self)
])
self.stateMachine.enter(StartState.self)
}
override func update(_ currentTime: TimeInterval) {
self.stateMachine.update(deltaTime: currentTime)
}
}
Then initialise the states so they accept GameScene as input parameter.
import SpriteKit
import GameplayKit
class StartState: GKState {
var scene: GameScene!
init(scene: GameScene){
super.init()
self.scene = scene
}
override func isValidNextState(_ stateClass: Swift.AnyClass) -> Bool {
return stateClass == PlayingState.self
}
override func updateWithDeltaTime(seconds: TimeInterval) {
self.stateMachine?.enterState(PlayingState.self)
}
}

Related

Setting preferred focus in TVOS on an SKSpriteNode?

Since I'm working in SpriteKit, my buttons are SKSpriteNodes...however I find myself in a situation where I need to set focus in my viewController via overriding preferredFocusedView. Is there a way to downcast an SKSpriteNode to a UIView? If so I haven't been able to figure out yet...any alternative?
let playButton = SKSpriteNode(imageNamed: "PlayButton")
playButton.position = CGPoint(x: scene.size.width * 0.25, y: scene.size.height * 0.25)
playButton.zPosition = Layer.UI.rawValue
scene.worldNode.addChild(playButton)
override var preferredFocusedView: UIView? {
get {
return //playButton how?
}
}
Focus navigation is only now supported with tvOS 10 and SpriteKit, prior to that you had to do it manually using your own focus system. For that reason preferred focus view is deprecated because it only supports UIViews. You should now use preferred focus environments instead.
First thing you do is in your GameViewController set the preferred focus environment to the currently presented SKScene. This essentially means that your SKScenes will handle the preferred focus instead of GameViewController. In a SpriteKit game the SKScenes should handle the UI such as buttons using SpriteKit APIs such as SKLabelNodes, SKSpriteNodes etc. Therefore you need to pass the preferred focus to the SKScene.
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// default code to present your 1st SKScene.
}
}
#if os(tvOS)
extension GameViewController {
/// Tell GameViewController that the currently presented SKScene should always be the preferred focus environment
override var preferredFocusEnvironments: [UIFocusEnvironment] {
if let scene = (view as? SKView)?.scene {
return [scene]
}
return []
}
}
#endif
Your playButton should be a subclass of SKSpriteNode that you will use for all your buttons in your game. Use enums and give them different names/ identifiers to distinguish between them when they are pressed (checkout Apples sample game DemoBots).
class Button: SKSpriteNode {
var isFocusable = true // easy way to later turn off focus for your buttons e.g. when overlaying menus etc.
/// Can become focused
override var canBecomeFocused: Bool {
return isFocusable
}
/// Did update focus
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
if context.previouslyFocusedItem === self {
// SKAction to reset focus animation for unfocused button
}
if context.nextFocusedItem === self {
// SKAction to run focus animation for focused button
}
}
}
Than in your SKScenes you can set the focus environment to your playButton or other UI.
e.g Start Scene
class StartScene: SKScene {
....
}
#if os(tvOS)
extension StartScene {
override var preferredFocusEnvironments: [UIFocusEnvironment] {
return [playButton]
}
}
#endif
e.g GameScene (e.g transfer focus to game menu when needed)
class GameScene: SKScene {
....
}
#if os(tvOS)
extension GameScene {
override var preferredFocusEnvironments: [UIFocusEnvironment] {
if isGameMenuShowing { // add some check like this
return [gameMenuNode]
}
return []
}
}
#endif
You will also have to tell your GameViewController to update its focus environment when you transition between SKScenes (e.g StartScene -> GameScene). This is especially important if you use SKTransitions, it took me a while to figure this out. If you use SKTransitions than the old and new scene are active during the transition, therefore the GameViewController will use the old scenes preferred focus environments instead of the new one which means the new scene will not focus correctly.
I do it like this every time I transition between scenes. You will have to use a slight delay or it will not work correctly.
...
view?.presentScene(newScene, transition: ...)
#if os(tvOS)
newScene.run(SKAction.wait(forDuration: 0.1)) { // wont work without delay
newScene.view?.window?.rootViewController?.setNeedsFocusUpdate()
newScene.view?.window?.rootViewController?.updateFocusIfNeeded()
}
#endif
You should read this article
https://medium.com/folded-plane/tvos-10-getting-started-with-spritekit-and-focus-engine-53d8ef3b34f3#.x5zty39pc
and watch the 2016 apple keynote called "Whats New in SpriteKit" where they talk about it half way through.
Hope this helps

SpriteKit - didMovetoView is not called

This is my code for the view controller
import UIKit
import SpriteKit
import GameplayKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
// Load the SKScene from 'GameScene.sks'
if let scene = SKScene(fileNamed: "GameScene") {
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFill
// Present the scene
view.presentScene(scene)
}
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
And this is my code for the GameScene
import SpriteKit
import GameplayKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
print("gets called")
}
But for some reasons, in the debug area, it didn't print "gets called", which indicates that didMove didn't even get called. What's happening here? Did I miss anything?
The iOS 9 way
In your GameViewController try to directly present your GameScene instead of a generic SKScene.
if let scene = GameScene(fileNamed: "GameScene") {
...
Remember "fileNamed" is not the name of the .swift file, its the name of the .sks file which is used for the xCode level editor.
The new iOS 10 way
It seems Apple now prefers to pass a generic SKScene like you are trying.
if let scene = SKScene(fileNamed: "GameScene") { ... }
To make it work go to the relevant .sks file and go to the inspector on the right. Click the second last item (custom class) and enter the name of the .swift file into the custom class field.
Hope this helps.
This could happen because you don't have the related SKS (GameScene.sks in your case) file reference to your project, check if you have added it or removed/renamed due to mistake.
I finally sort that out.
My project name contains a period at the end. Like the name "XXXX.". After several experimentations, I discovered that I can simply solve the problem by removing the period.
My suggestion:
use
override func didMoveToView(view: SKView)
instead of
override func didMove(to view: SKView)
Maybe a naming inconsistency in the documentation/API...
I second what Tom Xue said as someone who spent way too long looking for the answer to this. I had a hyphen in my app name and that seems to have been causing the problem. When I renamed my project, the scene was presented as it should have been.

What's the cleanest way to call a method from one SKScene to another in Swift?

So I got in my game 3 different scenes: MenuScene, GameScene and GameOverScene.
I would like to know how I can get the score variable located in GameScene from GameOverScene so I can show it up there after the player loses.
So in GameScene, I created this simple getter:
func getScore() -> Int {
return self.score
}
But if I try to do GameScene.getScore() from a different Scene I get an error.
Tried using "class" before func
class func getScore() -> Int {
return self.score
}
But that also gives me an error from GameScene saying:
"Instance member 'score' cannot be used on type GameScene"
So how's the best way to do it? I don't want to create a global variable for this, it would be ugly.
This is quite easy actually. When you segue to the new scene, you can also pass in a variable with it... Here is an example.
GameOverScene.swift
var score:Int = 0
GameScene.swift
var score:Int = THE USERS SCORE
func segue(){
let gameScene = GameOverScene(size: self.size)
gameScene.score = score
let transition = SKTransition.doorsCloseHorizontalWithDuration(0.5)
gameScene.scaleMode = SKSceneScaleMode.AspectFill
self.scene!.view?.presentScene(gameScene, transition: transition)
}
You need to keep an instance of GameScene in your MenuScene class (recommended). Alternatively, you can store the score in a global storage medium.
To enter the game scene, you need to create it first, right?
let scene = SKScene(fileNamed: "blah blah blah")
view.presentScene(scene)
Now instead of creating the scene and assigning it to a local variable, you assign it to a class level variable.
var gameScene: GameScene?
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if (...) {
gameScene = SKScene(fileNamed: "blah blah blah")
view.presentScene(gameScene!)
}
}
Then, when appropriate, you can access the score like this:
gameScene.getScore()
Another way is to use NSUserDefaults, but you don't seem to like it.
The final way that I can think of is to use static variables. You put all the stuff that you need to pass between the two scenes in a class as static variables. The disadvantage of this is that unless you declare the class private and put both scenes and the class in the same file, others classes can access and change the variables. This reduces maintainability.
Explicit getters are not needed in Swift, btw.
There are many ways to achieve what do you want to do. You could use a manager class , a shared instance to maintain your game variables.
Why this choice?
Because you will want to save your values or your profile or you will want to reset all the game progress, because you will want to select the old 2nd level when you have unlocked the 18th level, or repeat a failed level without accumulating values. It's fast to do it and you can handle separately your common game variables (it seems that I'm selling pots in an advertisement..)
class Settings: NSObject { // NSCoding in case you want to save your data
var score: Int = 0
func encodeWithCoder(aCoder: NSCoder!) {
aCoder.encodeInteger(score, forKey: "score")
}
init(coder aDecoder: NSCoder!) {
score = aDecoder. decodeIntegerForKey("score")
}
override init() {
super.init()
// do whatever you want to initializing your settings
}
}
class GameManager: NSObject {
static let sharedInstance = GameManager()
var settings : Settings! = Settings()
}
class MenuScene: SKScene {
let gameManager = GameManager.sharedInstance
override func didMoveToView(view: SKView) {
print("the current score is \(self.gameManager.settings.score)")
}
}
class GameScene: SKScene {
let gameManager = GameManager.sharedInstance
override func didMoveToView(view: SKView) {
print("the current score is \(self.gameManager.settings.score)")
}
}
class GameOverScene: SKScene {
let gameManager = GameManager.sharedInstance
override func didMoveToView(view: SKView) {
print("the current score is \(self.gameManager.settings.score)")
}
}

Use a boolean value from another scene in SpriteKit - Swift

I am trying to use a variable from my GameScene.swift file on my GameViewController.swift file to time my interstitial ads appropriately. It's a boolean value that determines if my player is dead or not.
var died = Bool()
That's all I did to create the variable in my GameScene.
When died == true in my GameScene, I want to send that to my GameViewController and then show an interstitial ad. I really just need to know how to pass a boolean between scenes. Thanks in advanced for your help.
You can follow these steps.
Do this in your GameScene:
protocol PlayerDeadDelegate {
func didPlayerDeath(player:SKSpriteNode)
}
class GameScene: SKScene {
var playerDeadDelegate:PlayerDeadDelegate?
...
// during your game flow the player dead and you do:
playerDeadDelegate.didPlayerDeath(player)
...
}
In the GameViewController you do:
class GameViewController: UIViewController,PlayerDeadDelegate {
override func viewDidLoad() {
super.viewDidLoad()
if let scene = GameScene(fileNamed:"GameScene") {
...
scene.playerDeadDelegate = self
}
}
func didPlayerDeath(player:SKSpriteNode) {
print("GameViewController: the player is dead now!!!")
// do whatever you want with the property player..
}
}
Your GameScene should have a reference object as delegate (e.g conforms to GameSceneDelegate protocol) which is actually pointing to GameViewController object. Then when died becomes true, inform your delegate object (GameViewController object) about this event via a delegate-method and implement that method by conforming to the above protocol in your GameViewController class.

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.

Resources