さて、レイアウトの修正は出来た。
次はボタンのタッチ処理を出来るようにする。
また、ボードのタッチ処理についても、修正を行う。
オブザーバパターンの実現
SwiftでSetの型パラメータにプロトコルを指定する方法について。 - いものやま。で言及したように、ここではオブザーバパターンを使って実装を進めていく。
プロトコルの定義
まずはプロトコルの定義から。
//============================== // YWF //------------------------------ // ButtonNodeObserver.swift //============================== public protocol ButtonNodeObserver: class { func buttonIsSelected(button: ButtonNode) }
//============================== // YWF //------------------------------ // BoardNodeObserver.swift //============================== public protocol BoardNodeObserver: class { func boardNodeActionIsFinished(node: BoardNode) func boardNodeSquareIsSelected(node: BoardNode, _ square: SquareNode) }
ボタンやボードのオブザーバとなるクラスは、これらのプロトコルに準拠している必要がある。
ボタンの実装の修正
ButtonNodeを修正して、オブザーバの登録、削除、それと通知を行えるようにする。
//============================== // YWF //------------------------------ // ButtonNode.swift //============================== import SpriteKit public class ButtonNode: SKSpriteNode { // 省略 private var observers: [ObjectIdentifier:ButtonNodeObserver] public init(type: Type, enabled: Bool = true) { self.type = type self.enabled = enabled self.observers = [ObjectIdentifier:ButtonNodeObserver]() let texture = ButtonNode.getTextureFor(type, enabled: enabled) super.init(texture: texture, color: SKColor.whiteColor(), size: texture.size()) self.userInteractionEnabled = enabled } // 省略 public func addObserver(observer: ButtonNodeObserver) { self.observers[ObjectIdentifier(observer)] = observer } public func removeObserver(observer: ButtonNodeObserver) { self.observers.removeValueForKey(ObjectIdentifier(observer)) } private func notifyObservers() { for (_, observer) in self.observers { observer.buttonIsSelected(self) } } public override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) { if self.enabled { let touch = touches.first as! UITouch let location = touch.locationInNode(self.parent) if self.containsPoint(location) { self.notifyObservers() } } } }
なお、ButtonNodeObserverのSetを作るために、SwiftでSetの型パラメータにプロトコルを指定する方法について。 - いものやま。で言及した方法を使っている。
もう一つ。
タッチで指が離れたときに、その位置がボタン内なら登録されているオブザーバに通知を行うようにしているのだけど、ちょっとハマったのがSKNode#containsPoint(_: CGPoint)。
ドキュメントだと、この引数の説明に"A point in the node’s coordinate system."と書かれているので、このノードでの座標を指定したのだけど、実際にはそれだと上手くいかなかった。
どうやら、レシーバとなるノードが配置されている座標系ーーすなわち、レシーバのノードの親ノードの座標系での座標を指定しないといけないみたい。
なので、touch.locationInNode(self.parent)というふうに、親ノードの座標系で座標を取得している。
ボードの実装の修正
ボードについても同様の修正を行っていく。
//============================== // YWF //------------------------------ // BoardNode.swift //============================== import SpriteKit public class BoardNode: SKSpriteNode { // 省略 private static let updateActionKey = "updateAction" // 省略 private var observers: [ObjectIdentifier:BoardNodeObserver] // 省略 public init(board: Board) { // 省略 self.observers = [ObjectIdentifier:BoardNodeObserver]() // 省略 } // 省略 public func addObserver(observer: BoardNodeObserver) { self.observers[ObjectIdentifier(observer)] = observer } public func removeObserver(observer: BoardNodeObserver) { self.observers.removeValueForKey(ObjectIdentifier(observer)) } private func notifyObserversActionIsFinished(newBoard: Board) { for (_, observer) in self.observers { observer.boardNodeActionIsFinished(self) } } private func notifyObserversSquareIsSelected(square: SquareNode) { for (_, observer) in self.observers { observer.boardNodeSquareIsSelected(self, square) } } public func play(row: Int, _ col: Int) { assert( self.board.isPlayable(row, col), "invalid play. [row: \(row), col: \(col)]") let originalUserInteractionEnabled = self.userInteractionEnabled self.userInteractionEnabled = false let originalBoard = self.board self.previousBoards.append(originalBoard) self.previousActions.append(Board.Action.Play(row, col)) self.board = self.board.play(row, col) let pieceStatus = (originalBoard.turn == .Bad) ? PieceNode.Status.Bad : PieceNode.Status.Good self.squares[row][col].addPiece(pieceStatus) self.squares[row][col].piece.fadeInWithCompletion { let duration = self.updateSquaresFrom(originalBoard, to: self.board, except: row, col) let waitAction = SKAction.waitForDuration(duration) self.runAction(waitAction) { self.notifyObserversActionIsFinished(self.board) self.userInteractionEnabled = originalUserInteractionEnabled } } } public func change(row: Int, _ col: Int) { assert( self.board.isChangeable(row, col), "invalid change. [row: \(row), col: \(col)]") let originalUserInteractionEnabled = self.userInteractionEnabled self.userInteractionEnabled = false let originalBoard = self.board self.previousBoards.append(originalBoard) self.previousActions.append(Board.Action.Change(row, col)) self.board = self.board.change(row, col) let pieceStatus = (originalBoard.turn == .Bad) ? PieceNode.Status.Bad : PieceNode.Status.Good self.squares[row][col].piece.changeTo(pieceStatus) { let duration = self.updateSquaresFrom(originalBoard, to: self.board, except: row, col) let action = SKAction.moveTo(BoardNode.tokenPosition[self.board.token]!, duration: 0.5) self.token.runAction(action) let waitAction = SKAction.waitForDuration(max(duration, NSTimeInterval(0.5))) self.runAction(waitAction) { self.notifyObserversActionIsFinished(self.board) self.userInteractionEnabled = originalUserInteractionEnabled } } } public func pass() { assert(self.board.mustPass, "invalid pass.") let originalBoard = self.board self.previousBoards.append(originalBoard) self.previousActions.append(Board.Action.Pass) self.board = self.board.pass() self.notifyObserversActionIsFinished(self.board) } private func updateSquaresFrom(original: Board, to new: Board, except row: Int, _ col: Int) -> NSTimeInterval { var maxDuration: NSTimeInterval = NSTimeInterval(0.0) for updateRow in Board.RowMin...Board.RowMax { for updateCol in Board.ColMin...Board.ColMax { if updateRow != row || updateCol != col { if let piece = self.squares[updateRow][updateCol].piece { let originalStatus = original.status(updateRow, updateCol) let newStatus = new.status(updateRow, updateCol) if originalStatus != newStatus { switch newStatus { case .Bad: piece.changeTo(.Bad, withKey: BoardNode.updateActionKey) case .Common: piece.changeTo(.Common, withKey: BoardNode.updateActionKey) case .Good: piece.changeTo(.Good, withKey: BoardNode.updateActionKey) default: break } let duration = piece.actionForKey(BoardNode.updateActionKey)?.duration if duration != nil && duration > maxDuration { maxDuration = duration! } } } } } } return maxDuration } // 省略 public override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) { self.clearCurrentSelectedSquareIfNeeded() if self.touch != nil && touches.contains(touch) { let location = self.touch.locationInNode(self) let touchedSquare = self.findSquareAtPoint(location) if self.touchedSquareIsValid(touchedSquare) { self.notifyObserversSquareIsSelected(touchedSquare) } } } // 省略 }
なお、ボードに対するアクションが終わったことを通知するために、BoardNode#play(_: Int, _: Int)、BoardNode#change(_: Int, _ int)にも修正を加えている。
(SKActionの実行が全部終わってから通知がされるようにするために、ちょっと無茶してる)
それと、ついでにBoardNode#pass()の実装も追加してある。
これでオブザーバパターンが実現されたのだけど、今までベタに書いていたボードをタッチしたときの処理を外部に移譲するようにしたので、このままだとゲームのプレイ自体が出来なくなっている。
なので、手番をコントロールするクラスを用意して、ゲームのプレイが出来るようにする必要がある。
今日はここまで!