読者です 読者をやめる 読者になる 読者になる

いものやま。

雑多な知識の寄せ集め

「BirdHead」の仕上げをしてみた。(その6)

昨日はモデルの保存と復元を行った。

今日は復元されたモデルを元に、ビューを復元する処理を実装する。

HandNodeの修正

まずは手札。

//==============================
// BirdHead
//------------------------------
// HandNode.swift
//==============================

import SpriteKit

class HandNode: SKNode {
  // 省略
  
  func restore(cards: [Int]) {
    for card in cards {
      let cardNode = try! CardNode.get(card, withFaceUp: self.isFaceUp)
      cardNode.isDisabled = true
      self.addChild(cardNode)
      self.cardNodes.append(cardNode)
    }
    
    var angle = HandNode.angle * CGFloat(self.cardNodes.count - 1) / 2.0
    for cardNode in self.cardNodes {
      let position = CGPoint(x: -self.radius * sin(angle),
                             y: self.radius * cos(angle) - self.baseY)
      
      cardNode.zRotation = angle
      cardNode.position = position
      
      angle -= HandNode.angle
    }
  }
  
  // 省略
}

// 省略

手札の情報を受け取って、適切な位置に追加していく。

PlayAreaNodeの修正

次はプレイエリア。

//==============================
// BirdHead
//------------------------------
// PlayAreaNode.swift
//==============================

import SpriteKit

class PlayAreaNode: SKNode {
  // 省略
  
  func restore(cards: [Int]) {
    for card in cards {
      let cardNode = try! CardNode.get(card, withFaceUp: true)
      self.addChild(cardNode)
      self.cardNodes.append(cardNode)
    }
    
    self.cardNodes.sortInPlace { $0.card < $1.card }
    
    let width = CardNode.size.width * self.scale * CGFloat(self.cardNodes.count - 1)
                  + PlayAreaNode.margin * CGFloat(self.cardNodes.count - 1)
    var x = -width / 2.0
    for i in 0..<self.cardNodes.count {
      self.cardNodes[i].position.x = x
      self.cardNodes[i].zPosition = 0.0
      self.cardNodes[i].xScale = self.scale
      self.cardNodes[i].yScale = self.scale
      
      x += CardNode.size.width * self.scale + PlayAreaNode.margin
    }
  }
  
  // 省略
}

こちらも同じく、プレイされたカードを適切な位置に追加。

MinusPointInfoNodeとMinusCardListNodeの修正

最後にマイナス点一覧の修正。

//==============================
// BirdHead
//------------------------------
// MinusPointInfoNode.swift
//==============================

import SpriteKit

class MinusPointInfoNode: SKNode {
  // 省略
  
  func restore(cards: [[Int]]) {
    for playerIndex in 0..<4 {
      self.minusCardListNodes[playerIndex].restore(cards[playerIndex])
    }
  }
  
  // 省略
}

まずはMinusPointInfoNodeだけど、こちらは各MinusCardListNodeに丸投げ。

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

import SpriteKit

class MinusCardListNode: SKNode {
  // 省略
  
  func restore(cards: [Int]) {
    for card in cards {
      self.point += card
      let cardNode = try! CardNode.get(card, withFaceUp: true)
      self.addChild(cardNode)
      self.cardNodes.append(cardNode)
    }
    
    var x = -self.frameNode.frame.width/2.0 + CardNode.size.width*self.scale/2.0
    for cardNode in self.cardNodes {
      cardNode.position.x = x
      x += MinusCardListNode.margin + CardNode.size.width*self.scale
    }
    self.pointLabelNode.text = String(format: MinusCardListNode.pointFormat, self.point)
  }
  
  // 省略
}

そして、MinusCardListNodeでは、カードを追加し、マイナス点の合計を増やして、点数の表示を更新している。

GameSceneの修正

あとはこれらのメソッドを呼び出して、実際に復元を行うだけ。

コントローラであるGameSceneを修正する。

//==============================
// BirdHead
//------------------------------
// GameScene.swift
//==============================

import SpriteKit

class GameScene: SKScene, GameInfoObserver, ButtonNodeDelegate {
  // 省略
  
  init(size: CGSize, info: GameInfo, restore: Bool = false) {
    // 省略
    
    if restore {
      self.restoreFromInfo()
    }
  }
  
  // 省略
  
  private func restoreFromInfo() {
    for playerIndex in 0..<4 {
      let playerView = try! self.info.playerViewFor(playerIndex)
      self.handNodes[playerIndex].restore(playerView.hands)
    }
    
    if self.info.actionsInTrick.count > 0 {
      var lastPlayIndex = 0
      let startPlayerIndex = (self.info.playerCount + self.info.turnPlayerIndex
                               - self.info.actionsInTrick.count) % self.info.playerCount
      for offset in 0..<self.info.actionsInTrick.count {
        let playerIndex = (startPlayerIndex + offset) % self.info.playerCount
        let action = self.info.actionsInTrick[offset]
        self.playAreaNodes[playerIndex].restore(action.cards())
        if case .Play = action {
          lastPlayIndex = playerIndex
        }
      }
      for playerIndex in 0..<4 {
        if playerIndex != lastPlayIndex {
          self.playAreaNodes[playerIndex].disableCardNodes()
        }
      }
    }
    
    self.minusPointInfoNode.restore(self.info.minusPointCards)
  }
  
  // 省略
  
  override func didMoveToView(view: SKView) {
    self.actionQueue.addActionBlock {executor in
      executor.startAction()
      self.deckNode.readyToDeal() {
        self.selectQueue.addOperationWithBlock {
          if self.info.inDeal {
            self.selectAndDoAction()
          } else if !self.info.isEnd {
            try! self.info.deal()
          } else {
            self.actionQueue.addActionBlock { executor in
              self.minusPointInfoNode.okButtonNode.userInteractionEnabled = false
              if self.minusPointInfoNode.parent == nil {
                self.responseLayer.addChild(self.minusPointInfoNode)
              }
              executor.startAction()
              self.minusPointInfoNode.show {
                executor.endAction()
              }
            }
            self.gameInfoEnded()
          }
        }
        executor.endAction()
      }
    }
  }
  
  override func willMoveFromView(view: SKView) {
    self.info.removeAllObservers()
    self.infoButtonNode.buttonNodeDelegate = nil
    self.minusPointInfoNode.okButtonNode.buttonNodeDelegate = nil
    self.minusPointInfoNode.exitButtonNode.buttonNodeDelegate = nil

    _ = self.players.map { $0.cancelSelect() }
    self.selectQueue.cancelAllOperations()
    self.actionQueue.cancelAllOperations()
    self.interruptionQueue.cancelAllOperations()
    self.okButtonNodeCompletions.removeAll()
    
    NSNotificationCenter.defaultCenter().postNotificationName(GameInfo.deleteGameInfoKey, object: nil)
    
    self.resume()
  }
  
  // 省略
}

GameScene#restoreFromInfo()でビューの状態を復元。

それと、GameScene#didMoveToView(_: SKView)では、現在の状態に応じて、

  • ディール中なら、アクションの選択と実行
  • ディール外で、ゲームが終了していなければ、ディールを始める
  • ディール外で、ゲームが終了していれば、マイナス点一覧を表示して、結果も表示。

としている。

あと、GameScene#willMoveFromView(_: SKView)では、保存しているモデルを削除するようにしている。

ViewController

そういえば、ViewControllerのコードをブログには載せてなかったので、せっかくだから。

//==============================
// BirdHead
//------------------------------
// ViewController.swift
//==============================

import UIKit
import SpriteKit

class ViewController: UIViewController {
  @IBOutlet weak var skView: SKView!
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    CardNode.loadAssets()
    MinusPointInfoNode.loadAssets()
  }
  
  override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
  
    var size = self.view.bounds.size
    if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
      RuleScene.scale = 2.0
      size.width *= 2.0
      size.height *= 2.0
    }
    
    if let gameInfo = GameInfo.fromSaveData() {
      let gameScene = GameScene(size: size, info: gameInfo, restore: true)
      self.skView.presentScene(gameScene)
    } else {
      let startScene = StartScene(size: size)
      self.skView.presentScene(startScene)
    }
  }
  
  override func prefersStatusBarHidden() -> Bool {
    return true
  }
}

保存されたモデルがあれば、それを使ってゲームを再開。
そうでなければ、スタート画面を表示、という感じ。

今日はここまで!