【入門】iOS アプリ開発　#6【キャラクタの操作】

# はじめに

今回はパックマンのキャラクタ操作を作成する。操作はスワイプで行い、以下が完成動画。ソースコードは [GitHub](https://github.com/Kikutada/0004_PlayerTest) に公開しているので参照してほしい。

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

# 仕様書

注目すべきところはスピードレベルの仕様。単に１ドットずつ動かすなら簡単だが、微妙なスピード調整の仕様があり、ゲームの難易度を調整している。

![Spec1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678332/b1727dd6-0d59-1a18-388b-7d315d6da3c0.png)

エサを食べる、食べない、パワーエサを食べた状態などで、移動スピードが異なる。仕様段階でここまで定義していたとは奥深い。

![Spec3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678332/1802c2e7-6c01-ac57-a132-a397a2122ced.png)

「スピードの数字は、１フレームに１ドット移動するスピードを”１６”として、２ドット移動を”３２”として、他はそれに準じて分割。」と書いてあるので、１ドット進むスピードを 16 として、作成していく。

また仕様書には記載がないが、パックマンが迷路を曲がる時はモンスターより早く内側に曲り、曲りながら移動することでモンスターから引き離せるチューニングがされているようだ。本当に奥深い。

# スワイプ操作の作成

スワイプ操作をパックマン・オブジェクトに伝えるのは、

```swift
sendEvent(message: .Swipe, parameter: [direction])
```

というようにしたい。

イベントは、迷路シーンの中で生成したパックマン・オブジェクトへ通知する。

大もととなる GameScene クラスの touchesBegan, touchesEnded メソッドにスワイプ動作のコードを実装する。gameMain オブジェクトの sendEvent を呼ぶ。

```swift
class GameScene: SKScene {
    
    /// Main object with main game sequence
    private var gameMain: CgGameMain!
    
    /// Points for Swipe operation
    private var startPoint: CGPoint = CGPoint.init()
    private var endPoint: CGPoint = CGPoint.init()

    override func didMove(to view: SKView) {

        //  Create and start game sequence.
        gameMain  = CgGameMain(skscene: self)
        gameMain.startSequence()
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        // Get start touchpoint for swipe.
        if let t = touches.first {
            let location = t.location(in: self)
            startPoint = CGPoint(x: location.x, y: location.y)
        }
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        // Get end touchpoint for swipe.
        if let t = touches.first {
            let location = t.location(in: self)
            endPoint = CGPoint(x: location.x, y: location.y)
            
            let x_diff = endPoint.x - startPoint.x
            let y_diff = endPoint.y - startPoint.y

            // Send swipe message to GameMain.
            if abs(x_diff) > abs(y_diff) {
                gameMain.sendEvent(message: .Swipe, parameter: [Int(x_diff > 0 ? EnDirection.Right.rawValue : EnDirection.Left.rawValue)])
            } else {
                gameMain.sendEvent(message: .Swipe, parameter: [Int(y_diff > 0 ? EnDirection.Up.rawValue : EnDirection.Down.rawValue)])
            }

        }
    }
```

# CgGameMain class

gameMain: CgGameMain （オブジェクト：クラス）は、前回のアーキテクチャでいう、root のオブジェクトとして作成した。

gameMain の sendEvent メソッドは、gameMain が持つアクティブ（enabled=true）なオブジェクトにメッセージを送信する。

以下のコードにより、scene_maze（迷路を描画するシーン） が startSequence() により enabled=true となり該当する。

```swift
/// Main sequence of game scene.
class CgGameMain : CgSceneFrame {

    private var scene_attractMode: CgSceneAttractMode!
    private var scene_maze: CgSceneMaze!
    private var scene_intermission1: CgSceneIntermission1!
    private var scene_intermission2: CgSceneIntermission2!
    private var scene_intermission3: CgSceneIntermission3!
    
    init(skscene: SKScene) {
        super.init()

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

        scene_attractMode = CgSceneAttractMode(object: self)
        scene_maze = CgSceneMaze(object: self)
        scene_intermission1 = CgSceneIntermission1(object: self)
        scene_intermission2 = CgSceneIntermission2(object: self)
        scene_intermission3 = CgSceneIntermission3(object: self)
    }

    /// 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:
                // Start maze sequence.
                scene_maze.startSequence()
                goToNextSequence()

            default:
                // Forever loop
                break
        }
        
        // Continue running sequence.
        return true
    }

}
```

迷路シーンクラスでは、初期化時に新規に作成したパックマンのクラス CgPlayer を CgSceneMaze に関連付け(binding)て生成し、イベントが通知できるようにする。

そのため、CgPlayerクラスの基底クラスは CbObject となっている。

シーケンス実装の handleSequenceメソッド内でパックマンを player.start()　
（enabled=true）にすると、CgPlayer内の updateメソッドがフレーム毎に呼ばれて移動処理を更新する仕組みとなる。

```swift
class CgSceneMaze: CgSceneFrame, ActorDeligate {

    var player : CgPlayer!

    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)
    }

    /// 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()
                printBlinking1Up()

                drawPowerFeed(state: .Blinking)

                player.reset()
                player.start()
                
                goToNextSequence()
            
            case  1:
                // Foever loop
                break

            //
            //  Round clear animation(Maze flashes)
            //
            case  10:
                blinkingTimer = 104  // 104*16ms = 1664ms
                goToNextSequence()

            case  11:
                if blinkingTimer == 0 {
                    goToNextSequence()
                } else {
                    let remain = blinkingTimer % 26
                    if remain == 0 {
                        drawMazeWall(color: .White)
                    } else if remain == 13 { // 13*16ms = 208ms
                        drawMazeWall(color: .Blue)
                    }
                    blinkingTimer -= 1
                }

            case 12:
                // Stop and exit running sequence.
                return false

            default:
                // Stop and exit running sequence.
                return false
        }
        
        // Play BGM
        if player.timer_playerWithPower.isCounting() {
            sound.playBGM(.BgmPower)
        } else {
            sound.playBGM(.BgmNormal)
        }
        
        // Continue running sequence.
        return true
    }
```

# CgPlayer class

パックマンの CgPlayerクラスは、CbContainer & CbObjectクラスを継承した CgActorクラスを継承している。handleEventメソッドをオーバーライドすれば、Swipe イベントを取得できる。これがやりたいためにクラス構成を設計してきた。

```swift
/// Player(Pacman) class derived from CgAcotr
class CgPlayer : CgActor {

    enum EnPlayerAction: Int {
        case None, Stopping, Walking, Turning, EatingDot, EatingPower, EatingFruit
    }

    var targetDirecition: EnDirection = .Stop
    var actionState: EnPlayerAction = .None

    var timer_playerWithPower: CbTimer!
    var timer_playerNotToEat: CbTimer!
 
    override init(binding object: CgSceneFrame, deligateActor: ActorDeligate) {
        super.init(binding: object, deligateActor: deligateActor)
        timer_playerWithPower = CbTimer(binding: self)
        timer_playerNotToEat = CbTimer(binding: self)
        actor = .Pacman
        sprite_number = actor.getSpriteNumber()
        enabled = false
    }

    // ============================================================
    //  Event Handler
    // ============================================================

    /// Event handler
    /// - Parameters:
    ///   - sender: Message sender
    ///   - id: Message ID
    ///   - values: Parameters of message
    override func handleEvent(sender: CbObject, message: EnMessage, parameter values: [Int]) {
        switch message {
            case .Swipe:
                if let direction = EnDirection(rawValue: values[0]) {
                    targetDirecition = direction
                }
            default:
                break
        }
    }

    /// Update handler
    /// - Parameter interval: Interval time(ms) to update
    override func update(interval: Int) {
        if actionState == .Turning {
            turn()
        } else {
            if canMove(to: targetDirecition) {
                direction.set(to: targetDirecition)
            } else {
                direction.update()
                if canTurn() {
                    actionState = .Turning
                    direction.set(to: targetDirecition)
                    return
                }
            }
            move()
        }
    }
```

handleEventメソッドでパックマンの移動方向を設定し、updateメソッドで設定された移動方向の処理を行う。

# まとめ

今回、パックマンのキャラクタ操作を実装するにあたり、以下のファイル(クラス)を新規で作成した。

GameMain.swift（CgGameMain）
GamePlayer.swift(CgPlayer)
GameActor.swift(CgActor,CgPosition,CgDirection)
GameContext.swift(CbContext)

ソースコードは全体で 3000行程度。
操作のパフォーマンスは問題ない状況。

次はモンスターを作成していく。

# 次の記事
[【入門】iOS アプリ開発　#7【敵キャラクタの移動】]
(https://qiita.com/KIKU_CHU/items/b5f8a4e147cab22b14b8)