[Introduction] iOS application development # 10 [Various game settings (difficulty and speed level)]

Introduction

This time, we created details of the difficulty level and speed level that change depending on the round, and combined each screen mode created in # 5 [Sequence design]. Then, bring it to the almost completed state (the following is an image movie). The source code is available on GitHub, so please refer to it.

Specification

The specifications of the difficulty level and speed level that change depending on the round are as follows.

Spec1.png

Spec2.png

Image3.png

This time, the foreign version of the difficulty table and the spurt ② setting will not be created.

Implementation of difficulty table and speed table

Implement a difficulty table and a speed table as an array of tuples. Below, except for the combination of each screen mode, all are included in the CgContext class.

    enum EnLevel: Int {
        case Level_A = 0, Level_B, Level_C, Level_D
    }

    let table_difficultySettings: [(round: Int, levelOfSpeed: EnLevel, timeWithPower: Int, numberOfFeedsRemaingToSpurt: Int, levelOfAppearance: EnLevel, kindOfSpecialTarget: CgSpecialTarget.EnSpecialTarget, timeNotToEat: Int, intermission: Int)] = [
            //round, speedLevel, PowerTime[ms], Spurtfeeds, GhostAppear, SpecialTarget, NoEatTime[ms], Intermission
            (     1,   .Level_A,          6000,         20,    .Level_A,      .Cherry,           4000,            0 ),
            (     2,   .Level_B,          5000,         30,    .Level_B,  .Strawberry,           4000,            1 ),
            (     3,   .Level_B,          4000,         40,    .Level_C,      .Orange,           3000,            0 ),
            (     4,   .Level_B,          3000,         40,    .Level_C,      .Orange,           3000,            0 ),
            (     5,   .Level_C,          2000,         40,    .Level_C,       .Apple,           3000,            2 ),
            (     6,   .Level_C,          5000,         50,    .Level_C,       .Apple,           3000,            0 ),
            (     7,   .Level_C,          2000,         50,    .Level_C,       .Melon,           3000,            0 ),
            (     8,   .Level_C,          2000,         50,    .Level_C,       .Melon,           3000,            0 ),
            (     9,   .Level_C,          1000,         60,    .Level_C,    .Galaxian,           3000,            3 ),
            (    10,   .Level_C,          5000,         60,    .Level_C,    .Galaxian,           3000,            0 ),
            (    11,   .Level_C,          2000,         60,    .Level_C,        .Bell,           3000,            0 ),
            (    12,   .Level_C,          1000,         80,    .Level_C,        .Bell,           3000,            0 ),
            (    13,   .Level_C,          1000,         80,    .Level_C,         .Key,           3000,            3 ),
            (    14,   .Level_C,          3000,         80,    .Level_C,         .Key,           3000,            0 ),
            (    15,   .Level_C,          1000,        100,    .Level_C,         .Key,           3000,            0 ),
            (    16,   .Level_C,          1000,        100,    .Level_C,         .Key,           3000,            0 ),
            (    17,   .Level_C,             0,        100,    .Level_C,         .Key,           3000,            3 ),
            (    18,   .Level_C,          1000,        100,    .Level_C,         .Key,           3000,            0 ),
            (    19,   .Level_C,             0,        100,    .Level_C,         .Key,           3000,            0 ),
            (    20,   .Level_C,             0,        100,    .Level_C,         .Key,           3000,            0 ),
            (    21,   .Level_C,             0,        100,    .Level_C,         .Key,           3000,            0 ),
            (    22,   .Level_D,             0,        100,    .Level_C,         .Key,           3000,            0 )
        ]

    let table_speedSettings: [ (eatNone: Int, eatFeed: Int, eatPow: Int, eatNoneInPow: Int, eatFeedInPow: Int, eatPowInPow: Int,
         ghost: Int, ghostInSpurt: Int, ghostInPow: Int, ghostInWarp: Int) ]
        = [
            // Level A
            ( eatNone: 16, eatFeed: 15, eatPow: 13, eatNoneInPow: 18, eatFeedInPow: 17, eatPowInPow: 15,
              ghost: 15, ghostInSpurt: 16, ghostInPow: 10, ghostInWarp: 8 ),
            // Level B
            ( eatNone: 18, eatFeed: 17, eatPow: 15, eatNoneInPow: 19, eatFeedInPow: 18, eatPowInPow: 16,
              ghost: 17, ghostInSpurt: 18, ghostInPow: 11, ghostInWarp: 9 ),
            // Level C
            ( eatNone: 20, eatFeed: 19, eatPow: 17, eatNoneInPow: 20, eatFeedInPow: 19, eatPowInPow: 17,
              ghost: 19, ghostInSpurt: 20, ghostInPow: 12, ghostInWarp: 10 ),
            // Level D
            ( eatNone: 18, eatFeed: 17, eatPow: 15, eatNoneInPow: 18, eatFeedInPow: 17, eatPowInPow: 15,
              ghost: 19, ghostInSpurt: 20, ghostInPow: 10, ghostInWarp: 9 )
        ]

Extract the data according to the round from this tuple array and set it in each member of the CgContext class.


    /// Set difficulty of the round
    func setDifficulty() {
        let index = demo ? 0 : round-1
        let count = table_difficultySettings.count
        let table = (index < count) ? table_difficultySettings[index] : table_difficultySettings[count-1]
        
        levelOfSpeed = table.levelOfSpeed
        timeWithPower = table.timeWithPower
        numberOfFeedsRemaingToSpurt = table.numberOfFeedsRemaingToSpurt
        levelOfAppearance = table.levelOfAppearance
        kindOfSpecialTarget = table.kindOfSpecialTarget
        timeNotToEat = table.timeNotToEat
        intermission = table.intermission
    }

The method to get the speed of the player (Pac-Man) is as follows. Change the value to be acquired when you eat power food and reverse it and when it is not.

    func getPlayerSpeed(action: CgPlayer.EnPlayerAction, with power: Bool ) -> Int {
        let index = levelOfSpeed.rawValue
        let count = table_speedSettings.count
        let table = (index < count) ? table_speedSettings[index] : table_speedSettings[count-1]

        switch action {
            case .Walking where !power : return table.eatNone
            case .Walking where  power : return table.eatNoneInPow
            case .EatingFeed where !power : return table.eatFeed
            case .EatingFeed where  power : return table.eatFeedInPow
            case .EatingPower where !power : return table.eatPow
            case .EatingPower where  power : return table.eatPowInPow
            case .EatingFruit where !power : return table.eatNone
            case .EatingFruit where  power : return table.eatNoneInPow
            default: return 16
        }
    }

Implementation of monster (ghost) appearance timing

The number of foods Pac-Man has eaten since the start of play numberOfFeedsEated returns the number of ghosts that appear for each level.

    func getNumberOfGhostsForAppearace() -> Int {
        let numberOfGhosts: Int
        // Miss Bypass Sequence
        if playerMiss {
            if numberOfFeedsEatedByMiss < 7 {
                numberOfGhosts = 1
            } else if numberOfFeedsEatedByMiss < 17 {
                numberOfGhosts = 2
            } else if numberOfFeedsEatedByMiss < 32 {
                numberOfGhosts = 3
            } else {
                playerMiss = false
                numberOfGhosts = getNumberOfGhostsForAppearace()
            }
        } else {
            switch levelOfAppearance {
                case .Level_A:
                    if numberOfFeedsEated < 30 {
                        numberOfGhosts = 2
                    } else if numberOfFeedsEated < 90 {
                        numberOfGhosts = 3
                    } else {
                        numberOfGhosts = 4
                    }
                case .Level_B:
                    if numberOfFeedsEated < 50 {
                        numberOfGhosts = 3
                    } else {
                        numberOfGhosts = 4
                    }
                case .Level_C: fallthrough
                default:
                    numberOfGhosts = 4
            }
        }
        return numberOfGhosts
    }

Implementation of wavy attack

Switch between ChaseMode and ScatterMode depending on the time counted from the start. Determine the ChaseMode time for each level.

    func judgeGhostsWavyChase(time: Int) -> Bool {
        var chaseMode: Bool = false
        switch levelOfSpeed {
            case .Level_A:
                chaseMode = (time >= 7000 && time < 27000) || (time >= 34000 && time < 54000)
                         || (time >= 59000 && time < 79000) || (time >= 84000)
            case .Level_B:
                chaseMode = (time >= 7000 && time < 27000) || (time >= 34000 && time < 54000)
                         || (time >= 59000)
            case .Level_C: fallthrough
            case .Level_D:
                chaseMode = (time >= 5000 && time < 25000) || (time >= 30000 && time < 50000)
                         || (time >= 55000)
        }
        return chaseMode
    }

Combining each screen mode

Finally, combine each mode created so far into the CgGameMain class.

--Attract Mode: CgSceneAttractMode --Credit mode: CgSceneCreditMode * Added to GameSequences.swift created this time --Start mode: CgSceneMaze --Play mode: CgSceneMaze is running


class CgGameMain : CgSceneFrame {

    enum EnMainMode: Int {
        case AttractMode = 0, CreditMode, WaitForStartButton, StartMode, PlayMode
    }

    enum EnSubMode: Int {
        case Character = 0, StartDemo, PlayDemo
    }

    private var scene_attractMode: CgSceneAttractMode!
    private var scene_creditMode: CgSceneCreditMode!
    private var scene_maze: CgSceneMaze!
    private var subMode: EnSubMode = .Character

    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_creditMode = CgSceneCreditMode(object: self)
        scene_maze = CgSceneMaze(object: self)
    }

    /// 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 {
            if let mode: EnMainMode = EnMainMode(rawValue: getSequence()) {
                if mode == .AttractMode || mode == .WaitForStartButton {
                    goToNextSequence()
                }
            }
        }
    }
    
    /// 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 mode: EnMainMode = EnMainMode(rawValue: sequence) else { return false }

        switch mode {
            case .AttractMode: attarctMode()
            case .CreditMode: creditMode()
            case .WaitForStartButton: break // Forever loop
            case .StartMode: startMode()
            case .PlayMode: playMode()
        }
        
        // Continue running sequence.
        return true
    }

    // ============================================================
    //  Execute each mode.
    // ============================================================

    func attarctMode() {
        switch subMode {
            case .Character:
                scene_attractMode.resetSequence()
                scene_attractMode.startSequence()
                subMode = .StartDemo

            case .StartDemo:
                if !scene_attractMode.enabled {
                    context.demo = true
                    sound.enableOutput(false)
                    scene_maze.resetSequence()
                    scene_maze.startSequence()
                    subMode = .PlayDemo
                }

            case .PlayDemo:
                if !scene_maze.enabled {
                    subMode = .Character
                }
        }
    }
    
    func creditMode() {
        context.demo = false
        if scene_attractMode.enabled {
            scene_attractMode.stopSequence()
            scene_attractMode.clear()
        }
        if scene_maze.enabled {
            scene_maze.stopSequence()
            scene_maze.clear()
        }

        context.credit += 1
        scene_creditMode.resetSequence()
        scene_creditMode.startSequence()
        sound.enableOutput(true)
        sound.playSE(.Credit)
        goToNextSequence()
    }
    
    func startMode() {
        context.credit -= 1
        scene_creditMode.stopSequence()
        scene_maze.resetSequence()
        scene_maze.startSequence()
        goToNextSequence()
    }

    func playMode() {
        if !scene_maze.enabled {
            subMode = .Character
            goToNextSequence(EnMainMode.AttractMode.rawValue)
        }
    }

}

In addition to character introduction, there is a play demo in attract mode. This is a demo flag, and it was easy to implement by switching between swipe operation and operation table prepared in advance.

Operation table and get method are implemented in CgContext class. The direction is taken out according to the number of frames from the start.

    let table_operationInDemo: [ (frameCount: Int, direction: EnDirection) ] = [
        (9, .Left), (36, .Down), (61, .Right), (82, .Down), (109, .Right), (133, .Up), (162, .Right),
        (189, .Up), (215, .Right), (238, .Down), (261, .Right), (308, .Down), (335, .Left), (523, .Up),
        (555, .Right), (569, .Up), (609, .Left), (632, .Up), (648, .Right), (684, .Up), (732, .Left),
        (831, .Down), (864, .Left), (931, .Up), (948, .Left), (970, .Up), (1063, .Right), (1113, .Down),
        (1157, .Right), (1218, .Down)
    ]

    func getOperationForDemo() -> EnDirection {
        guard(demoSequence < table_operationInDemo.count) else { return .None }
        let table = table_operationInDemo[demoSequence]
        var direction: EnDirection = .None
        if counterByFrame >= table.frameCount {
            direction = table.direction
            demoSequence += 1
        }
        return direction
    }

Get the operation direction with the getOperationForDemo method and set it in the player. Added to the sequenceUpdating sequence of the CgSceneMaze class.

    func sequenceUpdating() {
        // Operate player in demonstration automatically.
        if context.demo {
            let direction = context.getOperationForDemo()
            if direction != .None {
                player.targetDirecition = direction
            }
        }

        //Omitted below

Summary

Finally, it was almost completed.

There is no problem with the operating speed of the game. The source code is still around 5000 lines, which is fairly easy to do.

Next time, I'm making my own work, so I'd like to make various arrangements to complete it.

Next article

[Introduction] iOS application development # 11 [Game arrangement (operation using accelerometer, etc.)]

Recommended Posts

[Introduction] iOS application development # 10 [Various game settings (difficulty and speed level)]
[Introduction] iOS application development # 10 [Various game settings (difficulty and speed level)]
Introduction to Android application development
iOS App Development Skill Roadmap (Introduction)
Game development with two people using java 2
Game development with two people using java 1
Game development with two people using java 3
Introduction to Android application development