いものやま。

雑多な知識の寄せ集め

変種オセロのUIを作ってみた。(その14)

さて、レイアウトの修正は出来た。

次はボタンのタッチ処理を出来るようにする。
また、ボードのタッチ処理についても、修正を行う。

オブザーバパターンの実現

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()の実装も追加してある。

これでオブザーバパターンが実現されたのだけど、今までベタに書いていたボードをタッチしたときの処理を外部に移譲するようにしたので、このままだとゲームのプレイ自体が出来なくなっている。
なので、手番をコントロールするクラスを用意して、ゲームのプレイが出来るようにする必要がある。

今日はここまで!