いものやま。

雑多な知識の寄せ集め

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

BoardNodeの実装の続き。

今日はタッチ処理を実装していく。

タッチの仕様

まず、タッチの仕様を考えてみる。

基本的な仕様は、以下。

  • 選択可能なマスが、ハイライトで表示される。
  • 選択可能なマスを1回タッチすると、マスが選択状態になる。
  • 選択状態になったマスは、より強いハイライトで表示される。
  • 同じマスをもう1回タッチすると選択が確定し、駒が置かれる(あるいは、普通の子になる)。

補足的な仕様としては、以下のようにする。

  • ハイライトは、手番の色で行う。
    (悪い子の手番なら黒くし、良い子の手番なら白くする)
  • タッチされた状態で指が動いたら、最後に触られていたマスがタッチされたという扱いにする。
  • 選択状態でないマスや、選択不可能なマスが選ばれたら、選択は解除される。
  • 最後にプレイされた場所のマスは、それをプレイした手番の色でハイライトされる。

マスの描画の修正

最初に、マスをハイライトに対応させるための修正を行う。

public class SquareNode: SKNode {
  public enum Status {
    case Normal
    case Lighted
    case HighLighted
  }
  
  public static let size = CGSize(width: 60.0, height: 60.0)
  private static let lightedAlpha: CGFloat = 0.3
  private static let highLightedAlpha: CGFloat = 0.6

  // 省略

マスの状態として、.Normal、.Lighted、.HighLightedという列挙子を用意。
それと、それぞれの状態の塗りのアルファ値をクラス定数として用意した。

そして、マスの状態を変えるためのメソッドを追加。

  // 省略

  public func changeStatusTo(status: Status, withTurn turn: Board.Status) {
    self.status = status
    
    switch status {
    case .Normal:
      self.square.fillColor = SKColor.clearColor()
    case .Lighted:
      if turn == .Bad {
        self.square.fillColor = SKColor.darkGrayColor().colorWithAlphaComponent(SquareNode.lightedAlpha)
      } else {
        self.square.fillColor = SKColor.whiteColor().colorWithAlphaComponent(SquareNode.lightedAlpha)
      }
    case .HighLighted:
      if turn == .Bad {
        self.square.fillColor = SKColor.darkGrayColor().colorWithAlphaComponent(SquareNode.highLightedAlpha)
      } else {
        self.square.fillColor = SKColor.whiteColor().colorWithAlphaComponent(SquareNode.highLightedAlpha)
      }
    }
  }
}

なお、悪い子のときにSKColor.blackColor()でなくSKColor.darkGrayColor()を使っているのは、blackColor()だと暗すぎてよくない感じだったので、少し弱い色にしたから。

駒のフェードイン/フェードアウト

駒をボードに置くときに、いきなり表示させるのもなんなので、フェードインとフェードアウトを出来るようにした。

public class PieceNode: SKSpriteNode {
  // 省略

  private static let fadeDuration = NSTimeInterval(0.2)

  // 省略

  public func fadeIn() {
    self.alpha = 0.0
    let action = SKAction.fadeInWithDuration(PieceNode.fadeDuration)
    self.runAction(action)
  }
  
  public func fadeInWithCompletion(completion: ()->Void) {
    self.alpha = 0.0
    let action = SKAction.fadeInWithDuration(PieceNode.fadeDuration)
    self.runAction(action, completion: completion)
  }
  
  public func fadeOut() {
    self.alpha = 1.0
    let action = SKAction.fadeOutWithDuration(PieceNode.fadeDuration)
    self.runAction(action)
  }
  
  public func fadeOutWithCompletion(completion: ()->Void) {
    self.alpha = 1.0
    let action = SKAction.fadeOutWithDuration(PieceNode.fadeDuration)
    self.runAction(action, completion: completion)
  }
  
  // 省略

なお、駒を置く→フェードインで表示される→表示が終わったら挟まれた駒の状態を変える、としたかったので、単にフェードイン/フォードアウトするメソッドの他に、完了ハンドラを引数とするメソッドも追加している。

駒の状態の変化の修正

駒の状態を変化させるメソッドについても、完了ハンドラを引数として取れるように修正を行う。

public class PieceNode: SKSpriteNode {
  // 省略

  public func changeTo(status: Status) {
    if (self.status == status) ||
        ((self.status == .Bad) && (status == .Good)) ||
        ((self.status == .Good) && (status == .Bad)) {
      return
    }

    let changeAction = self.createChangeActionToStatus(status)
    self.runAction(changeAction)
    
    self.previousStatus = self.status
    self.status = status
  }
  
  public func changeTo(status: Status, completion: ()->Void) {
    if (self.status == status) ||
      ((self.status == .Bad) && (status == .Good)) ||
      ((self.status == .Good) && (status == .Bad)) {
        return
    }
    
    let changeAction = self.createChangeActionToStatus(status)
    self.runAction(changeAction, completion: completion)
    
    self.previousStatus = self.status
    self.status = status
  }
  
  private func createChangeActionToStatus(status: Status) -> SKAction {
    let preAction: SKAction
    if (status != .Common) && (self.previousStatus != status) {
      let textures = PieceNode.rotateAnimations[status]!
      preAction = SKAction.animateWithTextures(textures, timePerFrame: NSTimeInterval(0.05))
    } else {
      preAction = SKAction.waitForDuration(NSTimeInterval(0.2))
    }
    
    let waitAction = SKAction.waitForDuration(NSTimeInterval(0.1))
    
    let textures: [SKTexture]
    switch self.status {
    case .Bad:
      textures = PieceNode.toCommonAnimations[.Bad]!
    case .Good:
      textures = PieceNode.toCommonAnimations[.Good]!
    case .Common:
      textures = PieceNode.fromCommonAnimations[status]!
    }
    let changeAction = SKAction.animateWithTextures(textures, timePerFrame: NSTimeInterval(0.05))
    
    return SKAction.sequence([preAction, waitAction, changeAction])
  }
}

完了ハンドラを取る場合も取らない場合も、実行するアクション自体には差がないので、アクションを作るメソッドはprivateメソッドにまとめて、完了ハンドラを取らない場合はSKNode#runAction(_: SKAction)、完了ハンドラを取る場合はSKNode#runAction(_: SKAction, completion: ()->Void)でアクションを実行させるようにしている。

ボードの描画の修正

まず、プレイ可能な場所をハイライトで表示したり、あるいはハイライトを消すためのメソッドを追加。

public class BoardNode: SKSpriteNode {
  // 省略

  public func lightUpEnableSquares() {
    let turn = self.board.turn
    for action in self.board.legalActions {
      switch action {
      case let .Play(row, col):
        self.squares[row][col].changeStatusTo(.Lighted, withTurn: turn)
      case let .Change(row, col):
        self.squares[row][col].changeStatusTo(.Lighted, withTurn: turn)
      case .Pass:
        break
      }
    }
  }
  
  public func lighDown() {
    for row in Board.RowMin...Board.RowMax {
      for col in Board.ColMin...Board.ColMax {
        self.squares[row][col].changeStatusTo(.Normal, withTurn: .Common)
      }
    }
  }
  
  // 省略

そして、ボードに駒を置いたり、駒を普通の子に変化させたり出来るように、メソッドを追加する。

public class BoardNode: SKSpriteNode {
  // 省略

  private var previousBoards: [Board]
  private var previousActions: [Board.Action]
  public private(set) var board: Board

  // 省略

  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 {
      for updateRow in Board.RowMin...Board.RowMax {
        for updateCol in Board.ColMin...Board.ColMax {
          if (updateRow != row || updateCol != col) &&
             (self.squares[updateRow][updateCol].piece != nil) &&
             (originalBoard.status(updateRow, updateCol) != self.board.status(updateRow, updateCol)) {
            switch self.board.status(updateRow, updateCol) {
            case .Bad:
              self.squares[updateRow][updateCol].piece.changeTo(.Bad)
            case .Common:
              self.squares[updateRow][updateCol].piece.changeTo(.Common)
            case .Good:
              self.squares[updateRow][updateCol].piece.changeTo(.Good)
            default:
              break
            }
          }
        }
      }
      
      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) {
      for updateRow in Board.RowMin...Board.RowMax {
        for updateCol in Board.ColMin...Board.ColMax {
          if (updateRow != row || updateCol != col) &&
             (self.squares[updateRow][updateCol].piece != nil) &&
             (originalBoard.status(updateRow, updateCol) != self.board.status(updateRow, updateCol)) {
            switch self.board.status(updateRow, updateCol) {
            case .Bad:
              self.squares[updateRow][updateCol].piece.changeTo(.Bad)
            case .Common:
              self.squares[updateRow][updateCol].piece.changeTo(.Common)
            case .Good:
              self.squares[updateRow][updateCol].piece.changeTo(.Good)
            default:
              break
            }
          }
        }
      }
      
      let action = SKAction.moveTo(BoardNode.tokenPosition[self.board.token]!, duration: 0.5)
      self.token.runAction(action)
      
      self.userInteractionEnabled = originalUserInteractionEnabled
    }
  }
  
  // 省略

直前のボードの状態を記憶しておいて、駒を置いたり変えた後に、差が出ているマスについて、駒の状態を更新させるようにしている。
なお、ボードの状態をいじっている途中でタッチ操作が有効になっていると面倒なので、その間はタッチ操作を無効にしている。

肝心のタッチ処理の部分がまだだけど、けっこう長くなったので、続きは明日ということで。

今日はここまで!