从零开始的8-bit迷宫探险【Level 21】进击的主角!暴风雨来呐,你坐啊!

远方有个一闪一闪的东西,吸引了山姆的目光。
「这颗水晶...特别的大颗!」山姆跑了过去。
碰触到魔幻水晶的那一刹那,彷佛得到了全宇宙的力量。
这是攻守互换的一刻!山姆的反击开始了!

今日目标

  • 新增魔幻水晶类别
  • 在迷宫中加入魔幻水晶,播放闪烁动画
  • 让主角可以收集魔幻水晶,碰触後魔幻水晶消失,并且让怪物变成逃逸模式
  • 怪物变成逃逸模式後,样子变成白云,并开始往主角的反方向移动
  • 主角反击 (碰触) 到逃逸模式的怪物後,怪物变成水滴,回到出生点後重生

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


<<:  Day 15 - 将 COMPANY 後台储存资料提取後,送至 Certificat 前台渲染画面 - 相簿资料渲染 - ASP.NET Web Forms C#

>>:  D19 - 用 Swift 和公开资讯,打造投资理财的 Apps { 移动平均线(MA线)实作.2 }

ASP.NET MVC 从入门到放弃(Day13) -C# HttpClient 泛型功能介绍

接着来讲讲泛型的部分.... 简单来说泛型就是传入值、传回值不固定的情况下这时候就可以使用泛型......

【Day 15】浅谈 Django model

关於网站 现在网站早已不像以前只是个一页式的静态网站,可能会多很多功能,例如:留言、回覆、公布栏等等...

LeetCode解题 Day16

54. Spiral Matrix https://leetcode.com/problems/sp...

Day04. 年後想换工作,104有RPA热门职缺,怎麽办? - 赶快开始BP Hello World 吧!

还记得做专案时,为了要与非IT专业的高层人士沟通,我们常常需要画业务流程图, 从开始、结束、程序、路...