LoginSignup
0
2

More than 3 years have passed since last update.

【入門】iOS アプリ開発 #7【敵キャラクタの移動】

Last updated at Posted at 2020-09-02

はじめに

今回はパックマンに登場する敵キャラクタを作成する。以下が完成動画。
作成した GameGhost.swift ファイルは、コードが 1000行程度になり全ては説明できないため、GitHub に公開しているものを参照してほしい。

※YouTube動画
IMAGE ALT TEXT HERE

テスト動作として、下記の仕様書にある「ノーイートタイム」を過ぎると、ゴースト(モンスター)が巣から出てきて攻撃中となるようにしている。そうでない時は休息中となる。またバッテン表示はデバッグ用としてゴーストの目標位置を示している。

仕様書

仕様書にはゴーストの出現タイミングや、それぞれ異なる4匹の性格仕様が記述されている。

Image2.png

Image31.png

整理していくと、以下のように全てのゴーストには共通の動作や状態があることがわかる。

  • プレイフィールド中(巣から出た状態)のゴーストの移動は、攻撃中と休息中の2つの状態がある
  • パックマンがパワーエサを食べた時、ゴーストはイジケ顔になってランダムに移動する
  • パックマンがゴーストに噛みついた時、ゴーストは目玉だけとなって巣に逃げる
  • ゴーストは特定のタイミングや条件によって巣から出てくる

これらは共通のクラスで実装し、攻撃中と休息中におけるゴーストの特徴の違いを派生クラスで実装するのが簡単そうだ。

キャラクタ・クラスの構成

下図のようなクラス継承構造で設計する。

ImageClass.png

各クラスの役割を補足すると、以下の通り。

  • CbObject : イベントをハンドリングする
  • CbContainer : CbObject(と派生クラス)を複数保持できるコンテナ
  • CgActor : キャラクタ共通の位置や方向、スプライト処理を扱う
  • CgGhost : ゴースト共通の処理を扱う
  • CgGhostBlinky : アカ・ゴースト
  • CgGhostPinky : ピンク・ゴースト
  • CgGhostInky : シアン・ゴースト
  • CgGhostClyde : オレンジ・ゴースト
  • CgPlayer : パックマン

CgGhost クラスの設計

ゴースト共通の動作や状態をステート・チャートにまとめてみた。

Image03.png

  • Standby は巣にいる状態
  • GoOut は巣からプレイフィールドに出る状態
  • Chase は攻撃中状態
  • Scatter は休息中状態
  • Frightened はイジケ状態
  • Escape は目玉になって巣に戻る状態
  • EscapeInNest は目玉の状態で巣に戻って、スタート位置まで戻る状態

Frightened は Standby, GoOut, Chase, Scatter をサブ状態とするスーパー状態となっている。

ソースコードは、フレーム毎に呼ばれる update メソッドをオーバーライドして、ステートマシンを実装していく。状態に入る時の entryアクション、状態に入っている時のアクティビティを一つ一つ作り込んで、ゴーストの動きを作成する。

class CgGhost : CgActor {

    enum EnGhostAction {
        case None, Walking, Spurting, Frightened, Warping, Standby, GoingOut, Escaping
    }

    enum EnMovementRestrictions {
        case None, OnlyVertical
    }

    var target: CgPosition = CgPosition()
    var state: CgGhostState!

    private var movementRestriction: EnMovementRestrictions = .None

    override init(binding object: CgSceneFrame, deligateActor: ActorDeligate) {
        super.init(binding: object, deligateActor: deligateActor)
        state = CgGhostState(binding: self)
        enabled = false
    }

    // ============================================================
    //   Core operation methods for actor
    //  - Sequence: reset()->start()->update() called->stop()
    // ============================================================
    /// Reset ghost states and draw at default position
    override func reset() {
        super.reset()
        direction.reset()
        state.reset()
        movementRestriction = .None
    }

    /// Start
    override func start() {
        super.start()
        draw()
    }

    /// Stop
    override func stop() {
        super.stop()
        draw()
    }

    /// Update handler
    /// - Parameter interval: Interval time(ms) to update
    override func update(interval: Int) {

        //
        // Entry Action to state
        //
        if state.isChanging() {
            switch state.getNext() {
                case .Init : break
                case .Standby: entryActionToStandby()
                case .GoOut: entryActionToGoOut()
                case .Scatter: entryActionToScatter()
                case .Chase: entryActionToChase()
                case .Escape: entryActionToEscape()
                case .EscapeInNest: entryActionToEscapeInNest()
            }
            state.update()
        }

        //
        // Do Action in state
        //
        switch state.get() {
            case .Init : break
            case .Standby: doActionInStandby()
            case .GoOut:  doActionInGoOut()
            case .Scatter:
                if state.isFrightened() {
                    doActionInFrightened()
                } else {
                    doActionInScatter()
                }
            case .Chase:
                if state.isFrightened() {
                    doActionInFrightened()
                } else {
                    doActionInChase()
                }
            case .Escape:  doActionInEscape()
            case .EscapeInNest:  doActionInEscapeInNest()
        }

        //  Update direction and sprite animation for changes.
        if state.isChanging() || direction.isChanging() || state.isDrawingUpdated() {
            if direction.isChanging() {
                position.roundDown()
                direction.update()
            }
            draw()
            state.clearDrawingUpdate()
        }

        // Update position.
        sprite.setPosition(sprite_number, x: position.x, y: position.y)
    }

    // 以下、省略   
}

Frightenedのスーパー状態は、別の CgGhostState クラスを作成して実現している。
state.isFrightened() が true ならイジケ状態で、scatter と chase状態において、doActionInFrightened() メソッド内でゴーストのランダム移動処理を行う。

CgGhostXxxx 敵キャラクタ・クラスの設計

CgGhost クラスを継承して、4匹のゴーストのスタート位置や攻撃動作を実装していく。

/// Ghost Blinky class
class CgGhostBlinky : CgGhost {

    override init(binding object: CgSceneFrame, deligateActor: ActorDeligate) {
        super.init(binding: object, deligateActor: deligateActor)
        actor = .Blinky
        sprite_number = actor.getSpriteNumber()
    }

    // ============================================================
    //   Core operation methods for actor
    //  - Sequence: reset()->start()->update() called->stop()
    // ============================================================
    /// Reset ghost state and draw at default position
    override func reset() {
        super.reset()
        position.set(column: 13, row: 21, dx: 4)
        updateDirection(to: .Left)
        state.set(to: .Scatter)
        draw()
    }

    // ============================================================
    //  General methods in this class
    // ============================================================
    /// Chase player to enter chase state.
    /// Always chase the Pacman during the chase mode.
    /// - Parameter playerPosition: Player's position
    func chase(playerPosition: CgPosition) {
        super.setStateToChase(targetPosition: playerPosition)
    }

    /// Set the target position in scatter state.
    /// Blinky moves around the upper right in the play field.
    override func entryActionToScatter() {
        target.set(column: 25, row: 35)
        super.entryActionToScatter()
    }

    /// Set return destination in nest from escape state.
    override func entryActionToEscapeInNest() {
        target.set(column: 13, row: 18, dx: 4, dy: -4)
    }

}

/// Ghost Pinky class
class CgGhostPinky : CgGhost {

    override init(binding object: CgSceneFrame, deligateActor: ActorDeligate) {
        super.init(binding: object, deligateActor: deligateActor)
        actor = .Pinky
        sprite_number = actor.getSpriteNumber()
    }

    // ============================================================
    //   Core operation methods for actor
    //  - Sequence: reset()->start()->update() called->stop()
    // ============================================================
    /// Reset ghost state and draw at default position
    override func reset() {
        super.reset()
        position.set(column: 13, row: 18, dx: 4)
        updateDirection(to: .Down)
        state.set(to: .Standby)
        draw()
    }

    // ============================================================
    //  General methods in this class
    // ============================================================
    /// Chase player to enter chase state.
    /// Aiming for Pacman's third destination
    /// - Parameters:
    ///   - playerPosition: Player's position
    ///   - playerDirection: Player's direction
    func chase(playerPosition: CgPosition, playerDirection: EnDirection) {
        let dx = playerDirection.getHorizaontalDelta()*3
        let dy = playerDirection.getVerticalDelta()*3
        let newTargetPosition = CgPosition(column: playerPosition.column+dx, row: playerPosition.row+dy)
        super.setStateToChase(targetPosition: newTargetPosition)
    }

    /// Set the direction in standby state.
    override func entryActionToStandby() {
        updateDirection(to: .Down)
    }

    /// Set the target position in scatter state.
    /// Pinky moves around the upper left on the play field.
    override func entryActionToScatter() {
        target.set(column: 2, row: 35)
        super.entryActionToScatter()
    }

    /// Set return destination in nest from escape state.
    override func entryActionToEscapeInNest() {
        target.set(column: 13, row: 18, dx: 4, dy: -4)
    }

}

//  以下、省略

resetメソッドは、初期スタート位置を設定し、Init状態からの初期の状態遷移を行う。
- Blinky(アカ)の場合は、巣の上からスタートし Scatter状態へ遷移する。
- Pinky(ピンク)の場合は、巣の中からスタートし、Standby状態へ遷移する。

chaseメソッドは、目標位置に向けて移動する攻撃中の状態へと遷移する。
- Blinky(アカ)の場合は、パックマンの位置を目標位置とする。
- Pinky(ピンク)の場合は、パックマンの口先の3つ先のマスを目標位置とする。

entryActionToScatterメソッドは、休息中の状態へ遷移する。
- Blinky(アカ)の場合は、プレイフィールド上の右上を目標位置とする。
- Pinky(ピンク)の場合は、プレイフィールド上の左上を目標位置とする。

entryActionToEscapeInNestメソッドは、目玉の状態で巣に戻った時のスタート位置を目標位置として設定する。

といった具合に、CgGhostクラスの基底メソッドをオーバーライドして実装していく。

ゴーストのシーケンス起動

作成したゴーストのクラスをシーケンスに実装して動かしていく。

プレイヤーの時と同様に、CgSceneMaze迷路シーンクラスにゴーストのクラス CgGhostXxxx を関連付け(binding)して生成し、イベントが通知できるようにする。

シーケンス実装の handleSequenceメソッド内でゴーストを ghost.start() 
(enabled=true)にすると、CgGhost内の updateメソッドがフレーム毎に呼ばれてステートマシンが実行される仕組みとなる。

class CgSceneMaze: CgSceneFrame, ActorDeligate {

    var player : CgPlayer!
    var blinky : CgGhostBlinky!
    var pinky  : CgGhostPinky!
    var inky   : CgGhostInky!
    var clyde  : CgGhostClyde!
    var allGhosts : [CgGhost] = []

    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)
        allGhosts.append(blinky)
        allGhosts.append(pinky)
        allGhosts.append(inky)
        allGhosts.append(clyde)
    }

    /// 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 {
        switch sequence {
            case  0:
                drawBackground()
                let _ = setAndDraw()
                printPlayers()

                player.reset()
                for ghost in allGhosts { ghost.reset() }
                goToNextSequence()

            case 1:
                printBlinking1Up()
                drawPowerFeed(state: .Blinking)

                player.start()
                for ghost in allGhosts { ghost.start() }
                goToNextSequence()

            case  2:
                // ===========
                // Foever loop
                // ===========

                // Player checks to collide ghost.
                for ghost in allGhosts {
                    if player.detectCollision(ghostPosition: ghost.position) {
                        if ghost.state.isFrightened()  {
                            ghost.setStateToEscape()
                        }
                    }
                }

                // Set the chase state after after the time that the player doesn't eat feed.
                if player.timer_playerNotToEat.isEventFired()  {
                    blinky.chase(playerPosition: player.position)
                    pinky.chase(playerPosition: player.position, playerDirection: player.direction.get())
                    inky.chase(playerPosition: player.position, blinkyPosition: blinky.position)
                    clyde.chase(playerPosition: player.position)

                    for ghost in allGhosts { ghost.setStateToGoOut() }

                } else {
                    for ghost in allGhosts { ghost.setStateToScatter() }
                }

                // For debug
                for ghost in allGhosts { ghost.drawTargetPosition(show: true) }
                break

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

        // Play BGM
        if sequence >= 2 {
            if player.timer_playerWithPower.isCounting() {
                sound.playBGM(.BgmPower)
            } else {
                sound.playBGM(.BgmNormal)
            }
        }

        // Continue running sequence.
        return true
    }

無限ループの case2 にテスト動作を実装している。

「ノーイートタイム(= player.timer_playerNotToEat.isEventFired())」を過ぎると、ゴースト(モンスター)が巣から出てきて攻撃中( = blinky.chase)となるようにしている。またゴーストが巣にいる時は外に出す( = ghost.setStateToGoOut())。
ノーイートタイムにならない間は、休息中( = ghost.setStateToScatter())としている。

まとめ

今回、パックマンの敵キャラクタの移動を作成した。ソースコードは全体で 4000行程度になっている。

細かい仕様が多く定義されていて、ゲームを面白くするための企画者のこだわりが感じられる。反面プログラマーは良く考えて設計していかないと、コードは大変な状況になりそうだ。Swiftでゲームアプリを開発する入門としては、意外と良い題材なのかもしれない。

次回は、スペシャルターゲットやパックマンがゴーストを噛み付いた時の得点表示、パックマンがゴーストに捕まった処理などを作成していく。

次の記事

【入門】iOS アプリ開発 #8【スペシャルターゲット・得点表示】

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2