ここからはStateパターンを使って変更に強いプログラムにしていきます。
StateパターンとはGoFの23個のデザインパターンの1つで各Stateでの処理を対応するStateクラスに持たせるデザインパターンです。
今回のような複数Stateがあるときは「このStateのときはこの処理をしてこのStateのときは別の処理をする」というようにStateごとの処理を1カ所にまとめて書くことが多いかと思います。しかし1カ所に集約するとStateが増えるたびにコードが長くなり、だんだんと保守性が下がってきます。
Stateパターンでは「このStateのときは、この処理」という部分を各Stateクラスに持たせるので、処理が追加されても特定箇所のコードが長くなりにくく保守性を維持できます。
説明だけでは分かりにくいと思うので実装してみましょう。
まずはChar.swiftを以下のように書き換えます。「AttackState」「StayState」という2つのStateクラスを作成してenableToAttackメソッドとupdateメソッドを持たせました。
class Char: SKSpriteNode {
class CharState {
weak var char: Char?
var power: Int {
return char?.power ?? 0
}
init(char: Char?) {
self.char = char
}
func update() {}
func enableToAttack() -> Bool {
return false
}
}
class AttackState: CharState {
override func enableToAttack() -> Bool {
return true
}
}
class StayState: CharState {
var enteredDate = NSDate()
override func update() {
if NSDate().timeIntervalSinceDate(enteredDate) > 0.2 {
char?.state = AttackState(char: char)
}
}
override func enableToAttack() -> Bool {
return false
}
}
lazy var state: CharState = {
return AttackState(char: self)
}()
var power = 1
}
併せてGameScene.swiftのupdateメソッドを修正します。
class GameScene: SKScene, SKPhysicsContactDelegate {
// 略...
override func update(currentTime: NSTimeInterval) {
char.state.update()
// 攻撃可能かはenableToAttackメソッドを参照
if char.state.enableToAttack() {
enemyList.enemiesCloseToPoint(char.frame.origin, distance: 50).forEach {
// lifeはchar.state.powerの分減らす
$0.life -= char.state.power
char.state = Char.StayState(char: char)
if $0.life <= 0 {
$0.physicsBody?.node?.removeFromParent()
$0.physicsBody?.node?.removeAllActions()
if enemyList.isAllEnemyRemoved() {
state = .GameClear
let myLabel = SKLabelNode(fontNamed: "HiraginoSans-W6")
myLabel.text = "ゲームクリア"
myLabel.fontSize = 45
myLabel.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame) - 20)
addChild(myLabel)
}
}
}
}
}
// func changeCharStateToAttack() {
// char.state = .Attack
// }
// 略...
}
これでStateパターンを使う形への置き換えは完了です。今回はStayStateからAttackStateへ戻る処理と攻撃可能かの条件を各Stateクラスに持たせることができました。しかし今の状態だと処理を移しただけなので、Stateパターンがどう役立ったかが分かりにくいかと思います。
そこで次項で攻撃力2倍状態というStateの追加をすることでStateパターンによってどう良くなったかを確かめようかと思います。
実際の修正はChar.swiftにAngryStateを追加するだけです。後は今後攻撃力2倍アイテムを実装したときにstateをAngryStateにする処理を書けば終わりです。
class Char: SKSpriteNode {
// 略...
class AngryState: CharState {
var enteredDate = NSDate()
override var power: Int {
return (char?.power ?? 0) * 2
}
override func enableToAttack() -> Bool {
return true
}
override func update() {
if NSDate().timeIntervalSinceDate(enteredDate) > 10.0 {
char?.state = AttackState(char: char)
}
}
}
// 略...
}
「もしStateパターンにする前にAngryStateを追加したら、どうなるか」を考えてみます。
まず「攻撃可能かどうか」の処理は下のように実装するかと思います。Stateが増えるたびに修正が必要なので抜け漏れが発生しそうです。
if char.state == .Attack || char.state == .Angry {
// 略...
}
敵にダメージを与える箇所は下のようになるかと思います。ここもStateが増えると分岐が増えて可読性が落ちそうです。
if char.state == .Attack {
$0.life -= char.power
} else if char.state == .Angry {
$0.life -= char.power * 2
}
このようにStateパターンで実装しておけばStateの追加に強くなります。
実際の現場でのタワーディフェンスゲーム作成では「自機のスピード2倍にするアイテムを追加してほしい」「自機の攻撃力を半分にするような敵を作ってほしい」といった自機にさまざまなStateを追加することになるかと思います。
そういったときにStateパターンで実装したことで既存コードへの修正が最小限に済み、スピード感ある改善が行えます。
開発者向けiOS 9、WatchOS 2、Swift 2、Xcode 7の新機能と新しいApple Developer Programの参考情報まとめ
Apple WatchやiPhoneのアプリを作ろう! Playgroundで学ぶSwiftの基礎―変数、定数、型、演算
iOSアプリ開発でObjective-CからSwiftに移行するための手順、注意点まとめ〜言語仕様の違いは? 連携時の呼び出し方は?Copyright © ITmedia, Inc. All Rights Reserved.