SpriteKit で 15パズルを作ってみた

今更ながら SpriteKit を触ってみたのでメモ。
とりあえずの習作として選んだのは 15パズル。
後でコードを見て「ん?」ってなりそうな部分についての覚書。

ソースコードはここから github: puzzle15

SkSpriteNodeの Flipアニメーション

パズルが完成したら左上から順番に駒をひっくり返して別の絵が表示されるようにしてある。ここで駒をひっくり返す部分に Flipアニメーションを使用している。
Flipアニメーションは SpriteKit に標準で用意されているアニメーションではないので既存のものを組み合わせる必要がある。

以下、その手順

・SKAktion.scaleX(to: 0, duration:) でスプライトの幅を 0にするアニメーションを起動
・スプライトのテクスチャを裏面のテクスチャへ変更
・SKAnimation.scaleX(to: 1, duration:) でスプライトの幅を 1にするアニメーションを起動

これをコードにするとこんな感じ。

let faceHalfFlip = SKAction.scaleX(to: 0.0, duration: 0.5)
let backHalfFlip = SKAction.scaleX(to: 1.0, duration: 0.5)
    
sprite.run(faceHalfFlip, completion: {
    sprite.texture = backTexture
    sprite.run(backHalfFlip)
})

実際のコードでは wait を入れたりして使用している。

Array2D(2次元配列)

15パズルは 4×4 の駒をスライドさせて遊ぶパズルなので、なんとなく2次元配列があると良いかな?と思って作ったクラス。
名前は 2次元配列そのまんま。(この時 matrix は思い浮かばなかった…)
結局、subscribe を 1次元、2次元の両方用意してどちらでもアクセス可能にしたり、Sequence Protocol で for in で使えるようにしたり、正直「2次元配列で作る必要なかったな」とは思う。

func makeIterator() -> AnyIterator<T?> {
    var index = 0
    return AnyIterator {
        defer { index += 1 }
        guard index < self.array.count else { return nil }
        return self.array[index]
    }
}

makeIterator() の部分はオリジナルの Array の Iterator を返すとかできそうだけど…

15パズルの初期配置

当初は正解の状態から 15番目の駒を外して、ここから 5000回ほどランダムに駒をタップして問題としていた。(「駒をタップ」は動かせない駒をタップすることもあるので、実際に駒は 5000回動いてない)

private func shuffleByMove() {
    for _ in 0..<5000 {
        let x = Int(arc4random_uniform(4))
        let y = Int(arc4random_uniform(4))
        if canMoveFrom(colmn: x, row: y) {
            let _ = moveFrom(colmn: x, row: y)
        }
    }
}


けれど、実際に駒が動く様子が見られるならいざ知らず、そうでないのにここで 5000回もループするのはどんなもんか?と思い、ランダムに駒を配置する方法に変更した。
が、全くランダムに配置すると絶対に解けない配置になることがあるので、ランダムに配置した後でそれが解ける配置か解けない配置かを確認する必要がある。
それが canSolve() 関数。

private func canSolve(_ gameBoard: Array2D<PuzzlePiece>) -> Bool {
    var count = 0
    var table = gameBoard.map {$0?.number}
        
    for (i, number) in table.enumerated() {
        if i == 15 { break }
        if i == number { continue }
        for j in i+1...15 {
            if table[j] == i {
                table[j] = table[i]
                table[i] = i
                count += 1
                break
            }
        }
    }
    return count % 2 == 0 ? true : false
}

以下、簡単に説明

1. 最初に添字 0-14までの 15マスにランダムに配置された駒の番号の table を作成。
2. その table の添字 X番目に同じ番号が入っているかどうかを確認
3. 違っていたらそれより大きな添字の中からX番目に入るべき番号が入っているものを探す。
4. 見つかったらそれと X番目を交換して、count を1つ増やす。
5. こうして 2-4 を繰り返して table が [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] になったら終了。
6. この時点で count が偶数ならパズルは解ける、奇数なら解けない。

詳細は以下のページに詳しく解説されている。

8パズル,15パズルの不可能な配置と判定法

SpriteKit と MVC

最後に SpriteKit で MVC を意識すると ViewController と SKScene はどういう関係になるのだろう?という疑問。
ViewController がいらなくなるというか、SKScene(SKView) が VC になってる気がするんだけど。
今回わざわざ PuzzleGame と GameScene の間に ViewController 入れてメッセージ送っているけど ViewController ない方がいい感じになりそう。正直 ViewController は Start ボタンのタップを受け取ってるだけだし。
ただ、規模が大きくなるとどんどん SKScene が肥大化していってこれまた大変になりそう…

次回は

もう少しゲームっぽいものを作ってみようということで、最近ちょっと縁のあったインベーダーゲームを作ってみる予定。

Leave a Reply

Your email address will not be published. Required fields are marked *

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)