いものやま。

雑多な知識の寄せ集め

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

昨日はカードの表示を実装した。

今日はデッキの表示を実装していく。

デッキとフレーム

デッキは、単純に考えると中央にカードを表示するだけなんだけど、実際には各プレイヤーにカードを配る必要があるので、もうちょい工夫が必要。

具体的には、デッキの他に「フレーム」という枠を用意しておいて、カードを配る場合、そのフレームの外へカードを配るようにする。

f:id:yamaimo0625:20151105060726p:plain

DeckNode

ということで、デッキの表示の実装。

DeckNodeはSKNodeを継承して実装した。

//==============================
// BirdHead
//------------------------------
// DeckNode.swift
//==============================

import SpriteKit

class DeckNode: SKNode {
  // 続く

カードを配る方向、クラス定数

まずはカードを配る方向を列挙型として定義し、アニメーションの時間をクラス定数として定義した。

  // 続き

  enum DealDirection {
    case Top
    case Right
    case Bottom
    case Left
  }
  
  private static let actionDuration = NSTimeInterval(0.3)

  // 続く

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

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

  // 続き
  
  private let deckPosition: CGPoint
  private let frameSize: CGSize
  
  private let frameNode: SKShapeNode
  private let dummyCardNode: CardNode
  
  private let actionQueue: NSOperationQueue
  
  init(deckPosition: CGPoint, frameSize: CGSize) {
    self.deckPosition = deckPosition
    self.frameSize = frameSize
    
    self.frameNode = SKShapeNode(rectOfSize: self.frameSize)
    self.frameNode.lineWidth = 0.0
    self.dummyCardNode = try! CardNode.get(2, withFaceUp: false)
    self.dummyCardNode.position = self.deckPosition
    
    self.actionQueue = NSOperationQueue()
    self.actionQueue.maxConcurrentOperationCount = 1
    
    super.init()
    
    self.addChild(self.frameNode)
  }

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

  // 続く

説明が必要なところとして、DeckNode#dummyCardNodeがありそう。
これは、「次に配られるカード」のダミー。
ビューでは、実際にモデル側でデッキの並びがどうなっているのかは知らない(し、知る必要もない)けど、

  • まだ山札が残っている→次のカードの裏側が見える
  • もう山札が残っていない→次のカードはない

ので、まだ山札が残っている場合には、次のカードの裏側の代わりとして、ダミーのカードの裏側を表示するようにする。

それと、DeckNode#actionQueueというのは、SpriteKitでアクションを確実に1つずつ実行する方法について。 - いものやま。で説明した、アクションを確実に1つずつ実行するためのオペレーションキュー。

カードを配る準備

次は、カードを配る準備。

  // 続き
  
  func readyToDeal(completion: (() -> Void)! = nil) {
    if self.dummyCardNode.parent == nil {
      self.actionQueue.addOperationWithBlock {
        self.actionQueue.suspended = true
        
        NSOperationQueue.mainQueue().addOperationWithBlock {
          self.dummyCardNode.alpha = 0.0
          self.insertChild(self.dummyCardNode, atIndex: 0)
          let fadeInAction = SKAction.fadeInWithDuration(DeckNode.actionDuration)
          self.dummyCardNode.runAction(fadeInAction) {
            self.actionQueue.suspended = false
          }
        }
      }
    }
    
    if completion != nil {
      self.actionQueue.addOperationWithBlock {
        NSOperationQueue.mainQueue().addOperationWithBlock {
          completion()
        }
      }
    }
  }

  // 続く

ダミーカードが表示されていない(=ダミーカードの親ノードが存在しない)場合には、ダミーカードをノードに追加して、フェードインさせるようにしている。
そして、コンプリーションが指定されていた場合、フェードインが完全に終わった後にコンプリーションが実行されるようにしている。

カードの分配

最後にカードの分配。

  // 続き
  
  func deal(card: Int, direction: DeckNode.DealDirection, deckRemain: Bool,
            completion: ((CardNode) -> Void)! = nil)
  {
    let cardNode = try! CardNode.get(card, withFaceUp: false)
    
    self.actionQueue.addOperationWithBlock {
      self.actionQueue.suspended = true
      
      NSOperationQueue.mainQueue().addOperationWithBlock {
        cardNode.position = self.deckPosition
        self.addChild(cardNode)
        if !deckRemain {
          self.dummyCardNode.removeFromParent()
        }
        let moveAction: SKAction
        switch direction {
        case .Top:
          let y = self.frameSize.height/2.0 + CardNode.size.height
          moveAction = SKAction.moveToY(y, duration: DeckNode.actionDuration)
        case .Right:
          let x = self.frameSize.width/2.0 + CardNode.size.width
          moveAction = SKAction.moveToX(x, duration: DeckNode.actionDuration)
        case .Bottom:
          let y = -self.frameSize.height/2.0 - CardNode.size.height
          moveAction = SKAction.moveToY(y, duration: DeckNode.actionDuration)
        case .Left:
          let x = -self.frameSize.width/2.0 - CardNode.size.width
          moveAction = SKAction.moveToX(x, duration: DeckNode.actionDuration)
        }
        let rotateAction = SKAction.rotateByAngle(CGFloat(M_PI), duration: DeckNode.actionDuration)
        let dealAction = SKAction.group([moveAction, rotateAction])
        cardNode.runAction(dealAction) {
          self.actionQueue.suspended = false
        }
      }
    }
    
    if completion != nil {
      self.actionQueue.addOperationWithBlock {
        NSOperationQueue.mainQueue().addOperationWithBlock {
          completion(cardNode)
        }
      }
    }
  }
}

配るように指定されたカードのノードを生成し、それを指定された方向へ180度回転させながら、フレームの外側に出るまで移動するアニメーションを実行する。
ここでもし、デッキの残りがない場合、ダミーのカードは取り除くようにしている。(そうしないと、デッキの残りがないにもかかわらず、まだデッキに残りがあるかのように見えてしまう)
そして、コンプリーションが指定されていた場合、そのアニメーションが終わった後でコンプリーションが実行されるようにしてある。

なお、このコンプリーションは配られたカードを引数として呼び出されるので、ここで配られたカードを手札に入れる処理などを行うことになる。

今日はここまで!