【入門】iOS アプリ開発　#2【SpriteKit を使う】

# SpriteKit

iOS向けの2Dゲーム開発用として、SpriteKit というフレームワークが提供されている。
 
今回、古典的なピクセルアートのゲーム「パックマン」を作るため、スプライトのキャラクタをもっと簡単に扱えるようにしたい。

キャラクタは基本 16x16ドットで構成されており、テクスチャの切り替えアニメーションも考慮すると、オブジェクトで管理するのは大変そう。

そのため１つの大きなアセット画像を 16x16ドットで切り出して、テクスチャ番号で扱えるようにする。

スプライトもオブジェクト管理ではなく、スプライト管理番号で扱えるようなクラスを作成する。
 
# Sprite Manager class の作成

スプライトを表示する API はシンプルに、

```swift
draw(0, x:100, y:50, texture:1)
``` 

とする。
 
また一定間隔(0.1s)でアニメーションする API は、
 
```swift
startAnimation(0, sequence: [1,2,3], timePerFrame: 0.1, repeat: true)
``` 
 
という感じにする。

このような API を持つ、次の CgAssetManager クラスを継承する CgSpriteManager クラスを作成する。

 
# Asset Manager class の作成
テストで使用するアセット画像は下記で、これを 16x16ドットで切り出して、テクスチャ番号を割り当てる CgAssetManager クラスを作成する。

[spriteTest.png]

<img src="https://github.com/Kikutada/0001_SpritekitTest/blob/master/0001_SpritekitTest/Assets.xcassets/spriteTest.imageset/spriteTest.png?raw=true" width=256>

テクスチャ番号は左下から右上へ、下記のように割り当てる。

| 4 | 5 | 6 | 7 |
|---|---|---|---|
| 0 | 1 | 2 | 3 |

 
# スプライト描画のプログラム

```swift
// Create a sprite manager object.
let sprite = CgSpriteManager(view: self, imageNamed: "spriteTest.png", width: 16, height: 16, maxNumber: 64)
        
// Draw a #0 sprite with #1 texture at (8,8) position.
sprite.draw(0, x: 8, y: 8, texture: 1)
```

CgSpriteManagerクラスは、"spriteTest.png" のアセット画像から 16x16ドットでテクスチャを切り出し、同時に最大64個のスプライトを描画するための管理番号を持つ SpriteManager オブジェクトを生成する。

SpriteManagerオブジェクトの draw API で、スプライト管理番号0とし、ピクセル座標(8, 8)にテクスチャ番号1のスプライトを描画する。
 
# Background Manager class の作成

背景となるタイル描画も必要で、8x8ドットのピクセル文字も再現したい。
 
これらも同様に、大きな背景アセット画像から 8x8ドットのテクスチャを切り出し、テクスチャ番号で扱えるようにする。
 
背景のタイル描画 API は、

```swift
put(0, column: 2, row: 3, texture: 1)
``` 
 
とする。
 
また、ピクセル文字を表示する API は、
 
```swift
putString(0, column: 2, row: 4, string: "TEST")
``` 
 
という感じにする。

テストで使用する１つのアセット画像は下記で、SpriteManagerと同様に、8x8ドットに切り出してテクスチャ番号を割り当てる CgAssetManager クラスを継承して、CgBackgroundManager クラスを作成する。

[backgroundTest.png]

<img src="https://github.com/Kikutada/0001_SpritekitTest/blob/master/0001_SpritekitTest/Assets.xcassets/backgroundTest.imageset/backgroundTest.png?raw=true" width=384>

# 背景描画のプログラム

```swift
// Create a background manager object.
let background = CgBackgroundManager(view: self, imageNamed: "backgroundTest.png", width: 8, height: 8, maxNumber: 2)

// Draw a #0 background of (28x36) size at (14*8,18*8) position.
background.draw(0, x: 14*8, y: 18*8, columnsInWidth: 28, rowsInHeight: 36)

// Put a #1 texture on #0 background at (14,19).
background.put(0, column: 14, row: 19, texture: 1)

// Print text on #0 background at (8,18).
background.putString(0, column: 8, row: 18, string: "SPRITEKIT TEST", offset: -16*2 /* ASCII offset */)
```

CgBackgroundManagerクラスは、"backgroundTest.png" のアセット画像から 8x8ドットのテクスチャを切り出し、同時に最大2面の背景を描画するための管理番号を持つ BackgroundManager オブジェクトを生成する。

BackgroundManagerオブジェクトの draw API で、背景管理番号0座標(14*8, 18*8)に 28x36 のタイル面を生成・描画する。１つのタイルサイズは、テクスチャサイズの 8x8ドットとなる。

put API は、背景管理番号 0 のタイル位置(14, 19)にテクスチャ番号 1 のタイルを描画する。

putString API では、背景管理番号 0 のタイル位置(8, 18)に "SPRITEKIT TEST" のタイルを描画する。先頭文字 "S" の ASCIIコードは 83 となり、offset: -16*2 を引いた 51番のテクスチャを描画する仕組みとなる。

#テスト・プログラム

[GitHub](https://github.com/Kikutada/0001_SpritekitTest) に公開しているテスト・プログラムを実行すると、以下のような画面が表示され、赤モンスターがアニメーションしながら移動する。

<img src="https://github.com/Kikutada/0001_SpritekitTest/blob/master/imgaes/0001_spritekitTest.png?raw=true" width=288>

Asset/Sprite/Background Manager クラスは、SpritekitManager.swift ファイルにコーディングしている。コメント入れて 500行未満。

GameViewController.swift では、GameSceneのサイズを (28*8, 36*8)ドットに変更している。

```swift
class GameViewController: UIViewController {

    private var scene: SKScene!
    
    override func viewDidLoad() {
       super.viewDidLoad()
       
       if let view = self.view as! SKView? {
           
           let size = CGSize(width: 28*8, height: 36*8)
           scene = GameScene(size: size)
           
           // Set background color to black
           scene.backgroundColor = UIColor.black

           // Set the scale mode to scale to fit the window
           scene.scaleMode = .aspectFit
           
           // Present the scene
           view.presentScene(scene)
           
           view.ignoresSiblingOrder = true
           
           view.showsFPS = true
           view.showsNodeCount = true
       }
    }
```

GameScene.swift の didMove関数で Managerオブジェクトの生成と描画、update関数で赤モンスターを移動させている。

```swift
class GameScene: SKScene {
    
    private var sprite: CgSpriteManager!
    private var background: CgCustomBackground!
    
    override func didMove(to view: SKView) {

        // Create sprite and background objects.
        sprite = CgSpriteManager(view: self, imageNamed: "spriteTest.png", width: 16, height: 16, maxNumber: 64)
        background = CgCustomBackground(view: self, imageNamed: "backgroundTest.png", width: 8, height: 8, maxNumber: 2)

        // Draw cherries.
        sprite.draw(0, x: 8, y: 8, texture: 3)
        sprite.draw(1, x: 16*13+8, y: 8, texture: 3)
        sprite.draw(2, x: 8, y: 16*17+8, texture: 3)
        sprite.draw(3, x: 16*13+8, y: 16*17+8, texture: 3)

        // Draw and animate a Pacman.
        sprite.setPosition(4, x: 13*8+8, y: 16*12)
        sprite.setRotation(4, radians: CGFloat(90.0 * .pi / 180.0))
        sprite.startAnimation(4, sequence: [0,1,2], timePerFrame: 0.1, repeat: true)
        
        // Draw grids on #0 background.
        background.draw(0, x: 14*8, y: 18*8, columnsInWidth: 28, rowsInHeight: 36)
        background.setDepth(0, zPosition: 0)

        for y in 0 ..< 18  {
            for x in 0 ..< 14  {
                background.put(0, column: x*2, row: y*2, columnsInwidth: 2, rowsInHeight: 2, textures: [4,5,6,7])
            }
        }

        // Print text on #1 background.
        background.draw(1, x: 14*8, y: 18*8, columnsInWidth: 28, rowsInHeight: 36)
        background.setDepth(1, zPosition: 1)

        let asciiOffset = -16*2
        background.putString(1, column: 8, row: 18, string: "SPRITEKIT TEST", offset: asciiOffset)
 
        // Put a #63 texture on #1 background.
        background.put(1, column: 14, row: 19, texture: 128)
    }
    
    private var x: CGFloat = 0
    private var dx: CGFloat = 0

    override func update(_ currentTime: TimeInterval) {
        // Called before each frame is rendered

        // Move and animate a Ghost.
        if x == 0 {
            dx = 1
            sprite.startAnimation(5, sequence: [4,5], timePerFrame: 0.1, repeat: true)
        } else if x == 28*8 {
            dx = -1
            sprite.startAnimation(5, sequence: [6,7], timePerFrame: 0.1, repeat: true)
        }
        
        x += dx
        sprite.setPosition(5, x: x, y: 16*6)
    }
}
```

#参考

* [【Swift】Spritekitを使う時の最小限の雛形](https://qiita.com/fuku2014/items/212e05f15cffce7ee93f)
* [Swiftでゲーム作り！！ SpriteKitフレームワークの紹介](https://qiita.com/shimesaba/items/bcdbb5e778e92e0fa1d7)


#次の記事
[【入門】iOS アプリ開発　#3【Sound を再生する】]
(https://qiita.com/KIKU_CHU/items/01a45a3d4698058bd512)