iT邦幫忙

2021 iThome 鐵人賽

DAY 28
1
Mobile Development

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

從零開始的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


上一篇
從零開始的8-bit迷宮探險【Level 27】神助攻-老弟幫我配個音效
下一篇
從零開始的8-bit迷宮探險【Level 29】讓你的 App 與眾不同!設計 Icon 及 LaunchScreen
系列文
從零開始的8-bit迷宮探險!Swift SpriteKit 遊戲開發實戰30

尚未有邦友留言

立即登入留言