iT邦幫忙

2021 iThome 鐵人賽

DAY 17
0
Mobile Development

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

從零開始的8-bit迷宮探險【Level 17】稻草人也想要智慧大腦,給怪物一點靈魂跟一點點個性

「我們不能漫無目的地追,要擬定包夾計畫!」Rain 大聲地說,並展露出大哥是對的姿態。
「我去魔幻水晶的地方埋伏。」Storm 自告奮勇地說。
「那我到處偵查。」Lightning 天生就是偵察兵的料。
「好,那我來找尋那個傢伙的足跡。話說,你們有看到 Snow 跑去哪了嗎?」

今日目標

  • 讓怪物追蹤主角的位置,來決定移動路線
  • 讓四種怪物分別有不同的追擊方式

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


設定目標點:主角

  • 我們需要設定一個目標的格子點,讓怪物追蹤,目前的目標物就是主角
  • 在前面的遊戲企劃中,我們預計怪物會有不同的移動模式,不只有攻擊,可能還有逃跑,所以目標物不會永遠都是追著主角走,因此先寫一個更新模式的方法 updateMode,先在裡頭寫上攻擊的模式對應的目標物格子,其他的模式未來再慢慢補上就好
  • 呼叫 setTarget 方法,改變怪物的屬性 targetGridXtargetGridY,這邊我們將主角 sam 的格子點位置存起來
  • Weather.swift
class Weather: GameCharacter, Move {
    ...
    func updateMode() {
        switch mode {
        case .ATTACK:
            self.setTarget(targetX: self.sam!.gridX, targetY: self.sam!.gridY)
        default:
            break
        }
    }
    func setTarget(targetX: Int, targetY: Int) {
        self.targetGridX = targetX
        self.targetGridY = targetY
    }
}

取得與目標點距離最近的方向

請先定義以下變數及常數

  • directions:所有方向
  • pathDirection:最後要回傳的方向
  • pathDistance:距離
  • newX:新的方向位置的 position x
  • newY:新的方向位置的 position y

使用數學公式計算兩點距離
讓我們一起來回憶高中數學,計算兩點間距離的公式
https://imgur.com/K8OYJo7.png

  • 在 swift 裡可以用 sqrtf 來計算開根號的值
  • 分別換算新方向位置目標點位置的值 (position)
    • newXnewY 的位置依照座標系統加減好
    • 將目標點的位置使用 targetGridXtargetGridY 乘以格子寬度計算出來
  • 使用 sqrtf 將兩點間的距離 distance 計算出來
  • 這邊的 isTrace 值,代表的是追逐/遠離目標物,這邊預設目前是 true,我們也先考量未來怪物可能會有逃離行為的移動,依照 isTrace 分別取得較短(追逐)及較長(遠離)路徑的方向
  • 最後將取得的方向 return
  • Weather.swift
class Weather: GameCharacter, Move {
    ...
    func getPathDirection()-> Direction {
        let directions: [Direction] = [.LEFT, .RIGHT, .UP, .DOWN]
        var pathDirection: Direction = .NONE
        var pathDistance: Float = 0.00
        var newX = self.node.position.x
        var newY = self.node.position.y
        
        for dir in directions {
            switch dir {
            case .LEFT:
                newX = self.node.position.x - CGFloat(self.gridWH)
                break
            case .RIGHT:
                newX = self.node.position.x + CGFloat(self.gridWH)
                break
            case .UP:
                newY = self.node.position.y + CGFloat(self.gridWH)
                break
            case .DOWN:
                newY = self.node.position.y - CGFloat(self.gridWH)
                break
            case .NONE:
                break
            }
            let lengthA = CGFloat(self.targetGridX * self.gridWH + (self.gridWH/2)) - newX
            let lengthB = -CGFloat(self.targetGridY * self.gridWH + (self.gridWH/2)) - newY
            let distance = sqrtf(Float(lengthA * lengthA + lengthB * lengthB))
            // 取較短的距離
            if (self.isTrace && (pathDistance == 0.00 || distance < pathDistance)) {
                pathDistance = distance
                pathDirection = dir
            }
            // 取較長的距離
            if (!self.isTrace && (pathDistance == 0.00 || distance > pathDistance)) {
                pathDistance = distance
                pathDirection = dir
            }
        }
        return pathDirection
    }
}

修改移動方法

回到昨天新加的 startMove,繼續優化怪物的移動

  • 先呼叫剛剛的更新模式方法 updateMode,在每一次的移動時先更新主角的位置
  • 接著是昨天已經加上的:取得隨機一條可行的方向
  • 呼叫 getPathDirection 方法取得往目標物的最短方向 bestDirection
  • 接著將原本的 newDirection = randomDirection 改為先取看看最短方向,如果取出來的最短方向不包含在可行的方向中時,才改取隨機方向
  • Weather.swift
class Weather: GameCharacter, Move {
    ...
    func startMove(direction : Direction) {
        let tempDirection: Direction = self.direction
        var newDirection: Direction = .NONE
        
        // 更新模式
        self.updateMode()
        
        let validDirections: [Direction] = self.getValidDirection()
        guard let randomDirection = validDirections.randomElement() else {
            return
        }
        
        // 取得往目標物的最短方向
        let bestDirection = self.getPathDirection()
        
        // 先取得可行的最短方向,否則取隨機方向
        // newDirection = randomDirection
        newDirection = validDirections.contains(bestDirection) ? bestDirection : randomDirection
        ...
    }
}

看一下目前結果

我們先暫時將怪物都放在起始點內
並將主角放置在起始點下方
https://imgur.com/pUvVLb8.gif

隔著牆的位置有點 bug
可以發現怪物往上跑之後,又馬上往下移動,因為這時候偵測最短路徑應該是向下,但是中間隔著牆壁,造成反覆移動無法走出框框外。直到我們將主角往上移動,怪物才能正常往主角方向前進

優化移動方法

我們將 getPathDirection 調整一下,在初始取得方向時,先移除反方向的路線,目的是讓怪物不要一直來回走

  • Weather.swift
class Weather: GameCharacter, Move {
    ...
    func getPathDirection()-> Direction {
        // let directions: [Direction] = [.LEFT, .RIGHT, .UP, .DOWN]
        var directions: [Direction] = []
        ...
        switch self.direction {
        case .LEFT:
            directions = [.LEFT, .UP, .DOWN]
        case .RIGHT:
            directions = [.RIGHT, .UP, .DOWN]
        case .UP:
            directions = [.LEFT, .RIGHT, .UP]
        case .DOWN:
            directions = [.LEFT, .RIGHT, .DOWN]
        default:
            break
        }
        ...
    }
}

優化後的結果

怪物可以走出框框了,而且可以跟隨著主角移動追擊的目標點
https://imgur.com/NQPUM2Q.gif


給怪物不同的移動方式

大家是否有發現怪物的追蹤能力太強了呢?
四隻怪物都一起追著主角跑,有點難度太高了。讓怪物有點不同的移動方式,甚至是有點呆呆的,其實也挺有趣味性的

設定怪物的個性

  • Rain:以主角為目標,追著主角跑,不離不棄
  • Storm:埋伏系,埋伏在有魔幻水晶的位置附近等待突擊
  • Lightning:個性隨性,隨機選路走 (其實沒有要追的意思)
  • Snow:迷糊系,常有意想不到的舉動。與主角距離遠時,以主角為目標前進。與主角距離近時,以湖為移動目標

宣告目標點

新增兩個格子點:

  • 任一個魔幻水晶的位置 (我們先預計會在右下角有一顆,未來會加上)
  • 湖的位置 (右上角)
  • GameScene.swift
struct gridMapping {
    ...
    struct crystalCorner {
        static let x = 15
        static let y = 21
    }
    struct lakeCorner {
        static let x = 14
        static let y = 2
    }
}

調整更新模式方法

在剛剛已經寫好的 updateMode方法中,繼續新增設定,依照怪物種類設定攻擊模式時的目標點

  • Rain:主角的位置 self.sam!.gridXself.sam!.gridY
  • Storm:魔幻水晶的位置 gridMapping.crystalCorner.xgridMapping.crystalCorner.y
  • Lightning:隨機移動,沒有目標點。這邊要調整 startMove 方法,判斷如果角色為 .LIGHTNING,就將 newDirection 設定取隨機移動的路線
  • Snow:計算與主角的距離
    • 當小於 10 個格子時,往湖邊前進 gridMapping.lakeCorner.xgridMapping.lakeCorner.y
    • 當大於等於 10 個格子時,往主角的位置前進 self.sam!.gridXself.sam!.gridY
  • Weather.swift
class Weather: GameCharacter, Move {
    ...
    func updateMode() {
        switch mode {
        case .ATTACK:
            switch self.role {
            case .RAIN:
                self.setTarget(targetX: self.sam!.gridX, targetY: self.sam!.gridY)
            case .STORM:
                self.setTarget(targetX: gridMapping.crystalCorner.x, targetY: gridMapping.crystalCorner.y)
            case .LIGHTNING:
                break
            case .SNOW:
                let lengthA = self.sam!.node.position.x - self.node.position.x
                let lengthB = self.sam!.node.position.y - self.node.position.y
                let distance = sqrtf(Float(lengthA * lengthA + lengthB * lengthB))
                if distance < Float(10 * self.gridWH) {
                    self.setTarget(targetX: gridMapping.lakeCorner.x, targetY: gridMapping.lakeCorner.y)
                } else {
                    self.setTarget(targetX: self.sam!.gridX, targetY: self.sam!.gridY)
                }
            default:
                break
            }
        default:
            break
        }
    }
    func startMove(direction : Direction) {
        ...
        // newDirection = validDirections.contains(bestDirection) ? bestDirection : randomDirection
        newDirection = self.role == .LIGHTNING ? randomDirection :validDirections.contains(bestDirection) ? bestDirection : randomDirection
        ...
    }
}

運行結果

https://imgur.com/qmHnrf5.gif


今日小結

成功的讓怪物有不同風格的移動方式了!
能讓玩家比較猜不透他們的移動路徑/images/emoticon/emoticon70.gif


上一篇
從零開始的8-bit迷宮探險【Level 16】丞相,起風了!遠方飄來烏雲怪物了
下一篇
從零開始的8-bit迷宮探險【Level 18】為什麼他們開始亂跑?捉摸不定的怪物移動模式
系列文
從零開始的8-bit迷宮探險!Swift SpriteKit 遊戲開發實戰30

尚未有邦友留言

立即登入留言