从零开始的8-bit迷宫探险【Level 28】看我把关卡难度提升-在场景加上聚光灯效果

天色突然暗了下来,一股诡谲感弥漫,令人不禁冒出冷汗。
还好,随身携带头灯可是探险家的必备要领。
山姆把头灯戴上,整座黑森林里只看得见山姆一个人。
「必须下山了!终点应该快到了吧!」

今日目标

  • 侦测破关 (主角收集完全部的水晶及魔幻水晶)
  • 显示等级,破关後等级上升
  • 破关後将角色设定回原本的位置,准备进入下一关
  • 增加下一关的关卡难度 (将场景加上聚光灯效果,只有主角附近的范围可以看得见)

PS. 这里是开发 iOS 手机游戏的系列文,如果还没看过之前 剧情 文章的朋友,欢迎先点这边回顾唷!


侦测破关

我们一样将侦测的逻辑写在 update 里,将 crystalsmagicCrystals 使用 filter 过滤出还没有被收集的 水晶/魔幻水晶,并使用 isComplete 参数来判断是否已经进入破关状态,接着执行破关方法: gameComplete

  • gameComplete 中写入要做的动作:

    • stopTimer():将所有计时器关闭
    • setCanMove(isCanMove: false):让主角及怪物设定为不能移动
    • playMusicByName:播放游戏破关音乐 FinishMusic,约 6 秒
    • 设定一个 Timer,6 秒後执行下一关的动作 gameNextLevel
  • gameNextLevel

    • resetPosition:主角及怪物设定回原来的位置
    • setMode(mode: .ATTACK):将怪物的模式设定回攻击
    • setGotten(isGotten:false):收集物皆设定回尚未收集的状态
    • level 加 1
    • isComplete 设定回 false
    • 呼叫 gameStart 方法,让游戏开始
  • GameScene.swift
class GameScene: SKScene {
    ...
    var isComplete: Bool = false
    var level: Int = 1
    override func update(_ currentTime: TimeInterval) {
        ...
        if self.crystals.filter({!$0.isGotten}).count == 0 && self.magicCrystals.filter({!$0.isGotten}).count == 0 && !self.isComplete {
            self.isComplete = true
            self.gameComplete()
        }
    }
    func gameComplete() {
        self.stopTimer()
        if let sam = self.sam {
            sam.setCanMove(isCanMove: false)
        }
        for weather in weathers {
            weather.setCanMove(isCanMove: false)
        }
        self.playMusicByName(musicName: "FinishMusic")
        Timer.scheduledTimer(timeInterval: 6, target: self, selector: #selector(gameNextLevel), userInfo: nil, repeats: false)
    }
    @objc func gameNextLevel() {
        for weather in self.weathers {
            weather.resetPosition()
            weather.setMode(mode: .ATTACK)
        }
        if let sam = self.sam {
            sam.resetPosition()
        }
        for crystal in self.crystals {
            crystal.setGotten(isGotten:false)
        }
        for magicCrystal in self.magicCrystals {
            magicCrystal.setGotten(isGotten:false)
        }
        for mushroom in self.mushrooms {
            mushroom.setGotten(isGotten:false)
        }
        self.level += 1
        self.isComplete = false
        
        self.gameStart()
    }
}

等级节点

  • 在画面上新增一个文字节点 levelLabel,用来显示当前的等级,显示文字设定为 Level: \(self.level)。将文字的颜色 (fontColor)、大小 (fontSize)、字体 (fontName)、垂直对齐 (verticalAlignmentMode)、水平对齐 (horizontalAlignmentMode) 分别设定好,将等级文字节点加到场景中
  • applySafeArea 方法中,校正等级文字节点的位置,方法跟之前使用的一样
  • 调整 gameNextLevel 方法,进入下一关时,更新等级的文字显示
  • GameScene.swift
class GameScene: SKScene {
    ...
    var levelLabel: SKLabelNode?
    
    override func didMove(to view: SKView) {
        ...
        self.levelLabel = SKLabelNode(text: "Level: \(self.level)")
        if let levelLabel = self.levelLabel {
            levelLabel.fontColor = UIColor.white
            levelLabel.fontSize = CGFloat(22)
            levelLabel.fontName = "Copperplate"
            levelLabel.verticalAlignmentMode = .center
            levelLabel.horizontalAlignmentMode = .right
            self.addChild(levelLabel)
        }
    }
    func applySafeArea() {
        ...
        if let mapNode = self.mapNode, let scoreNode = self.scoreNode, let lifeNode = self.lifeNode, let levelLabel = self.levelLabel {
            mapNode.position = CGPoint(x: 0, y: -self.topSafeArea - scoreNode.size.height)
            scoreNode.position =  CGPoint(x: 0 ,y: -self.topSafeArea - scoreNode.size.height/2)
            lifeNode.position = CGPoint(x: 0, y: -self.topSafeArea - scoreNode.size.height - mapNode.size.height - 15)
            levelLabel.position = CGPoint(x: self.size.width - 10, y: -self.topSafeArea - scoreNode.size.height - mapNode.size.height - 15)
        }
    }
    @objc func gameNextLevel() {
        ...
        self.levelLabel!.text = "Level: \(self.level)"
    }
}

破关提示节点

  • 新增破关提示的文字节点 clearLabel,放在画面的中央,文字设定为 Clear!
  • 将文字的颜色 (fontColor)、大小 (fontSize)、字体 (fontName)、位置 (position)、垂直对齐 (verticalAlignmentMode)、水平对齐 (horizontalAlignmentMode)、层级 (zPosition) 分别设定好
  • 初始设定 alpha0,先暂时不显示
  • 将节点加进地图 mapNode
  • 当破关时 (gameComplete),播放让破关提示文字闪烁的动画,次数设定为 5
  • GameScene.swift
class GameScene: SKScene {
    ...
    var clearLabel: SKLabelNode?
    
    override func didMove(to view: SKView) {
        ...
        self.clearLabel = SKLabelNode(text: "Clear!")
        if let clearLabel = self.clearLabel {
            clearLabel.fontColor = UIColor.white
            clearLabel.fontSize = CGFloat(22)
            clearLabel.fontName = "Copperplate"
            clearLabel.position = CGPoint(x: self.gridWH * 8 + gridWH/2, y: -gridWH * 12 - gridWH/2);
            clearLabel.verticalAlignmentMode = .center
            clearLabel.horizontalAlignmentMode = .center
            clearLabel.zPosition = 5
            clearLabel.alpha = 0
            self.mapNode!.addChild(clearLabel)
        }
    }
    func gameComplete() {
        ...
        let ani1 = SKAction.fadeAlpha(to: 1, duration: 0.6)
        let ani2 = SKAction.fadeAlpha(to: 0, duration: 0.3)
        let aniAlpha = SKAction.sequence([ani1, ani2])
        let aniRepeat = SKAction.repeat(aniAlpha, count: 5)
        self.clearLabel!.run(aniRepeat)
    }
}

执行结果

破关後,游戏中间会出现 Clear! 的文字,接着游戏重新设定,并且可以看到右下角的等级上升。
但目前破关後没有任何的改变,我们接着来为它增加下一关的难度。
https://imgur.com/kLoEsaq.gif


加上聚光灯效果 (SKLightNode)

我们先制作第 2 关以後的游戏场景,都具有聚光灯的效果
可以想像成游戏画面会变成黑色,只有放上光节点的地方会被照亮,我们让这个光源跟着主角一起移动,制造出像是在主角头上打了一盏聚光灯的感觉

新增 SKLightNode

使用 SKLightNode 类别可以新增一个灯光实体,用来照亮附近的节点。
有三种颜色属性可以设定:

  • ambientColor:灯光周围的颜色,预设颜色为黑色 (black)
  • lightColor:光源散射和反射的颜色,预设颜色为白色 (white)
  • shadowColor:阴影的颜色,由节点 (sprite) 投射而成,预设颜色为黑色 (black),预设透明度为 0.5

接着我们来做设定:

  • 新增加入灯光的方法 addLight()
  • 新增灯光的实体 light
  • 调整 ambientColorlightColor 的颜色
  • categoryBitMask 设定为 1,设定灯光的类型为 1
  • falloff:光源的衰减率指数,设定为 1
  • 将光的节点加进地图节点 (mapNode) 中
  • GameScene.swift
class GameScene: SKScene {
    ...
    var light: SKLightNode?

    func addLight() {
        if self.light == nil {
            self.light = SKLightNode()
            self.light!.ambientColor = UIColor(red: 50/255, green: 50/255, blue: 50/255, alpha: 0.3)
            self.light!.lightColor = UIColor(red: 250/255, green: 250/255, blue: 250/255, alpha: 0.8)
            self.light!.categoryBitMask = 1
            self.light!.falloff = 1
            self.mapNode!.addChild(self.light!)
        }
    }
}

lightingBitMask

我们希望迷宫中的所有东西,包含主角、怪物、地图、水晶等收集物都能有光照的效果,因此可以透过设定节点的 lightingBitMask 的值,让它跟灯光的 categoryBitMask 属性值一样,就可以产生被光源照射的效果

  • 将所有角色及迷宫中的节点的 lightingBitMask 值设定为 1
  • GameScene.swift
class GameScene: SKScene {
    ...
    override func didMove(to view: SKView) {
        ...
        self.sam!.node.lightingBitMask = 1
        rain.node.lightingBitMask = 1
        storm.node.lightingBitMask = 1
        lightning.node.lightingBitMask = 1
        snow.node.lightingBitMask = 1
    }
    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 _ where mapKeys.contains(mapRowArr[j]):
                    let spriteItem = SKSpriteNode(imageNamed: wallMapping[mapRowArr[j]]!)
                    ...
                    spriteItem.lightingBitMask = 1
                case "+":
                    let mushroom = Collection(gridWH: self.gridWH, gridX: j, gridY: i, imageName: "mushroom")
                    ...
                    mushroom.node.lightingBitMask = 1
                case ".":
                    let crystal = Collection(gridWH: self.gridWH, gridX: j, gridY: i, imageName: "crystal")
                    ...
                    crystal.node.lightingBitMask = 1
                case "*":
                    let magicalCrystal = MagicalCrystal(gridWH: self.gridWH, gridX: j, gridY: i, imageName: "magical-crystal")
                    ...
                    magicalCrystal.node.lightingBitMask = 1
                default:
                    break
                }
            }
        }
    }
}

破关时加上灯光

写好新增灯光节点 (SKLightNode) 的方法後,我们在游戏进入下一关的时候呼叫它: addLight()

  • GameScene.swift
class GameScene: SKScene {
    ...
    @objc func gameNextLevel() {
        self.addLight()
        ...
    }
}

移动灯光

为了让灯光能跟着主角移动,在 update 中,判断游戏如果已经有新增灯光节点的话,就改动它的位置 position,让它跟主角的位置一样

  • GameScene.swift
class GameScene: SKScene {
    ...
    override func update(_ currentTime: TimeInterval) {
        ...
        if let light = self.light {
            light.position = sam.node.position
        }
    }
}

游戏结束时,移除灯光

在游戏结束时,将灯光节点移除 removeChildren(in: [light])

  • GameScene.swift
class GameScene: SKScene {
    ...
    @objc func gameOver() {
        ...
        if let light = self.light {
            self.mapNode!.removeChildren(in: [light])
        }
        ...
    }
}

执行结果

  • 可以看到游戏破关後,开始有场景灯光效果,也会跟着主角移动
    https://imgur.com/j27ACRy.gif

  • 聚光灯效果
    只有主角附近有灯光,其他会随着范围越远而看不到,更有黑森林的感觉了!
    https://imgur.com/KZ2MGnv.png
    https://imgur.com/xudlFaR.png


今日小结

大家可以试着优化,将玩家的最高关卡等级也纪录到本机中,并且显示在游戏结束的画面上。
这边因为篇幅有限的关系,只介绍第 2 关以後的关卡加上聚光灯效果来提升难度,大家可以再继续让後面关卡的地图,样子长得跟之前的关卡都不一样,增加多样性。


参考来源:
repeat(_:count:)
SKLightNode
ambientColor
lightColor
shadowColor
categoryBitMask
falloff
lightingBitMask


<<:  22 准备完成後跳转到游戏页面

>>:  [DAY-22] 填补知识缺口 寻找导师 持续学习

22.MYSQL NOT BETWEEN AND指令

除了有BETWEEN AND 之外,还有NOT BETWEEN AND (NOT BETWEEN A...

Day 28 - Spring Security (五) JwtAuthenticationProvider

实作 新增依赖 <!-- JWT --> <dependency> <...

Day 27 利用transformer自己实作一个翻译程序(九) Point wise feed forward network

Point wise feed forward network 在两层全连阶层中加入一个relu的激...

JS 19 - 我要一份能控制网页内容的三人全家桶!

大家好! 今天要介绍的是能控制网页内容的原型方法。 我们进入今天的主题吧! 插入元素 Felix.p...

[day 17] Swift 语法梳理後续

Swift 语法介绍 枚举(Enumerations) ,类和结构体 枚举(Enumerations...