Skip to content

Instantly share code, notes, and snippets.

@sarah-j-smith
Last active September 12, 2015 07:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sarah-j-smith/8d87e44378a77127066a to your computer and use it in GitHub Desktop.
Save sarah-j-smith/8d87e44378a77127066a to your computer and use it in GitHub Desktop.
Fade an SKNode in and out using a GKStateMachine

Control visibility of an SKNode via a State Machine

Problem with conventional approach: you want to hide a node by fading it out. But when you do that sometimes its already fading in, or partly faded out. You need to cancel any previous animation and then start the new animation.

Solution: use a state machine to capture the potential transitions between visible, fading in/out, and hidden.

To add to a node

var visibilityState: FaderStateMachine?

init() {
  super.init()
  visibilityState = FaderStateMachine(self, 0.4)
}

Then to hide:

visibilityState.enterState(Hiding)

or

visibilityState.enterState(Showing)
//
// FaderStateMachine.swift
// WordMonsters
//
// Created by Sarah Smith on 9/12/15.
// Copyright © 2015 Sarah Smith. All rights reserved.
//
import Foundation
import GameplayKit
import SpriteKit
class FaderStateMachine : GKStateMachine
{
let statesList: [GKState]
init(targetNode: SKNode, duration: NSTimeInterval)
{
statesList = [
Hidden(node: targetNode, duration: duration),
Hiding(node: targetNode, duration: duration),
Showing(node: targetNode, duration: duration),
Shown(node: targetNode, duration: duration)
]
super.init(states: statesList)
}
}
//
// Hidden.swift
// WordMonsters
//
// Created by Sarah Smith on 9/12/15.
// Copyright © 2015 Sarah Smith. All rights reserved.
//
import Foundation
import GameplayKit
import SpriteKit
class Hidden : GKState, NodeAffector
{
var targetNode : SKNode?
var fadeDuration : NSTimeInterval?
init(node: SKNode, duration: NSTimeInterval)
{
targetNode = node
fadeDuration = duration
}
override func didEnterWithPreviousState(previousState: GKState?)
{
if let actualTargetNode = targetNode {
actualTargetNode.hidden = true
actualTargetNode.alpha = 0.0
}
}
/**
The only thing a Hidden node can do is begin Showing itself.
*/
override func isValidNextState(stateClass: AnyClass) -> Bool {
return stateClass == Showing.self
}
}
//
// Hiding.swift
// WordMonsters
//
// Created by Sarah Smith on 9/12/15.
// Copyright © 2015 Sarah Smith. All rights reserved.
//
import Foundation
import GameplayKit
import SpriteKit
class Hiding : GKState, NodeAffector
{
var targetNode : SKNode?
var fadeDuration : NSTimeInterval?
init(node: SKNode, duration: NSTimeInterval)
{
targetNode = node
fadeDuration = duration
}
override func didEnterWithPreviousState(previousState: GKState?)
{
guard let actualTargetNode = targetNode else { return }
actualTargetNode.removeActionForKey("FadeInAnimation")
let fade = SKAction.fadeOutWithDuration(NSTimeInterval(fadeDuration!))
let changeState = SKAction.runBlock({
self.stateMachine?.enterState(Hidden.self)
})
let fadeAndChangeState = SKAction.sequence([fade, changeState])
actualTargetNode.runAction(fadeAndChangeState, withKey: "FadeOutAnimation")
}
/**
A node that is Hiding can do a u-turn and start Showing, or it can become Hidden.
*/
override func isValidNextState(stateClass: AnyClass) -> Bool
{
return stateClass == Showing.self || stateClass == Hidden.self
}
}
//
// NodeAffector.swift
// WordMonsters
//
// Created by Sarah Smith on 9/12/15.
// Copyright © 2015 Sarah Smith. All rights reserved.
//
import Foundation
import SpriteKit
protocol NodeAffector
{
var targetNode : SKNode? { get set }
var fadeDuration: NSTimeInterval? { get set }
}
//
// Showing.swift
// WordMonsters
//
// Created by Sarah Smith on 9/12/15.
// Copyright © 2015 Sarah Smith. All rights reserved.
//
import Foundation
import GameplayKit
class Showing : GKState, NodeAffector
{
var targetNode : SKNode?
var fadeDuration : NSTimeInterval?
init(node: SKNode, duration: NSTimeInterval)
{
targetNode = node
fadeDuration = duration
}
override func didEnterWithPreviousState(previousState: GKState?)
{
guard let actualTargetNode = targetNode else { return }
actualTargetNode.removeActionForKey("FadeOutAnimation")
let appear = SKAction.fadeInWithDuration(NSTimeInterval(fadeDuration!))
let changeState = SKAction.runBlock({
self.stateMachine?.enterState(Shown.self)
})
let fadeAndChangeState = SKAction.sequence([appear, changeState])
actualTargetNode.runAction(fadeAndChangeState, withKey: "FadeInAnimation")
}
/**
A node that is Showing can do a u-turn and begin Hiding again, or it can
become fully Shown.
*/
override func isValidNextState(stateClass: AnyClass) -> Bool
{
print("Checking if I can go from: \(stateClass) to \(self)")
return stateClass == Hiding.self || stateClass == Shown.self
}
}
//
// Shown.swift
// WordMonsters
//
// Created by Sarah Smith on 9/12/15.
// Copyright © 2015 Sarah Smith. All rights reserved.
//
import Foundation
import GameplayKit
class Shown : GKState, NodeAffector
{
var targetNode : SKNode?
var fadeDuration : NSTimeInterval?
init(node: SKNode, duration: NSTimeInterval)
{
targetNode = node
fadeDuration = duration
}
override func didEnterWithPreviousState(previousState: GKState?)
{
if let actualTargetNode = targetNode {
actualTargetNode.hidden = false
actualTargetNode.alpha = 1.0
}
}
/**
A Shown node can begin Hiding.
*/
override func isValidNextState(stateClass: AnyClass) -> Bool
{
return stateClass == Hiding.self
}
}
@sarah-j-smith
Copy link
Author

No adjustment is made in the case where you do a "u-turn" - that is going from Showing straight to Hiding or vice-versa. In that case if the "Showing" animation is e.g. 1/3 done and alpha is 0.33, when you start hiding, the duration of the animation should really be reduced to 1/3 of its normal duration.

Because no adjustment is made, if duration is large players might notice a discrepancy in animation speed in the "u-turn" case. This would not be hard to but I have not bothered as my durations are small.

@sarah-j-smith
Copy link
Author

Arrgh! The sense of isValidNextState was completely wrong in the first issue of this gist. Its not what I read it as "am I a valid next state, given this source state" - its actually "given I'm the current state, can I transition to this next state".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment