【入門】iOS アプリ開発　#5【シーケンスの設計】

# はじめに

今回は画面モード内の処理シーケンスを作成していく。SpriteKit Manager を使用して、文字やキャラクタの表示・アニメーションを行う。パックマンの仕様書にあるスタートモードとクレジットモードは除き、完成映像は以下の通り。

* アトラクトモード

<img src="https://github.com/Kikutada/0003_GameArchTest/blob/master/images/Image1.png?raw=true?raw=true" width=512>

> 『仕様書より』
>　　・モンスターキャラクター説明；逆転のデモンストレーション；得点表
>　　　パックマンがやられるまでのプレイデモンストレーションの２パターンが繰り返される。
>　　・電源投入時は、ハイスコアと 2UP の得点、ラウンド表示のくだもの類がない。
>　　・プレイ後は、最終プレイヤーの得点とくだもの類が表示される。

* プレイモード

<img src="https://github.com/Kikutada/0003_GameArchTest/blob/master/images/Image2.png?raw=true" width=512>

> 『仕様書より』
>　　・ゲームオーバーすると”GAME OVER”表示される。

* マンガ（Ｍ１）/ Intermission No.1

<img src="https://github.com/Kikutada/0003_GameArchTest/blob/master/images/Image3.png?raw=true" width=512>

> 『仕様書より』
> 　マンガ（Ｍ１）：赤モンスターに追われたパックマンが大きくなっていじけを追う。

* マンガ（Ｍ２）/ Intermission No.2

<img src="https://github.com/Kikutada/0003_GameArchTest/blob/master/images/Image4.png?raw=true" width=512>

> 『仕様書より』
> 　マンガ（Ｍ２）：赤モンスターの服が、クギに引っかかって破ける。

* マンガ（Ｍ３）/ Intermission No.3

<img src="https://github.com/Kikutada/0003_GameArchTest/blob/master/images/Image5.png?raw=true" width=512>

> 『仕様書より』
> 　マンガ（Ｍ３）：服を脱がされたハダカモンスターが、服を引きずって逃げる。

マンガ仕様は、たったこれだけの記述なので、詳細の動作は YouTube などを見ながら作成する。

# シーケンスの設計

シーケンス動作は「１番目の処理、時間を待って２番目の処理、・・・」と処理が進めていければ良い。簡単に switch - case で実装してみたい。

```swift
func handleSequence(sequence: Int) -> Bool {

    switch sequence {
        case  0:
            // 初期化含めた 0番目の処理
            goToNextSequence()  // 16ms後に #1 へ

        case  1:
            // 1番目の処理
            goToNextSequence(after: 800)  // 800ms後に #2 へ

        case  2:
            // 2番目の処理
            goToNextSequence(after: 480)  // 480ms後に #3へ

        // ・・・　同様に　・・・

        default:
            // Stop and exit running sequence.
            return false
    }
    
    // Continue running sequence.
    return true
}
```

こんな感じ。

ここで『800ms』など時間をカウントするための、カウントダウン・タイマーを作成しておく。
後々、多くの場面で使用しそうだ。

# CbTimer class

```swift
class CbTimer : CbObject {

    private var currentTime = 0
    private var settingTime = 0
    private var eventFired = false

    func reset() {
        currentTime = settingTime
        eventFired = false
        self.enabled = false
    }
    
    func set(interval: Int) {
        settingTime = interval
        reset()
    }

    func start() {
        self.enabled = true
    }

    func restart() {
        reset()
        start()
    }

    func pause() {
        self.enabled = false
    }

    func stop() {
        self.enabled = false
        currentTime = 0
    }

    func get() -> Int {
        return currentTime
    }
    
    func isCounting() -> Bool {
        return self.enabled && currentTime > 0
    }
    
    func isEventFired() -> Bool {
        return eventFired
    }
    
    /// Update handler
    /// - Parameter interval: Interval time(ms) to update
    override func update(interval: Int) {
        guard isCounting() else { return }

        currentTime -= interval
        if currentTime <= 0 {
            currentTime = 0
            eventFired = true
        }
    }
}
```

カウントダウン・タイマーの CbTimerクラスは、CbObjectクラスを継承して作成する。基底クラスの updateメソッドをオーバーライドすれば、フレーム毎(16ms)に自動的にカウントダウンしてくれるように実装できる。

使い方は、CbContainerの派生クラスの中で、

```swift
var timer: CbTimer!

timer = CbTimer(binding: self)  // CbContainerに紐付け
timer.set(time: 800) //ms

timer.start()  // カウントダウン開始

//    .. カウントダウン中 ..

if timer.isFired() { /* 指定時間が経過した時の処理 */ }

timer.restart()  // タイマーの再スタート
```

という具合で動作する。

# CbScene class

前回作った CbContainer クラスを継承して、シーケンスを実行する CbSceneクラスを作成する。

```swift
class CbScene : CbContainer {

    private var timer_sequence: CbTimer!
    private var current_sequence: Int = 0
    private var next_sequence: Int = 0
    
    /// Initialize self
    /// - Parameter object: Parent object
    override init(binding object: CbObject) {
        super.init(binding: object)
        timer_sequence = CbTimer(binding: self)
        resetSequence()
    }
    
    /// Reset sequence settings to start
    func resetSequence() {
        timer_sequence.reset()
        current_sequence = 0
        next_sequence = 0
        self.enabled = false
    }
    
    /// Start sequence of scene
    func startSequence() {
        self.enabled = true
    }

    /// Stop sequence of scene
    func stopSequence() {
        self.enabled = false
    }
    
    /// Implementation of update method in CbContainer
    /// Handle a sequence with a timer every update cylce
    override func update(interval: Int) {
        if timer_sequence.enabled {
            if timer_sequence.isEventFired() {
                timer_sequence.reset()
                current_sequence = next_sequence
            } else {
                return
            }
        }
        self.enabled = handleSequence(sequence: current_sequence)
    }

    /// Handle sequence
    /// To override in a derived class.
    /// - Parameter sequence: Sequence number
    /// - Returns: If true, continue the sequence, if not, end the sequence.
    func handleSequence(sequence: Int) -> Bool {
        // TO DO: override
        // (This is pure virtual method.)
        return false
    }
    
    /// Get current sequence number
    /// - Returns: Sequence number(from 0  to n)
    func getSequence() -> Int {
        return current_sequence
    }

    /// Go to specified sequence number
    /// - Parameter number: Sequence number(from 0  to n)
    func goToSequence(number: Int) {
        timer_sequence.stop()
        current_sequence = number
        next_sequence = number
    }
    
    /// Go to next sequence after the specified time
    /// - Parameters:
    ///   - number: Sequence number
    ///   - time: Waiting time
    func goToNextSequence(_ number: Int = -1, after time: Int = 0) {  // ms
        next_sequence =  (number == -1) ? current_sequence+1 : number
        if time > 0 {
            timer_sequence.set(interval: time)
            timer_sequence.start()
        } else {
            timer_sequence.stop()
            current_sequence = next_sequence
        }
    }
}
```
同様に updateメソッドをオーバーライドすれば、フレーム毎(16ms)に呼ばれて、goToNextSequenceメソッドで、カウントダウン・タイマーが起動されていれば、設定時間だけ待った後に、handleSequenceメソッドが呼ばれる。

# CgSceneFrame class

CbSceneクラスから、「各画面モードで使用するオブジェクト」への参照を持つクラスを作成する。具体的には、これまで作成してきた Sprite Manager, Background Manager, Sound Manager と、ゲームのデータを格納する CbContext のオブジェクトが参照できるようにする。


```swift
 class CgSceneFrame: CbScene {

    var context: CbContext!

    var sprite: CgSpriteManager!
    var background: CgCustomBackgroundManager!
    var sound: CgSoundManager!

    convenience init(binding object: CbObject, context: CbContext, sprite: CgSpriteManager, background: CgCustomBackgroundManager, sound: CgSoundManager) {
        self.init(binding: object)
        self.context = context
        self.sprite = sprite
        self.background = background
        self.sound = sound
    }

    func drawFrame() {
        background.draw(0, x: CGFloat(BG_WIDTH/2*8), y: CGFloat(BG_HEIGHT/2*8), columnsInWidth: BG_WIDTH, rowsInHeight: BG_HEIGHT)
    }

    func printFrame() {
        background.print(0, color: .White, column:  0, row: 35, string: "   1UP   HIGH SCORE         ")
        background.print(0, color: .White, column:  0, row: 34, string: "     00        00           ")
    }

    func printPlayerScore() {
        let str = String(format: "%6d0", context.score/10)
        background.print(0, color: .White, column: 0, row: 34, string: str)
    }
}
```

この CgSceneFrameクラスを継承して、各画面モードのクラスを実装していく。

# CbContext class

ゲームのデータを格納する CbContextクラスは、テストとして下記を記述しておく。

```swift
class CbContext {

    enum EnLanguage: Int {
        case English = 0, Japanese
    }

    var language: EnLanguage = .Japanese

    var highScore = 8150
    var score = 2020
    var credit = 1
    var numberOfPlayers = 3
    var round = 0

}
```

# 各画面モードの実装

CgSceneFrameクラスを継承して、作成した画面モードは以下の通り。アニメーション処理は意外とアナログ的な実装となった。

* class CgSceneAttractMode
* class CgSceneMaze
* class CgSceneIntermission1
* class CgSceneIntermission2
* class CgSceneIntermission3

# 動作テスト

```swift
class GameScene: SKScene {
    
    private var context : CbContext!
    private var root: CbContainer!
    private var gameScenes: [CbScene] = []

    private var sprite: CgSpriteManager!
    private var background: CgCustomBackgroundManager!
    private var sound: CgSoundManager!

    override func didMove(to view: SKView) {

        // Create game context and root container.
        context = CbContext()
        root  = CbContainer()

        // Create SpriteKit managers.
        sprite = CgSpriteManager(view: self, imageNamed: "pacman16_16.png", width: 16, height: 16, maxNumber: 64)
        background = CgCustomBackgroundManager(view: self, imageNamed: "pacman8_8.png", width: 8, height: 8, maxNumber: 2)
        sound = CgSoundManager(binding: root, view: self)

        // Create and append scenes with scequences.
        gameScenes.append(CgSceneAttractMode(binding: root, context: context, sprite: sprite, background: background, sound: sound))
        gameScenes.append(CgSceneMaze(binding: root, context: context, sprite: sprite, background: background, sound: sound))
        gameScenes.append(CgSceneIntermission1(binding: root, context: context, sprite: sprite, background: background, sound: sound))
        gameScenes.append(CgSceneIntermission2(binding: root, context: context, sprite: sprite, background: background, sound: sound))
        gameScenes.append(CgSceneIntermission3(binding: root, context: context, sprite: sprite, background: background, sound: sound))
    }
        
    var mode: Int = 4

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

        // When the scene ends, start the next scene.
        if !gameScenes[mode].enabled {
            mode = mode == 4 ? 0 : mode+1
            gameScenes[mode].resetSequence()
            gameScenes[mode].startSequence()
            context.round += 1
        }

        // Send update message every 16ms.
        root.sendEvent(message: .Update, parameter: [16])
    }
}
```
resetSequence()でシーケンス初期化、 startSequence()でシーケンスを起動し、enabledが false になれば、その画面モードのシーケンスが終了したことを示す。

# まとめ

ゲーム全体を構成する画面モードとその内部シーケンス処理を実装した。

[GitHub](https://github.com/Kikutada/0003_GameArchTest)  に公開しているテスト・プログラムを実行すると、各画面モードが切り替わり表示される。

現在ソースコードはコメント付きで、全体1800行程度となっている。

これからキャラクタの動作を作成していく。

# 次の記事
[【入門】iOS アプリ開発　#6【キャラクタの操作】]
(https://qiita.com/KIKU_CHU/items/b09aa2184881ae57e523)