【入門】iOS アプリ開発　#11【ゲームのアレンジ(加速度センサーを使用した操作など)】

# はじめに

公開されている[仕様書](https://jsai.ixsq.nii.ac.jp/ej/?action=pages_view_main&active_action=repository_view_main_item_detail&item_id=9670&item_no=1&page_id=13&block_id=23)をもとに作成したパックマンに、アレンジを加えてみたい。パックマンををスワイプだけでなくタッチや加速度センサーで操作できるようにし、オリジナルの迷路も追加してみる。ソースコードは [GitHub](https://github.com/Kikutada/0008_GamePacman) に公開しているので参照してほしい。

![Image2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678332/333577f6-9842-45ba-5db2-773cf6ff77b4.png)

# コンフィグレーション・メニュー

操作や迷路を切り替えるためのコンフィグレーション・メニューを追加する。
追加する迷路は開発途中の下記、仕様書を参考にした。

![Image3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678332/3de39a51-7104-37d7-5fa6-9a9a07a53dda.png)

# ソースコード

CgSceneCreditMode にメニューを追加する。クレジットを入れて暫くすると case 1 でメニューの入り口が表示される。そこをタッチするとメニューに入れるようにした。

CgSceneFrameクラスの構造は、以前の[【入門】iOS アプリ開発　#5【シーケンスの設計】](https://qiita.com/KIKU_CHU/items/0448daadb09ff581d483)を参照してほしい。

```swift
/// Credit Mode
class CgSceneCreditMode : CgSceneFrame {

    enum EnEvent: Int {
        case EnterConfig = 3
        case Operation = 5
        case ExtraMode = 6
        case DebugMode = 7
        case Language = 8
        case ResetSetting = 9
        case ExitConfig = 10
        case None
    }

    private let table_enterConfiguration: [(Int,Int,Int,Int,EnEvent)] = [
        (  26, 34, 28, 36, .EnterConfig)
    ]

    private let table_setConfiguration: [(Int,Int,Int,Int,EnEvent)] = [
        (  26, 34, 28, 36, .ExitConfig),
        (   4, 25, 28, 27, .Operation),
        (   4, 20, 28, 22, .ExtraMode),
        (   4, 15, 28, 17, .DebugMode),
        (   4, 10, 28, 12, .Language),
        (   4,  5, 28,  7, .ResetSetting)
    ]

    private var table_search: [(column0:Int,row0:Int,column1:Int,row1:Int,event:EnEvent)] = []
    private var configMode: Bool = false

    /// Event handler
    /// - Parameters:
    ///   - sender: Message sender
    ///   - id: Message ID
    ///   - values: Parameters of message
    override func handleEvent(sender: CbObject, message: EnMessage, parameter values: [Int]) {
        if message == .Touch {
            let position = CgPosition.init(x: CGFloat(values[0]), y: CGFloat(values[1]))
            let event = search(column: position.column, row: position.row)
            if event == .None {
                if !configMode {
                    stopSequence()
                }
            } else {
                goToNextSequence(event.rawValue)
            }
        }
    }

    /// 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:
                configMode = false
                table_search = []
                clear()
                printFrame()
                printPlayerScore()
                printHighScore()
                printCredit()
                printRounds()
                background.print(0, color: .Orange, column: 6, row: 19, string: "PUSH START BUTTON")
                background.print(0, color: .Cyan, column: 8, row: 15, string: "1 PLAYER ONLY")

                if context.language == .English {
                    background.print(0, color: .Pink, column: 1, row: 11, string: "BONUS PAC-MAN FOR 20000 ]^_")
                    background.print(0, color: .Purple, column:4, row: 4, string: "@ 2020 HIWAY.KIKUTADA")
                } else {
                    background.print(0, color: .Pink, column: 1, row: 11, string: "BONUS PAC-MAN FOR 10000 ]^_")
                    background.print(0, color: .Purple, column: 7, row: 7, string: "@ #$%&'()*  2020")   // NAMACO
                }
                goToNextSequence(after: 60*16*2)
            
            case 1:
                table_search = table_enterConfiguration
                background.print(0, color: .White, column:  27, row: 35, string: "$")
                goToNextSequence()

            case 2:
                // wait for event
                break;

            case 3:
                configMode = true
                table_search = table_setConfiguration
                background.fill(0, texture: 0)
                printFrame()
                printPlayerScore()
                printHighScore()
                printCredit()
                background.print(0, color: .White, column: 27, row: 35, string: "#")
                background.print(0, color: .Red,   column:  8, row: 31, string: "CONFIGURATION")
                background.print(0, color: .White, column:  4, row: 26, string: "OPERATION")
                background.print(0, color: .White, column:  4, row: 21, string: "EXTRA MODE")
                background.print(0, color: .White, column:  4, row: 16, string: "DEBUG MODE")
                background.print(0, color: .White, column:  4, row: 11, string: "LANGUAGE")
                background.print(0, color: .White, column:  4, row:  6, string: "SETTING")
                print_operation(color: .Pink)
                print_extraMode(color: .Pink)
                print_debugMode(color: .Pink)
                print_language(color: .Pink)
                print_resetSetting(color: .Pink)
                goToNextSequence()

            case 4:
                // wait for event
                break;

            case 5: // operation
                context.operationMode = context.operationMode.getNext()
                print_operation(color: .Yellow)
                goToNextSequence(4)
                
            case 6: // ExtraMode/
                context.extraMode = context.extraMode.getNext()
                print_extraMode(color: .Yellow)
                goToNextSequence(4)

            case 7: // DebugMode
                context.debugMode = context.debugMode.getNext()
                print_debugMode(color: .Yellow)
                goToNextSequence(4)

            case 8: // Language
                context.language = context.language.getNext()
                print_language(color: .Yellow)
                goToNextSequence(4)

            case 9: // ResetSetting
                context.resetSetting = context.resetSetting.getNext()
                print_resetSetting(color: .Yellow)
                goToNextSequence(4)

            case 10:
                context.saveConfiguration()
                goToNextSequence(0)

            default:
                clear()
                // Stop and exit running sequence.
                return false
        }
        
        return true
    }
    
    func clear() {
        background.fill(0, texture: 0)
    }

    func print_operation(color: CgCustomBackgroundManager.EnBgColor) {
        let str = context.operationMode.getString()
        background.print(0, color: color, column: 16, row: 26, string: str)
    }

    func print_extraMode(color: CgCustomBackgroundManager.EnBgColor) {
        let str = context.extraMode.getString()
        background.print(0, color: color, column: 16, row: 21, string: str)
    }

    func print_debugMode(color: CgCustomBackgroundManager.EnBgColor) {
        let str = context.debugMode.getString()
        background.print(0, color: color, column: 16, row: 16, string: str)
    }

    func print_language(color: CgCustomBackgroundManager.EnBgColor) {
        let str = context.language.getString()
        background.print(0, color: color, column: 16, row: 11, string: str)
    }

    func print_resetSetting(color: CgCustomBackgroundManager.EnBgColor) {
        let str = context.resetSetting.getString()
        background.print(0, color: color, column: 16, row: 6, string: str)
    }

    func search(column: Int, row: Int) -> EnEvent {
        var event: EnEvent = .None
        for i in 0 ..< table_search.count {
            let t = table_search[i]
            if (t.column0 <= column && t.row0 <= row) && (t.column1 >= column && t.row1 >= row) {
                event = t.event
                break
            }
        }
        return event
    }

}
```

handelEventメソッドで Touch のイベントを受信したら、search関数で押された範囲をチェックして該当していれば、そのシーケンスの case を実行する。

設定値は CgContextクラスで定義しておく。

```swift
class CgContext {

    enum EnOperationMode: Int {
        case Swipe = 0, Touch, Accel
        
        func getString() -> String {
            switch self {
                case .Swipe: return "(SWIPE)"
                case .Touch: return "(TOUCH)"
                case .Accel: return "(ACCEL)"
            }
        }
        
        func getNext() -> EnOperationMode {
            switch self {
                case .Swipe: return .Touch
                case .Touch: return .Accel
                case .Accel: return .Swipe
            }
        }
    }

    enum EnLanguage: Int {
        case English = 0, Japanese
        
        func getString() -> String {
            switch self {
                case .English:  return "(ENGLISH) "
                case .Japanese: return "(JAPANESE)"
            }
        }
        
        func getNext() -> EnLanguage {
            switch self {
                case .English: return .Japanese
                case .Japanese: return .English
            }
        }
    }

    enum EnOnOff: Int {
        case Off = 0, On
        
        func getString() -> String {
            switch self {
                case .On:  return "(ON) "
                case .Off: return "(OFF)"
            }
        }
        
        func getNext() -> EnOnOff {
            switch self {
                case .On: return .Off
                case .Off: return .On
            }
        }
    }

    enum EnSetting: Int {
        case Clear = 0, Keep
        
        func getString() -> String {
            switch self {
                case .Clear: return "(CLEAR)"
                case .Keep:  return "(KEEP) "
            }
        }
        
        func getNext() -> EnSetting {
            switch self {
                case .Clear: return .Keep
                case .Keep: return .Clear
            }
        }
    }

    var operationMode: EnOperationMode = .Swipe
    var extraMode: EnOnOff = .Off
    var debugMode: EnOnOff = .Off
    var resetSetting: EnSetting = .Clear
    var language: EnLanguage = .Japanese

    // 以下、省略

```

# 加速度センサーによる操作

```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()

    // MotionManager for accel
    private var motionManager: CMMotionManager!

    override func didMove(to view: SKView) {

        //  Create and start game sequence.
        gameMain  = CgGameMain(skscene: self)
        gameMain.startSequence()

        // Create motion manager
        motionManager = CMMotionManager()
        motionManager.accelerometerUpdateInterval = 0.05

        motionManager.startAccelerometerUpdates(
            to: OperationQueue.current!, withHandler: {
                (accelData: CMAccelerometerData?, errorOC: Error?) in self.sendAccelEvent(acceleration: accelData!.acceleration)
            }
        )

    }

    func sendAccelEvent(acceleration: CMAcceleration){
        let x_diff: Int = Int(acceleration.x * 100)
        let y_diff: Int = Int(acceleration.y * 100)

        if abs(x_diff) > abs(y_diff) {
            gameMain.sendEvent(message: .Accel, parameter: [Int(x_diff > 0 ? EnDirection.Right.rawValue : EnDirection.Left.rawValue)])
        } else {
            gameMain.sendEvent(message: .Accel, parameter: [Int(y_diff > 0 ? EnDirection.Up.rawValue : EnDirection.Down.rawValue)])
        }
    }
```

GameSceneクラスで CMMotionManager を生成し、値を取得する周期を accelerometerUpdateIntervalメンバに設定、
コールバック・メソッドに sendAccelEvent を設定しておく。

sendAccelEvent メソッドは 0.05s周期で呼ばれ、この中で加速度センサーの傾きから方向を算出し、スワイプ操作と同様に gameMain.sendEvent でオブジェクトにイベント送信する。

```swift
class CgPlayer : CgActor {

    /// Event handler
    /// - Parameters:
    ///   - sender: Message sender
    ///   - id: Message ID
    ///   - values: Parameters of message
    override func handleEvent(sender: CbObject, message: EnMessage, parameter values: [Int]) {
        guard !deligateActor.isDemoMode() else { return }
        switch message {
            case .Accel where deligateActor.isOperationMode(mode: CgContext.EnOperationMode.Accel): fallthrough
            case .Swipe where deligateActor.isOperationMode(mode: CgContext.EnOperationMode.Swipe):
                if let direction = EnDirection(rawValue: values[0]) {
                    targetDirecition = direction
                }

            case .Touch where deligateActor.isOperationMode(mode: CgContext.EnOperationMode.Touch):
                setTargetPosition(x: values[0], y: values[1])
                targetDirecition = decideDirectionByTarget(forcedDirectionChange: true)
                position.amountMoved = 0
                
            default:
                break
        }
    }
```

CgPlayerクラスの handleEvent の中で、コンフィグレーション・メニューで設定した操作が有効ならば（deligateActor.isOperationMode(mode: CgContext.EnOperationMode.Accel)）、加速度センサーのイベントを受け付けるようにする。

こちらは以前の、[【入門】iOS アプリ開発　#6【キャラクタの操作】](https://qiita.com/KIKU_CHU/items/b09aa2184881ae57e523)を参照してほしい。

# オリジナル迷路の追加

CgSceneMazeクラスの getMazeSourceメソッドを、メニューの値で迷路データを切り替えるようにする。この迷路データの作りは古典的だが意外と簡単。

```swift
    func getMazeSource() -> [String] {
        
        let mazeSource: [String] = [
            "aggggggggggggjiggggggggggggb",
            "e111111111111EF111111111111f",
            "e1AGGB1AGGGB1EF1AGGGB1AGGB1f",
            "e3E  F1E   F1EF1E   F1E  F3f",
            "e1CHHD1CHHHD1CD1CHHHD1CHHD1f",
            "e11111111111111111111111111f",
            "e1AGGB1AB1AGGGGGGB1AB1AGGB1f",
            "e1CHHD1EF1CHHJIHHD1EF1CHHD1f",
            "e111111EF1111EF1111EF111111f",
            "chhhhB1EKGGB1EF1AGGLF1Ahhhhd",
            "     e1EIHHD2CD2CHHJF1f     ",
            "     e1EF          EF1f     ",
            "     e1EF QhUWWVhR EF1f     ",
            "gggggD1CD f      e CD1Cggggg",
            "____  1   f      e   1  ____" ,
            "hhhhhB1AB f      e AB1Ahhhhh",
            "     e1EF SggggggT EF1f     ",
            "     e1EF          EF1f     ",
            "     e1EF AGGGGGGB EF1f     ",
            "aggggD1CD1CHHJIHHD1CD1Cggggb",
            "e111111111111EF111111111111f",
            "e1AGGB1AGGGB1EF1AGGGB1AGGB1f",
            "e1CHJF1CHHHD2CD2CHHHD1EIHD1f",
            "e311EF1111111  1111111EF113f",
            "kGB1EF1AB1AGGGGGGB1AB1EF1AGl",
            "YHD1CD1EF1CHHJIHHD1EF1CD1CHZ",
            "e111111EF1111EF1111EF111111f",
            "e1AGGGGLKGGB1EF1AGGLKGGGGB1f",
            "e1CHHHHHHHHD1CD1CHHHHHHHHD1f",
            "e11111111111111111111111111f",
            "chhhhhhhhhhhhhhhhhhhhhhhhhhd"
        ]

        let mazeSourceExtra1: [String] = [
                "aggggjiggggggjiggggggjiggggb",
                "e1111EF111111EF111111EF1111f",
                "e1AB1EF1AGGB1CD1AGGB1EF1AB1f",
                "e3EF1EF1E  F1111E  F1EF1EF3f",
                "e1CD1CD1CHHD1AB1CHHD1CD1CD1f",
                "e111111111111EF111111111111f",
                "kGGB1AGGB1AGGLKGGB1AGGB1AGGl",
                "YHJF1EIHD1CHHJIHHD1CHJF1EIHZ",
                "e1EF1EF111111EF111111EF1EF1f",
                "e1EF1EKGGGGB1EF1AGGGGLF1EF1f",
                "e1CD1CHHHHHD2CD2CHHHHHD1CD1f",
                "e11111111          11111111f",
                "e1AB1AGGB QhUWWVhR AGGB1AB1f",
                "e1EF1CHHD f      e CHHD1EF1f",
                "e1EF11111 f      e 11111EF1f",
                "kGLF1AGGB f      e AGGB1EKGl",
                "YHHD1EIHD SggggggT CHJF1CHHZ",
                "e1111EF11          11EF1111f",
                "e1AGGLF1AGGGGGGGGGGB1EKGGB1f",
                "e1CHHJF1CHHHHJIHHHHD1EIHHD1f",
                "e1111EF111111EF111111EF1111f",
                "kGGB1EKGGGGB1EF1AGGGGLF1AGGl",
                "YHHD1CHHHHHD2CD2CHHHHHD1CHHZ",
                "e111111111111  111111111111f",
                "e1AGGGB1AGGGGGGGGGGB1AGGGB1f",
                "e1CHHJF1CHHHHJIHHHHD1EIHHD1f",
                "e3111EF111111EF111111EF1113f",
                "kGGB1EF1AB1AGLKGB1AB1EF1AGGl",
                "YHHD1CD1EF1CHHHHD1EF1CD1CHHZ",
                "e1111111EF11111111EF1111111f",
                "chhhhhhhnmhhhhhhhhnmhhhhhhhd"
        ]

        return context.extraMode == CgContext.EnOnOff.Off ? mazeSource : mazeSourceExtra1
    }
```

# まとめ

今回パックマン・ゲームのアレンジとして以下を追加した。
　- コンフィグレーション・メニュー
　- 加速度センサー、タッチ操作
　- オリジナル迷路

# 最後に

パックマンのゲームを題材にして、SwiftによるiOSプログラミングを勉強してきた。
ソースコードはコメント含む 5000行程度で、アーケードゲームに相当するクオリティとアレンジを簡単に実現することができた。

コロナ禍のどこに行けない夏休みから始まったが、また一つプログラミングの面白さを味わう良い機会ともなった。

全11回のiOSアプリ開発入門は、これにて終了。

＜読んでいただいた方、ありがとうございました〜＞