【入門】iOS アプリ開発　#9【ゲームの状態遷移とシーケンス動作】

# はじめに
今回はゲームとしてプレイできるように、ゲームの開始からゲームオーバーなどの状態遷移や各シーケンスの動作を作成する。以下が完成イメージ。ソースコードは [GitHub](https://github.com/Kikutada/0006_GamePlayTest) に公開しているので参照してほしい。

※YouTube動画
[![IMAGE ALT TEXT HERE](http://img.youtube.com/vi/nKVW342kZFo/0.jpg)](http://www.youtube.com/watch?v=nKVW342kZFo)

# 状態遷移に関する仕様書

![Image100.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678332/774f311f-2d6d-0135-d1eb-6b9460af40e5.png)

スタートモードとして、ゲームを開始する時のシーケンスが詳細に定義されている。

プレイモードはゲームプレイ中の状態で、プレイヤーがミスするとそのままのエサの状態からスタートし、残りのパックマンがいなくなるとゲーム・オーバーとなる。

またプレイフィールドのエサを全て食べるとラウンド・クリアとなる。

# 状態遷移とシーケンス動作の設計

ゲームプレイ中での必要なシーケンスを考慮して、下記の状態遷移図を作成した。

![Image41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678332/70d8d9d6-3ffb-bd78-1856-3e99bd952cec.png)


スタートモードは Start, Ready, Go の３つの状態に分けてシーケンスを作成する。

プレイモードは、主に Updating, ReturnToUpdating の２つの状態からなり、
プレイヤーがミスした場合は、PlayerMiss→PlayerDisappeared→PlayerRestartの状態/シーケンスを実行し、残りのパックマンがいれば Ready状態へ、なくなれば GameOver状態となる。

またプレイフィールドのエサを全て食べると、RoundClear→PrepareFlashMaze→FlashMazeの状態/シーケンスを実行しラウンドクリアとなり、Ready状態へ移って次のラウンドが開始される。

# 状態遷移のソースコード

CgSceneMaze の handleSequenceメソッドに、状態遷移とシーケンス処理を実装していく。

```swift
/// Maze scene class for play mode
/// This class has some methods to draw a maze and starting messages.
class CgSceneMaze: CgSceneFrame, ActorDeligate {

    var player: CgPlayer!
    var blinky: CgGhostBlinky!
    var pinky: CgGhostPinky!
    var inky: CgGhostInky!
    var clyde: CgGhostClyde!
    var ptsManager: CgScorePtsManager!
    var specialTarget: CgSpecialTarget!
    var ghosts = CgGhostManager()
    var counter_judgeGhostsWavyChase: Int = 0

    convenience init(object: CgSceneFrame) {
        self.init(binding: object, context: object.context, sprite: object.sprite, background: object.background, sound: object.sound)
        player = CgPlayer(binding: self, deligateActor: self)
        blinky = CgGhostBlinky(binding: self, deligateActor: self)
        pinky  = CgGhostPinky(binding: self, deligateActor: self)
        inky   = CgGhostInky(binding: self, deligateActor: self)
        clyde  = CgGhostClyde(binding: self, deligateActor: self)
        ptsManager = CgScorePtsManager(binding: self, deligateActor: self)
        specialTarget = CgSpecialTarget(binding: self, deligateActor: self)
        
        ghosts.append(blinky)
        ghosts.append(pinky)
        ghosts.append(inky)
        ghosts.append(clyde)
    }
    
    /// States of game model
    enum EnGameModelState: Int {
        case Init = 0
        case Start, Ready, Go, Updating, ReturnToUpdating, RoundClear, PrepareFlashMaze, FlashMaze,
             PlayerMiss, PlayerDisappeared, PlayerRestart, GameOver
    }

    /// Handle sequence
    /// To override in a derived class.
    /// - Parameter sequence: Sequence number
    /// - Returns: If true, continue the sequence, if not, end the sequence.
    override func handleSequence(sequence: Int) -> Bool {
        guard let state: EnGameModelState = EnGameModelState(rawValue: sequence) else { return false }

        switch state {
            case .Init: sequenceInit()
            case .Start: sequenceStart()
            case .Ready: sequenceReady()
            case .Go: sequenceGo()
            case .Updating: sequenceUpdating()
            case .ReturnToUpdating: sequenceReturnToUpdating()
            case .RoundClear: sequenceRoundClear()
            case .PrepareFlashMaze: sequencePrepareFlashMaze()
            case .FlashMaze: sequenceFlashMaze()
            case .PlayerMiss: sequencePlayerMiss()
            case .PlayerDisappeared: seauencePlayerDisappeared()
            case .PlayerRestart: sequencePlayerRestart()

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

    // ============================================================
    //  Execute sequence in each state.
    // ============================================================
    func sequenceInit() {
        drawBackground()
        goToNextSequence()
    }
    
    func sequenceStart() {
        context.resetGame()
        context.resetRound()
        context.numberOfFeeds = drawMazeWithSettingValuesAndAttributes()
        printBlinking1Up()
        printPlayers(appearance: false)
        printStateMessage(.PlayerOneReady)
        sound.enableOutput(true)
        sound.playSE(.Beginning)
        goToNextSequence(.Ready, after: 2240)
    }
    
    func sequenceReady() {
        printStateMessage(.ClearPlayerOne)
        printPlayers(appearance: true)
        player.reset()
        ghosts.reset()
        specialTarget.reset()
        ptsManager.reset()
        goToNextSequence(.Go, after: 1880)
    }
    
    func sequenceGo() {
        printStateMessage(.ClearReady)
        drawPowerFeed(state: .Blinking)
        player.start()
        ghosts.start()
        
        // Reset counter for wavy attack of ghosts
        counter_judgeGhostsWavyChase = 0
        goToNextSequence()
    }

    // 〜　以下、省略　〜
}

```

Start, Ready, Go それぞれの状態に対応するシーケンスを sequenceStart(), sequenceReady(), sequenceGo()メソッドで実装する。

仕様書とメソッドを対応させると以下の通り。

- ”1UP”表示が点滅　→ printBlinking1Up()
- 巣の上に”PLAYER ONE”表示、巣の下に”READY”表示　→　printStateMessage(.PlayerOneReady)
- 設定パックマンの数だけ”●”がプレイフィールド外の左下に表示される　→　　printPlayers(appearance: false)
- スタートミュージック　→　sound.playSE(.Beginning)
- ”PLAYER ONE”表示が赤モンスターに代わり　→　printStateMessage(.ClearPlayerOne), ghosts.reset()
- 設定パックマン（プレイフィールド外の左下）が１つ減る　→　printPlayers(appearance: true)
- パックマンがスタート位置に表示される　→ player.reset()
- ”READY!”表示が消えて、プレイモードに移る　→ printStateMessage(.ClearReady)


# プレイモードの仕様書

プレイモードにおいては、モンスターの出現タイミングや波状攻撃、アカモンスターのスパートといった細かい仕様が定義されている。これらによってモンスターは単調な追いかけ動作にならず、またプレイヤーのゲーム進行に合わせて難易度が調整される仕組みになっている。

![Image11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678332/b78438d1-ac64-f7b2-3ab3-197a7108c710.png)

![Image12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678332/b937ed5c-574b-7509-5ef4-8018cc3a3959.png)

![Image13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678332/9ba9a986-9674-d5fc-bcb3-c37903fe06c8.png)

ラウンドに対して更にモンスター出現タイミングのレベル、波状攻撃のスピードレベル、スパートする残りエサ数が定義されているが、今回は固定のレベル「A」、スパートは「①イ」のみ実装していく。

# プレイモードのソースコード

プレイモードの仕様は sequenceUpdating() に動作を実装していく。

```swift
    func sequenceUpdating() {
        // Player checks to collide ghost.
        let collisionResult = ghosts.detectCollision(playerPosition: player.position)

        switch collisionResult {
            case .None:
                // When it's no eat time, ghost goes out one by one.
                if player.timer_playerNotToEat.isEventFired()  {
                    player.timer_playerNotToEat.restart()
                    ghosts.setStateToGoOut(numberOfGhosts: 4, forcedOneGhost: true)
                }
                
                // Appearance Timing of Ghosts
                ghosts.setStateToGoOut(numberOfGhosts: context.getNumberOfGhostsForAppearace(), forcedOneGhost: false)

                // Wavy Attack of ghosts
                // - Do not count timer when Pac-Man has power.
                if !player.timer_playerWithPower.isCounting() {
                    counter_judgeGhostsWavyChase += SYSTEM_FRAME_TIME
                }

                // Select either Scatter or Chase mode.
                let chaseMode = context.judgeGhostsWavyChase(time: counter_judgeGhostsWavyChase)

                if chaseMode {
                    pinky.chase(playerPosition: player.position, playerDirection: player.direction.get())
                    inky.chase(playerPosition: player.position, blinkyPosition: blinky.position)
                    clyde.chase(playerPosition: player.position)

                } else {
                    pinky.setStateToScatter()
                    inky.setStateToScatter()
                    clyde.setStateToScatter()
                }

                // If Blinky becomes spurt or not.
                let blinkySpurt: Bool = context.judgeBlinkySpurt() && !ghosts.isGhostInNest()
                blinky.state.setSpurt(blinkySpurt)

                // Blinky doesn't become scatter mode when he spurts.
                if blinkySpurt || chaseMode {
                    blinky.chase(playerPosition: player.position)
                } else {
                    blinky.setStateToScatter()
                }

                // For debug
                ghosts.drawTargetPosition(show: true)

            case .PlayerEatsGhost:
                let pts = context.ghostPts
                ptsManager.start(kind: pts, position: ghosts.collisionPosition, interval: 1000) //ms
                context.ghostPts = pts.get2times()
                addScore(pts: pts.getScore())
                player.stop()
                player.clear()
                specialTarget.enabled = false
                ghosts.stopWithoutEscaping()
                sound.playSE(.EatGhost)
                sound.stopBGM()  // REMARKS: To change playBGM(.BgmEscaping) immediately.
                goToNextSequence(.ReturnToUpdating, after: 1000)

            case .PlayerMiss:
                goToNextSequence(.PlayerMiss)
        }
        
        playBGM()
    }
```

はじめにプレイヤーとモンスターの衝突判定を ghosts.detectCollision(playerPosition: player.position) で行う。

衝突がなければ(.None)、ノーイートタイムによって、ゴーストが巣から出ている処理を行う。４匹の中から必ず１匹は外に出して、ノーイートタイムをリスタートさせる。

次は、食べたエサの数でゴーストを巣から出していく。すでに指定の数が出ていたら何もしない。ミスして新たにスタートするときは、ミスバイパスシーケンスを通す。

CgContextクラスの getNumberOfGhostsForAppearace()メソッドは以下の通り。

```swift
    func getNumberOfGhostsForAppearace() -> Int {
        let numberOfGhosts: Int
        // Miss Bypass Sequence
        if playerMiss {
            // Level A
            if numberOfFeedsEatedByMiss < 7 {
                numberOfGhosts = 1
            } else if numberOfFeedsEatedByMiss < 17 {
                numberOfGhosts = 2
            } else if numberOfFeedsEatedByMiss < 32 {
                numberOfGhosts = 3
            } else {
                playerMiss = false
                numberOfGhosts = getNumberOfGhostsForAppearace()
            }
        } else {
            // Level A
            if numberOfFeedsEated < 30 {
                numberOfGhosts = 2
            } else if numberOfFeedsEated < 90 {
                numberOfGhosts = 3
            } else {
                numberOfGhosts = 4
            }
        }
        return numberOfGhosts
    }
```

sequenceUpdating() メソッドの処理フローに戻る。

次の波状攻撃は、パターンスタートからカウントしている時間 counter_judgeGhostsWavyChase によって Scatter と Chase を切り替える。
ただしプレイヤーが逆転している時は、カウントをストップする。

CgContextクラスの judgeGhostsWavyChase()メソッドは以下の通り。

```swift
    func judgeGhostsWavyChase(time: Int) -> Bool {
        let mode: Bool
        // Level A
        if time < 7000 || (time >= 27000 && time < 34000) ||
           (time >= 54000 && time < 59000) || (time >= 79000 && time < 84000) {
            mode = false
        } else {
            mode = true
        }
        return mode
    }
```

sequenceUpdating() メソッドの最後の処理フローでは、アカモンスター(Blinky)のスパート処理を行う。残りエサ数に達した時かつ全モンスターが巣から出ている時（!ghosts.isGhostInNest()）にスパートする。

残りは衝突判定で、パックマンがモンスターを噛み付いた時（.PlayerEatsGhost)
、逆に捕まった時（.PlayerMiss）の処理をそれぞれ実装する。

# まとめ

ようやくゲームとしてプレイができるようになってきた。現在のソースコードは約5000行。
次はラウンドによって変化する難易度レベルやスピードレベルの詳細を作り込んでいく。

# 次の記事
[【入門】iOS アプリ開発　#10【ゲームの各種設定（難易度やスピードレベル）】]
(https://qiita.com/KIKU_CHU/items/d342b9a878fd66639d1b)