いものやま。

雑多な知識の寄せ集め

「BirdHead」のUIを作ってみた。(その8)

昨日はアクションを実行するためのコードに関してリファクタリングを実施した。

今日はマイナス点一覧に使う部品を実装していく。

必要なもの

マイナス点一覧を表示するときに、必要になるものをリストアップしてみると、以下。

  • ボタン
  • テキスト(プレイヤー名など)
  • マイナス点となるカードのリスト

ここで、テキストはSKLabelNodeを使えばいいので、残りの2つについて実装していく。

ButtonNode

まずはボタンから。

これは簡単で、YWFのときと同じ感じで実装。

//==============================
// BirdHead
//------------------------------
// ButtonNode.swift
//==============================

import SpriteKit

class ButtonNode: SKSpriteNode {
  weak var buttonNodeDelegate: ButtonNodeDelegate?
  
  init(texture: SKTexture?) {
    self.buttonNodeDelegate = nil
    
    super.init(texture: texture, color: SKColor.clearColor(), size: (texture?.size())!)
    
    self.userInteractionEnabled = true
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
    let touch = touches.first!
    let location = touch.locationInNode(self.parent!)
    if self.containsPoint(location) {
      self.buttonNodeDelegate?.buttonNodeIsSelected(self)
    }
  }
}

protocol ButtonNodeDelegate: class {
  func buttonNodeIsSelected(buttonNode: ButtonNode)
}

ただ、オブザーバパターンを使ったYWFのときと違って、デリゲートを使っている。
理由は、どうせデリゲート先となるのはコントローラ(となるシーン)だけだからw

一つ、デリゲートを使う場合のSwiftでのベストプラクティスとして、デリゲート先のオブジェクトに対する参照は、弱い参照にして、オプショナル型にしておく、というのがある。
こうしておくと、参照が循環してしまうのを防げ、また、オプショナルチェーン?.を使うことで簡単にメソッドを呼び出すことが出来る。

MinusCardListNode

続いて、マイナス点となるカードのリスト。

//==============================
// BirdHead
//------------------------------
// MinusCardListNode.swift
//==============================

import SpriteKit

class MinusCardListNode: SKNode {
  // 続く

クラス定数

まずはクラス定数から。

  // 続き

  private static let actionDuration = NSTimeInterval(0.3)
  private static let margin: CGFloat = 10.0
  private static let fontSize: CGFloat = 48.0
  private static let pointFormat: String = "%d pt"
  
  // 続く

マージンは、カードの間をどれくらい開けるのかという値。

あと、フォントサイズとテキストのフォーマットを定義しているけど、これは、マイナス点のカードのリストと一緒に、分かりやすいようにマイナス点の合計も表示するためのもの。

プロパティとイニシャライザ

次はプロパティとイニシャライザ。

  // 続き

  private(set) var point: Int
  
  private let scale: CGFloat
  
  private let frameNode: SKShapeNode
  private var cardNodes: [CardNode]
  private let pointLabelNode: SKLabelNode
  
  private let actionQueue: ActionQueue
  
  override var frame: CGRect {
    var frame = self.frameNode.frame
    frame.origin.x += self.position.x
    frame.origin.y += self.position.y
    return frame
  }
  
  init(size: CGSize) {
    self.point = 0
    
    self.scale = min(1.0, size.height / CardNode.size.height)
    
    self.frameNode = SKShapeNode(rectOfSize: size)
    self.frameNode.lineWidth = 0.0
    self.cardNodes = [CardNode]()
    
    self.pointLabelNode = SKLabelNode(text: String(format: MinusCardListNode.pointFormat, self.point))
    self.pointLabelNode.fontColor = SKColor.blackColor()
    self.pointLabelNode.fontSize = MinusCardListNode.fontSize*self.scale
    self.pointLabelNode.horizontalAlignmentMode = .Right
    self.pointLabelNode.verticalAlignmentMode = .Center
    
    self.actionQueue = ActionQueue()
    
    super.init()
    
    self.addChild(self.frameNode)
    
    self.pointLabelNode.position.x = self.frameNode.frame.width/2.0
    self.addChild(self.pointLabelNode)
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  // 続く

プロパティでスケールを用意しているのは、PlayAreaNodeと同様に、カードがフレームに収まるようにするため。
もっとも、幅は十分あるので、高さだけチェックをするようにしている。

それと、マイナス点の合計を表示するためのラベルノードも用意してある。

一つ、注目して欲しいのは、昨日作ったActionQueueを使っているところ。
これを使ってアクションを登録する様子は、以下で。

カードの追加

さて、カードの追加。
マイナス点となるカードを追加し、マイナス点の合計を表示しているラベルを更新する。

  // 続き
  
  func addCardNode(cardNode: CardNode, completion: (() -> Void)! = nil) {
    self.actionQueue.addActionBlock { executor in
      cardNode.xScale = self.scale
      cardNode.yScale = self.scale
      
      let x = (-self.frameNode.frame.width/2.0
                 + (CardNode.size.width*self.scale + MinusCardListNode.margin)*CGFloat(self.cardNodes.count)
                 + CardNode.size.width*self.scale/2.0)
      cardNode.position = CGPoint(x: x, y: MinusCardListNode.margin)
      cardNode.alpha = 0.0
      self.addChild(cardNode)
      self.cardNodes.append(cardNode)
      
      let moveAction = SKAction.moveByX(0.0, y: -MinusCardListNode.margin, duration: MinusCardListNode.actionDuration)
      let fadeInAction = SKAction.fadeInWithDuration(MinusCardListNode.actionDuration)
      let addAction = SKAction.group([moveAction, fadeInAction])
      executor.executeAction(addAction, forNode: cardNode)
      
      self.point += cardNode.card
      let fadeOutAction = SKAction.fadeOutWithDuration(MinusCardListNode.actionDuration/2.0)
      executor.executeAction(fadeOutAction, forNode: self.pointLabelNode) {
        self.pointLabelNode.text = String(format: MinusCardListNode.pointFormat, self.point)
        let fadeInAction = SKAction.fadeInWithDuration(MinusCardListNode.actionDuration/2.0)
        executor.executeAction(fadeInAction, forNode: self.pointLabelNode)
      }
    }
    
    if completion != nil {
      self.actionQueue.addBlock(completion)
    }
  }
}

見ての通り、キューを一時停止したり再開させたりする処理を書く必要はなく、メインキューに投げる処理を書く必要もなくなっている。

肝心の処理は、カードを追加する位置を計算して、その位置にカードをスッと差し込むようなアクションを実行している。
(具体的には、フェードインと、上→下の移動を組合せたアクションを実行している)
このとき、単にSKNode#runAction(_: SKAction)を使うのではなく、ActionQueue.Executor#executeAction(_: SKAction, forNode: SKNode)を使っているのがポイント。
こうすることで、終わりを待つ必要のある処理の数を自前で管理する必要がなくなっている。

また、マイナス点の合計を表示しているラベルについても、一度フェードアウトさせ、表示を更新し、再度フェードインで表示させるとしている。

これで部品は用意できたので、明日はこれらを組合せて、マイナス点一覧を実装していく。

今日はここまで!