素材作りは昨日でとりあえず完了。
今日からはコーディングをしていく。
駒の描画
まずは、駒を描画するところから。
ということで、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
今日はここまで!