#前回の記事
前回GameplayKit】iOSゲームアプリにおけるStateパターン実践 Part_4では、ステートマシンによるステート遷移をいくつかの方法を使って実装しました。
#この記事について
SpriteKit
のスプライトアクションを使って、複雑なアニメーションを実装します。
SpriteKit
場でのアニメーションは SKAction
クラスで実現します。
また、ステート遷移先を条件によって分岐させます。
#手順
ボトルのアニメーションを実装
水量減少のアニメーションを実装
水流のアニメーションを実装
条件付きでカラ状態に遷移させる
##ボトルのアニメーション
waitAndExit()
メソッドの名前をplayDispensingAnimationThenExit()
メソッドに変更しておきます。呼び出し部分も合わせて変更します。
アニメーションはSKAction
オブジェクトとして、playDispensingAnimationThenExit()
メソッドに実装します。
まずは、給水中の状態ServeState
に遷移後、画面端からボトルスプライトがスライドしてくるアニメーションを実装します。スライドしてきたボトルは、1秒間だけ静止した後に画面外へスライドしていきます。
事前にActions.sks
ファイルで定義しておいたSKAction
のタイムラインを基にして、slideCupAction
オブジェクトを生成します。
class ServeState: DispenserState {
static let timeScale = 1.0
init(game: GameScene) {
super.init(game: game, associatedNodeName: "ServeState")
}
override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState)
playDispensingAnimationThenExit()
}
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
return stateClass is PartiallyFullState.Type
}
func playDispensingAnimationThenExit() {
let slideCupAction = SKAction(named: "slideCup", duration: 3 * ServeState.timeScale)!
let slideCupActionOnNode = SKAction.run(slideCupAction, onChildWithName: "//bottle")
//let waitAction = SKAction.wait(forDuration: ServeState.timeScale)
game.scene?.run(slideCupActionOnNode, completion: {
self.stateMachine?.enter(PartiallyFullState.self)
})
}
}
SKAction(named:duration:)
メソッドでアニメーション動作のオブジェクトを生成し、run(onChildWithName:)
メソッドを使ってスプライトノードに関連づけています。
ここでは、ボトルのスプライトノード座標を元の場所から(0, 0)にMoveした1秒後、さらに(1000, 0)にMoveしています。
##水量変化のアニメーション
続いて、水量変化のアニメーションを実装します。
func playDispensingAnimationThenExit() {
let slideCupAction = SKAction(named: "slideCup", duration: 3 * ServeState.timeScale)!
let drainWaterAction = SKAction(named: "drainWater", duration: ServeState.timeScale)!
let resetStreamAction = SKAction(named: "resetStream", duration: 0)!
let resetCupAction = SKAction(named: "resetCup", duration: 0)!
let slideCupActionOnNode = SKAction.run(slideCupAction, onChildWithName: "//bottle")
let drainWaterActionOnNode = SKAction.run(drainWaterAction, onChildWithName: "//water")
let resetStreamActionOnNode = SKAction.run(resetStreamAction, onChildWithName: "//stream")
let resetCupActionOnNode = SKAction.run(resetCupAction, onChildWithName: "//bottle")
let waitAction = SKAction.wait(forDuration: ServeState.timeScale)
let innerSequence = [waitAction, drainWaterActionOnNode,
waitAction, resetStreamActionOnNode,
waitAction, resetCupActionOnNode]
let innerSequenceAction = SKAction.sequence(innerSequence)
let group = [slideCupActionOnNode, innerSequenceAction]
let groupAction = SKAction.group(group)
game.scene?.run(groupAction, completion: {
self.stateMachine?.enter(PartiallyFullState.self)
})
}
###ビルド
シミュレータの画面をタップすると、ボトルがスライドしてきた後、給水機タンクの水量が減少してからボトルがさらにスライドしていきます。
##水流のアニメーション
ボトルスライドのアクションとタンク水量減少アクションの間に、水流アニメーションを挿入します。
func playDispensingAnimationThenExit() {
let slideCupAction = SKAction(named: "slideCup", duration: 3 * ServeState.timeScale)!
let drainWaterAction = SKAction(named: "drainWater", duration: ServeState.timeScale)!
let resetStreamAction = SKAction(named: "resetStream", duration: 0)!
let resetCupAction = SKAction(named: "resetCup", duration: 0)!
let fillCupAction = SKAction(named: "fillCup", duration: 2 * ServeState.timeScale)!
let slideCupActionOnNode = SKAction.run(slideCupAction, onChildWithName: "//bottle")
let drainWaterActionOnNode = SKAction.run(drainWaterAction, onChildWithName: "//water")
let resetStreamActionOnNode = SKAction.run(resetStreamAction, onChildWithName: "//stream")
let resetCupActionOnNode = SKAction.run(resetCupAction, onChildWithName: "//bottle")
let fillCupActionOnNode = SKAction.run(fillCupAction, onChildWithName: "//stream")
let waitAction = SKAction.wait(forDuration: ServeState.timeScale)
let innerSequence = [waitAction, drainWaterActionOnNode,
waitAction, resetStreamActionOnNode,
waitAction, resetCupActionOnNode]
let innerSequenceAction = SKAction.sequence(innerSequence)
let group = [slideCupActionOnNode, fillCupActionOnNode, innerSequenceAction]
let groupAction = SKAction.group(group)
game.scene?.run(groupAction, completion: {
self.stateMachine?.enter(PartiallyFullState.self)
})
}
###ビルド
シミュレータでビルドします。
今度は、水量が減少すると同時に水流がボトルに注ぎ込まれるアニメーションが表示されます。
##条件付きでカラ状態に遷移させる
給水機のタンクが空になったら、ステートマシンをEmptyState
に遷移させます。
まずは、カラの状態を表現するEmptyState
を定義します。
メニューバーから「File > New > File... > iOS > Swift Class」を選択します。(EmptyState.swift)
GameplayKit
フレームワークをインポートして、DispenserState
クラスを継承したEmptyState
クラスを定義します。
import GameplayKit
class EmptyState: DispenserState {
init(game: GameScene) {
super.init(game: game, associatedNodeName: "EmptyState")
}
override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState)
let red = SKColor.red
changeIndicatorLightToColor(red)
}
override func willExit(to nextState: GKState) {
super.willExit(to: nextState)
let black = SKColor.black
changeIndicatorLightToColor(black)
}
}
イニシャライザ、didEnter(from:)
メソッドおよび willExit(to:)
メソッドを実装しておきます。
EmptyState
クラスを定義したら、ステートマシンに登録しておきます。
override func didMove(to view: SKView) {
let fullState = FullState(game: self)
let serveState = ServeState(game: self)
let partiallyFullState = PartiallyFullState(game: self)
let emptyState = EmptyState(game: self)
stateMachine = GKStateMachine(states: [fullState,
serveState,
partiallyFullState,
emptyState])
stateMachine.enter(FullState.self)
}
###カラ状態への条件
給水機のタンク水量(スプライトノードのheight
プロパティ)が1以下の場合、EmptyState
に遷移させるようにします。
func playDispensingAnimationThenExit() {
let slideCupAction = SKAction(named: "slideCup", duration: 3 * ServeState.timeScale)!
let drainWaterAction = SKAction(named: "drainWater", duration: ServeState.timeScale)!
let resetStreamAction = SKAction(named: "resetStream", duration: 0)!
let resetCupAction = SKAction(named: "resetCup", duration: 0)!
let fillCupAction = SKAction(named: "fillCup", duration: 2 * ServeState.timeScale)!
let slideCupActionOnNode = SKAction.run(slideCupAction, onChildWithName: "//bottle")
let drainWaterActionOnNode = SKAction.run(drainWaterAction, onChildWithName: "//water")
let resetStreamActionOnNode = SKAction.run(resetStreamAction, onChildWithName: "//stream")
let resetCupActionOnNode = SKAction.run(resetCupAction, onChildWithName: "//bottle")
let fillCupActionOnNode = SKAction.run(fillCupAction, onChildWithName: "//stream")
let waitAction = SKAction.wait(forDuration: ServeState.timeScale)
let innerSequence = [waitAction, drainWaterActionOnNode,
waitAction, resetStreamActionOnNode,
waitAction, resetCupActionOnNode]
let innerSequenceAction = SKAction.sequence(innerSequence)
let group = [slideCupActionOnNode, fillCupActionOnNode, innerSequenceAction]
let groupAction = SKAction.group(group)
game.scene?.run(groupAction, completion: {
let waterNode = self.game.childNode(withName: "//water") as! SKSpriteNode
let restOfWater = waterNode.size.height
if restOfWater < 1 {
self.stateMachine?.enter(EmptyState.self)
} else {
self.stateMachine?.enter(PartiallyFullState.self)
}
})
}
これでステートマシンは、遷移先の状態をEmptyState
とPartiallyFullState
で分岐しますが、isValidNextState(_:)
メソッドがPartiallyFullState
以外へのステート遷移を許可していません。
EmptyState
にも遷移できるようにisValidNextState(_:)
を修正します。
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
switch stateClass {
case is PartiallyFullState.Type, is EmptyState.Type:
return true
default:
return false
}
}
###ビルド
給水機のタンク残量が無くなると、ステート遷移パネルが「Empty」になり表示ランプがレッドになります。
#次回
複雑なアニメーションのほか、ステート遷移の分岐も実装しました。
【GameplayKit】iOSゲームアプリにおけるStateパターン実践 Part_6では、SpriteKit
におけるレンダリングを解説します。