いものやま。

雑多な知識の寄せ集め

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

素材作りは昨日でとりあえず完了。

今日からはコーディングをしていく。

駒の描画

まずは、駒を描画するところから。

ということで、SKSpriteNodeを継承して、PieceNodeを作る。

駒の状態

まず、駒の状態を表す列挙型を定義。

//==============================
// YWF
//------------------------------
// PieceNode.swift
//==============================

import SpriteKit

public class PieceNode: SKSpriteNode {
  public enum Status {
    case Bad
    case Common
    case Good
  }
  
  // 続く

以前書いたBoard.swiftとちょっと違って、壁や空白というのは不要なので、列挙子は3つだけ。
それに、列挙子の値を使って計算をするとかもないので、値は特に定義していない。

画像のロードとテンプレートの作成

次に、Adventureと同様に、画像をロードしてテンプレートを作成する。

  // 続き
  
  private static var textures = [String: SKTexture]()
  private static var toCommonAnimations = [Status: [SKTexture]]()
  private static var fromCommonAnimations = [Status: [SKTexture]]()
  private static var rotateAnimations = [Status: [SKTexture]]()
  private static var templates = [Status: PieceNode]()
  
  public class func loadAssetsAndCreateTemplates() {
    let atlas = SKTextureAtlas(named: "Piece")
    let names = [
      "Bad", "CommonBad", "CommonGood", "Good",
      "BadCommon01", "BadCommon02", "BadCommon03",
      "GoodCommon01", "GoodCommon02", "GoodCommon03",
      "CommonBadCommonGood01", "CommonBadCommonGood02", "CommonBadCommonGood03",
      "CommonGoodCommonBad01", "CommonGoodCommonBad02", "CommonGoodCommonBad03",
    ]
    for name in names {
      PieceNode.textures[name] = atlas.textureNamed(name)
    }
    
    PieceNode.toCommonAnimations[.Bad] = [SKTexture]()
    for name in ["BadCommon01", "BadCommon02", "BadCommon03", "CommonBad"] {
      PieceNode.toCommonAnimations[.Bad]!.append(PieceNode.textures[name]!)
    }
    PieceNode.toCommonAnimations[.Good] = [SKTexture]()
    for name in ["GoodCommon01", "GoodCommon02", "GoodCommon03", "CommonGood"] {
      PieceNode.toCommonAnimations[.Good]!.append(PieceNode.textures[name]!)
    }

    PieceNode.fromCommonAnimations[.Bad] = [SKTexture]()
    for name in ["BadCommon03", "BadCommon02", "BadCommon01", "Bad"] {
      PieceNode.fromCommonAnimations[.Bad]!.append(PieceNode.textures[name]!)
    }
    PieceNode.fromCommonAnimations[.Good] = [SKTexture]()
    for name in ["GoodCommon03", "GoodCommon02", "GoodCommon01", "Good"] {
      PieceNode.fromCommonAnimations[.Good]!.append(PieceNode.textures[name]!)
    }

    PieceNode.rotateAnimations[.Bad] = [SKTexture]()
    for name in ["CommonGoodCommonBad01", "CommonGoodCommonBad02", "CommonGoodCommonBad03", "CommonBad"] {
      PieceNode.rotateAnimations[.Bad]!.append(PieceNode.textures[name]!)
    }
    PieceNode.rotateAnimations[.Good] = [SKTexture]()
    for name in ["CommonBadCommonGood01", "CommonBadCommonGood02", "CommonBadCommonGood03", "CommonGood"] {
      PieceNode.rotateAnimations[.Good]!.append(PieceNode.textures[name]!)
    }

    PieceNode.templates[.Bad] = PieceNode(.Bad, previous: .Common)
    PieceNode.templates[.Common] = PieceNode(.Common, previous: .Bad)
    PieceNode.templates[.Good] = PieceNode(.Good, previous: .Common)
  }
  
  public class func get(status: Status) -> PieceNode {
    return PieceNode.templates[status]!.copy() as! PieceNode
  }
  
  private class func getTextureFor(status: Status, previous previousStatus: Status) -> SKTexture {
    let texture: SKTexture?
    switch status {
    case .Bad:
      texture = PieceNode.textures["Bad"]
    case .Good:
      texture = PieceNode.textures["Good"]
    case .Common:
      switch previousStatus {
      case .Bad, .Common:
        texture = PieceNode.textures["CommonBad"]
      case .Good:
        texture = PieceNode.textures["CommonGood"]
      }
    }
    return texture!
  }

  // 続く

まず、画像のロードと、アニメーションに関して。

駒を立たせて「普通の子」を表現するとしたけど、その場合、「普通の子」には、2つの状態がある。

  • 悪い子を立たせて普通の子にした状態(黒が手前になる)
  • 良い子を立たせて普通の子にした状態(白が手前になる)

これらをそれぞれ、CommonBad.pngとCommonGood.pngとして用意した。

また、このように普通の子に2つの状態があるので、駒の状態の変化を列挙すると、

  • 悪い子→普通の子(黒が手前)
  • 良い子→普通の子(白が手前)
  • 普通の子(黒が手前)→悪い子
  • 普通の子(白が手前)→良い子
  • 普通の子(黒が手前)→普通の子(白が手前)→良い子
  • 普通の子(白が手前)→普通の子(黒が手前)→悪い子

の6通りがあることになる。

そこで、アニメーションを

  • 普通の子にするアニメーション
    • 悪い子から普通の子(黒が手前)にする
    • 良い子から普通の子(白が手前)にする
  • 普通の子から悪い子/良い子にするアニメーション
    • 普通の子(黒が手前)から悪い子にする
    • 普通の子(白が手前)から悪い子にする
  • 普通の子の状態を切り替える(回転させる)アニメーション
    • 普通の子(黒が手前)から普通の子(白が手前)にする
    • 普通の子(白が手前)から普通の子(黒が手前)にする

と、6つ用意することにする。

ここらへんを踏まえて、toCommonAnimations、fromCommonAnimations、rotateAnimationsというクラス変数を用意している。
(クラス変数にしているのは、画像は共有して使うから)

なお、画像はPiece.atlasというTextureAtlasにしているので、これを読み込んで、texturesというクラス変数へ格納している。

あとは、テンプレートの作成。

Adventureでは、コンストラクタを毎回呼び出すのではなく、テンプレートのオブジェクトをコピーして使っていたので、同じようにした。
ただ、呼び出し側がコピーするのもなんなので、クラスメソッドでコピーを取得できるようにしてある。

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

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

  // 続き

  private var previousStatus: Status
  private var status: Status
  
  private override init(texture: SKTexture!, color: UIColor!, size: CGSize) {
    self.previousStatus = .Common
    self.status = .Bad
    super.init(texture: texture, color: color, size: size)
  }
  
  private convenience init(_ status: Status, previous previousStatus: Status) {
    let texture = PieceNode.getTextureFor(status, previous: previousStatus)
    
    self.init(texture: texture,
              color: SKColor.whiteColor(),
              size: texture.size())
    
    self.status = status
    self.previousStatus = previousStatus
  }

  public required init?(coder aDecoder: NSCoder) {
      fatalError("init(coder:) has not been implemented")
  }
  
  public override func copyWithZone(zone: NSZone) -> AnyObject {
    var piece = super.copyWithZone(zone) as! PieceNode
    piece.status = self.status
    piece.previousStatus = self.previousStatus
    return piece
  }

  // 続く

普通の子に2つの状態があることから、今の状態だけでなく、直前の状態も保持するようにしてある。

つまり、今の状態が普通の子のときに、

  • 直前の状態が悪い子なら、黒が手前
  • 直前の状態が良い子なら、白が手前

という感じ。

イニシャライザの方は、SKSpriteNodeの指定イニシャライザをオーバーライドしている。
なんかこれを定義しておかないと、エラーになったので。(copy()で必要?)

なお、SKSpriteNodeの指定イニシャライザを呼ばないといけないので、注意。
SKSpriteNode#init(texture: SKTexture?)を使おうとしたら、簡易イニシャライザなので呼べなかった・・・

そして、実際に使う簡易イニシャライザを定義。
ただ、この簡易イニシャライザを呼ぶのは自分だけなので、可視性はprivateに。

あとは、SKSpriteNodeがNSCodingプロトコルに準拠してるので、ダミーのイニシャライザを用意したり。

それと、copy()でコピーできるように、copyWithZone(_ zone: NSZone)を定義している。

状態の変化

残るは、状態を変化させるメソッドのみ。

  // 続き

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

    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))

    let action = SKAction.sequence([preAction, waitAction, changeAction])
    self.runAction(action)
    
    self.previousStatus = self.status
    self.status = status
  }
}

駒にアニメーションのアクションを適用して、状態を変化させている。

なお、普通の子(手前が白)→悪い子の変化や普通の子(手前が黒)→良い子の変化だと、駒を寝かせる前に、駒を回転させて手前の色を変える必要があるので、これをpreActionとしている。
そして、これが不要の場合、同じ時間だけ待たせるようにしてある。

駒の描画の動作確認

駒が出来たので、これを実際に描画させてみる。

Main.storyboardにSKViewを用意し、さらにボタンを3つ(Bad/Common/Good)用意。
SKViewにはボードと駒を1つ描画させて、ボタンを押すと駒の状態が変わるようにしてみた。

//==============================
// YWF
//------------------------------
// GameViewController.swift
//==============================

import UIKit
import SpriteKit

class GameViewController: UIViewController {
  @IBOutlet weak var skView: SKView!
  @IBOutlet weak var badButton: UIButton!
  @IBOutlet weak var commonButton: UIButton!
  @IBOutlet weak var goodButton: UIButton!
  
  private var piece: PieceNode!
  
  override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    
    var size = self.view.bounds.size
    if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
      size.height *= 2
      size.width *= 2
    }
  
    let scene = GameScene(size: size)
    scene.scaleMode = .AspectFill
    
    let atlas = SKTextureAtlas(named: "Board")
    let backgroundTexture = SKTexture(imageNamed: "background")
    let background = SKSpriteNode(texture: backgroundTexture)
    background.position = CGPoint(x: scene.frame.width/2, y: scene.frame.height/2)
    scene.addChild(background)
    let tokenTexture = atlas.textureNamed("token")
    let token = SKSpriteNode(texture: tokenTexture)
    token.position = CGPoint(x: -300, y: 0)
    background.addChild(token)
    
    PieceNode.loadAssetsAndCreateTemplates()
    self.piece = PieceNode.get(.Bad)
    self.piece.position = CGPoint(x: 0, y: 0)
    background.addChild(self.piece)
    self.badButton.enabled = false
    self.goodButton.enabled = false

    self.skView.presentScene(scene)
  }
  
  @IBAction func chooseBad(sender: UIButton) {
    self.piece.changeTo(.Bad)
    
    self.badButton.enabled = false
    self.commonButton.enabled = true
    self.goodButton.enabled = false
  }
  
  @IBAction func chooseCommon(sender: UIButton) {
    self.piece.changeTo(.Common)
    
    self.badButton.enabled = true
    self.commonButton.enabled = false
    self.goodButton.enabled = true
  }
  
  @IBAction func chooseGood(sender: UIButton) {
    self.piece.changeTo(.Good)
    
    self.badButton.enabled = false
    self.commonButton.enabled = true
    self.goodButton.enabled = false
  }
  
  // hide status bar.
  override func prefersStatusBarHidden() -> Bool {
    return true
  }
}

なお、見ての通り、並列処理とかはしないで、割とベタw

これを実行してみると、次のような感じ。

うん、いい感じ♪

今日はここまで!