iT邦幫忙

2021 iThome 鐵人賽

DAY 21
1
Mobile Development

從零開始的8-bit迷宮探險!Swift SpriteKit 遊戲開發實戰系列 第 21

從零開始的8-bit迷宮探險【Level 21】進擊的主角!暴風雨來吶,你坐啊!

  • 分享至 

  • xImage
  •  

遠方有個一閃一閃的東西,吸引了山姆的目光。
「這顆水晶...特別的大顆!」山姆跑了過去。
碰觸到魔幻水晶的那一剎那,彷彿得到了全宇宙的力量。
這是攻守互換的一刻!山姆的反擊開始了!

今日目標

  • 新增魔幻水晶類別
  • 在迷宮中加入魔幻水晶,播放閃爍動畫
  • 讓主角可以收集魔幻水晶,碰觸後魔幻水晶消失,並且讓怪物變成逃逸模式
  • 怪物變成逃逸模式後,樣子變成白雲,並開始往主角的反方向移動
  • 主角反擊 (碰觸) 到逃逸模式的怪物後,怪物變成水滴,回到出生點後重生

PS. 這裡是開發 iOS 手機遊戲的系列文,如果還沒看過之前 劇情 文章的朋友,歡迎先點這邊回顧唷!


新增魔幻水晶類別

我們新增魔幻水晶類別,讓它繼承收集物類別
請新增一個 swift 檔案

  • 點選新增檔案
    https://imgur.com/DyRgPfA.png

  • 選擇 Swift File -> Next
    https://imgur.com/IJukGWI.png

  • 將檔案命名為 MagicalCrystal,點擊 Create 按鈕,完成 .swift 檔的新增

引入 SpriteKit

請先 import SpriteKit

import SpriteKit

類別內容

這邊做個讓魔幻水晶可以閃爍的效果,我們新增 playAnimation 方法

  • 使用 SKAction.fadeAlpha 來做閃爍效果,to 帶入 alpha 值 (不透明度),duration 帶入持續時間
  • 使用 SKAction.sequence 帶入 SKAction 的陣列
  • 使用 SKAction.repeatForever 重複播放這個序列動作
  • 使用 run 方法播放動畫
  • MagicalCrystal.swift
class MagicalCrystal: Collection {
    func playAnimation() {
        let ani1 = SKAction.fadeAlpha(to: 0, duration: 0.6)
        let ani2 = SKAction.fadeAlpha(to: 1, duration: 0.3)
        let aniAlpha = SKAction.sequence([ani1, ani2])
        let aniRepeat = SKAction.repeatForever(aniAlpha)
        self.node.run(aniRepeat)
    }
}

在迷宮中加上魔幻水晶

地圖陣列加上魔幻水晶

在先前的章節已經設定過地圖上要畫的圖片代號,想複習的朋友可以點這邊

  • 我們使用 * 代號代表這個位置要畫上魔幻水晶
  • GameScene.swift
let mapDraw = [
    "ccccccccpccccccci",
    "   .....e*......b",
    "aam.1ji.s.11.zy.b",
    "  d.3gh....1.wx.b",
    "jcl.....ji.1....b",
    "d*...11.gh...rt.b",
    "d.11.nm....2....b",
    "d..1.kl.22.1.naah",
    "ot...       .b   ",
    "d..1.jcu vci.b   ",
    "d.12.d     b.kccc",
    "d+...gaaaaah.    ",
    "gaam.       .11.n",
    "   d.rt.1#.q....b",
    "   d....21.e.ji.b",
    "cccl.ji....e.gh.b",
    "    .gh.13.s....b",
    "aaam....1....21.b",
    "   d...23.rt.1..b",
    "   d.1.........3b",
    "jccl.1.rft.3.1.1b",
    "d*.............*b",
    "gaaaaaaaaaaaaaaah",
]

準備魔幻水晶圖片

請準備好圖片,並且拖拉進專案中

https://imgur.com/PefZ2Js.png

加上魔幻水晶

  • 先宣告魔幻水晶陣列的變數 magicCrystals,準備將所有畫面上的魔幻水晶存在這邊
  • 在先前的 drawMap 方法裡,再加上 "*" 的 case
  • 新增魔幻水晶類別 MagicalCrystal 的實體,將需要的參數帶入
  • mapNode 裡加入魔幻水晶的 node
  • 將魔幻水晶的實體存在 magicCrystals 陣列中
  • 播放閃爍效果的動畫
  • GameScene.swift
class GameScene: SKScene {
    ...
    var magicCrystals: [Collection] = []
    ...
    func drawMap() {
        for i in 0..<gridYCount {
            let mapRowArr = Array(mapDraw[i]);
            for j in 0..<gridXCount {
                let mapKeys = wallMapping.keys
                switch mapRowArr[j] {
                ...
                case "*":
                    let magicalCrystal = MagicalCrystal(gridWH: self.gridWH, gridX: j, gridY: i, imageName: "magical-crystal")
                    self.mapNode!.addChild(magicalCrystal.node)
                    self.magicCrystals.append(magicalCrystal)
                    magicalCrystal.playAnimation()
                default:
                    break
                }
            }
        }
    } 
}

執行結果

https://imgur.com/ho3sLaC.gif


主角與魔幻水晶碰觸後,魔幻水晶消失

  • update 方法裡,再加上主角跟魔幻水晶之間的位置判斷,當主角跟魔幻水晶的格子位置一樣時,並且還沒有被收集時,就將魔幻水晶設定成被收集了 setGotten(isGotten: true)
  • GameScene.swift
class GameScene: SKScene {
    override func update(_ currentTime: TimeInterval) {
        ...
        for magicCrystal in self.magicCrystals where !magicCrystal.isGotten && magicCrystal.gridX == sam.gridX && magicCrystal.gridY == sam.gridY {
            magicCrystal.setGotten(isGotten: true)
        }
    }
}

執行結果

https://imgur.com/vuCMjjj.gif


讓怪物變成逃逸模式

逃逸模式與攻擊模式切換

  • 我們繼續在偵測魔幻水晶與主角碰觸的地方調整怪物模式
    • 讓所有的怪物變成逃逸模式: setMode(mode: .ESCAPE)
    • 新增 magicTimer,先判斷如果 magicTimer 不為 nil 時,關閉上一個 magicTimer,這邊的目的是防止 Timer 還沒結束時又再次啟動 Timer
    • 設定 20 秒後,讓所有怪物回到攻擊模式 eacapeToAttackModeAction
  • GameScene.swift
class GameScene: SKScene {
    ...
    var magicTimer: Timer? = nil
    ...
    override func update(_ currentTime: TimeInterval) {
        ...
        for magicCrystal in self.magicCrystals where !magicCrystal.isGotten && magicCrystal.gridX == sam.gridX && magicCrystal.gridY == sam.gridY {
            magicCrystal.setGotten(isGotten: true)
            for weather in self.weathers {
                weather.setMode(mode: .ESCAPE)
            }
            if let magicTimer = self.magicTimer {
                magicTimer.invalidate()
            }
            // 20秒後回復
            self.magicTimer = Timer.scheduledTimer(timeInterval: 20, target: self, selector: #selector(eacapeToAttackModeAction), userInfo: nil, repeats: false)
        }
    }
    @objc func eacapeToAttackModeAction() {
        self.magicTimer = nil
        for weather in self.weathers {
            if weather.mode == .ESCAPE {
                weather.setMode(mode: .ATTACK)
            }
        }
    }
}

變成白雲

變成逃逸模式後的怪物,會呈現另一種樣貌,作為區別

  • 加上白雲序列圖
    • 左:cloud_left_1、cloud_left_2
    • 右:cloud_right_1、cloud_right_2
    • 上:cloud_up_1、cloud_up_2
    • 下:cloud_down_1、cloud_down_2

https://imgur.com/UOGiaYV.png
https://imgur.com/h9tN3JC.png

  • 先前已經有在 getImage 中加入 .ESCAPE 的 case 了,檔名為 cloud
  • Weather.swift
enum Mode {
    case ATTACK
    case ESCAPE
    case PLAY
    case REBIRTH
    
    func getImage(role: Role) -> String {
        switch self {
        ...
        case .ESCAPE:
            return "cloud"
        case .REBIRTH:
            return "water"
        }
    }
}

往主角的反方向移動

  • updateMode 方法裡,加上 .ESCAPE case
    • 這邊的目標點還是一樣是主角
    • 我們將怪物的 isTrace 值改為 false,讓怪物偵測的路徑是往取得離主角較遠的方向前進
    • 設定 moveInterval,將速度調稍快一點
  • Weather.swift
class Weather: GameCharacter, Move {
    func updateMode() {
        switch mode {
        case .ATTACK:
            ...
            self.setPathMode(isTrace: true)
        case .PLAY:
            ...
            self.setPathMode(isTrace: true)
        case .ESCAPE:
            self.setTarget(targetX: self.sam!.gridX, targetY: self.sam!.gridY)
            self.setPathMode(isTrace: false)
            self.moveInterval = 0.25
        ...
        }
    }
    func setPathMode(isTrace: Bool) {
        self.isTrace = isTrace
    }

}

執行結果

可以看到當吃到魔幻水晶後,怪物都變成白雲了,並且往主角的反方向移動。
這時候當怪物與主角碰觸時,怪物就不能攻擊主角了。
https://imgur.com/VvrdolI.gif


主角反擊怪物後,怪物變成水滴,回到出生點後重生

  • 在先前的 主角與怪物接觸 判斷的地方,再加上當前模式為 .ESCAPE 的判斷,讓怪物變成重生的模式 setMode(mode: .REBIRTH)
  • GameScene.swift
class GameScene: SKScene {
    ...
    override func update(_ currentTime: TimeInterval) {
        ...
        for weather in self.weathers where weather.gridX == sam.gridX && abs(weather.node.position.y - sam.node.position.y) <= CGFloat(self.gridWH + 6) || weather.gridY == sam.gridY && abs(weather.node.position.x - sam.node.position.x) <= CGFloat(self.gridWH + 6) {
            if weather.mode == .ATTACK || weather.mode == .PLAY {
                if !self.isSamFall {
                    self.isSamFall = true
                    self.gameStop()
                }
            } else if weather.mode == .ESCAPE {
                // 讓怪物重生
                weather.setMode(mode: .REBIRTH)
            }
        }
    }
}

變成水滴

變成重生模式後的怪物,會呈現水滴的樣貌

  • 加上水滴序列圖
    • 左:water_left_1、water_left_2
    • 右:water_right_1、water_right_2
    • 上:water_up_1、water_up_2
    • 下:water_down_1、water_down_2

https://imgur.com/yqy9Oup.png
https://imgur.com/mrYQsz8.png

往出生點移動

  • 設定目標點為出生點 weatherHome
  • isTracetrue
  • 調整 moveInterval,將移動速度調快
  • Weather.swift
class Weather: GameCharacter, Move {
    ...
    func updateMode() {
        switch mode {
        ...
        case .REBIRTH:
            self.setTarget(targetX: gridMapping.weatherHome.x, targetY: gridMapping.weatherHome.y)
            self.setPathMode(isTrace: true)
            self.moveInterval = 0.2
        }
    }
}
  • 新增怪物的出生點位置設定
  • GameScene.swift
struct gridMapping {
    ...
    struct weatherHome {
        static let x = 8
        static let y = 10
    }
}

移動優化

為了讓怪物能更快的回到出生點,我們分別在出生點的左右入口處,以及其上方入口處新增位置設定

  • GameScene.swift
struct gridMapping {
    ...
    struct weatherHomeEntryLeft {
        static let x = 4
        static let y = 8
    }
    struct weatherHomeEntryRight {
        static let x = 12
        static let y = 8
    }
    struct weatherHomeEntry {
        static let x = 8
        static let y = 8
    }
}
  • 調整 startMove 方法
    • 判定如果當下的模式為 .REBIRTH,以及當下的格子位置在 weatherHomeEntryLeftweatherHomeEntryRightweatherHomeEntry 則強制讓怪物分別往 .RIGHT.LEFT.DOWN 的方向移動,讓怪物順利回到出生點
    • 其餘的模式,如果在 weatherHomeEntry 的位置時,則讓怪物只能左右移動,無法回到出生點
  • Weather.swift
class Weather: GameCharacter, Move {
    ...
    func startMove(direction : Direction) {
        ...
        if !isBack || !validDirections.contains(self.direction) {
            self.direction = newDirection
        }
        if self.mode == .REBIRTH {
            if gridX == gridMapping.weatherHomeEntryLeft.x && gridY == gridMapping.weatherHomeEntryLeft.y {
                self.direction = .RIGHT
            }
            if gridX == gridMapping.weatherHomeEntryRight.x && gridY == gridMapping.weatherHomeEntryRight.y {
                self.direction = .LEFT
            }
            if gridX == gridMapping.weatherHomeEntry.x && gridY == gridMapping.weatherHomeEntry.y {
                self.direction = .DOWN
            }
        } else { // 其餘時間不能回家
            if gridX == gridMapping.weatherHomeEntry.x && gridY == gridMapping.weatherHomeEntry.y {
                self.direction = [Direction.LEFT, Direction.RIGHT].randomElement()!
            }
        }
        ...
    }
}

回到出生點後,再次回到攻擊狀態

  • 當怪物回到出生點時,將怪物的模式設定為 .ATTACK,再次回到攻擊模式
  • GameScene.swift
class GameScene: SKScene {
    ...
    override func update(_ currentTime: TimeInterval) {
        ...
        for weather in self.weathers where weather.mode == .REBIRTH && weather.gridX == gridMapping.weatherHome.x && weather.gridY == gridMapping.weatherHome.y {
            weather.setMode(mode: .ATTACK)
        }
    }
}

執行結果

主角可以反擊怪物了!怪物會變成水滴往出生點的位置移動,一但回到出生點,又會再次變回攻擊模式
https://imgur.com/YJuyNYT.gif


今日小結

目前怪物所有的樣貌和模式已經有了一個基本循環
明日會在遊戲中再加上一些特殊道具的設定,增添遊戲性


參考來源:
fadeAlpha(to:duration:)
sequence(_:)
run(:)


上一篇
從零開始的8-bit迷宮探險【Level 20】搜集水晶可以召喚神龍嗎?
下一篇
從零開始的8-bit迷宮探險【Level 22】奧義隱身術 & 時間靜止術
系列文
從零開始的8-bit迷宮探險!Swift SpriteKit 遊戲開發實戰30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
daniel_ho
iT邦新手 5 級 ‧ 2021-09-29 13:19:09

好有趣

0
阿展展展
iT邦好手 1 級 ‧ 2021-10-01 11:25:15

魔幻水晶太美了吧

雪花冰 iT邦新手 5 級 ‧ 2021-10-01 13:45:14 檢舉

謝謝it宏的讚美~

我要留言

立即登入留言