从零开始的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


<<:  DAY11支持向量机演算法

>>:  Day 13 知己知彼

Day8 手牵手一步两步三步四步望着天 看星星一颗两颗三颗四颗连成线

Scatter and bubble chart 今天继续练习chart,其中scatter和bu...

Day 21 - Memorized Hook: useCallback

如果有错误,欢迎留言指教~ Q_Q 没写完啦 useCallback 回传一个 memoized ...

​ 疫情下的BCP对策

企业或机构日常管理铁三角 1. 合理化:做该做的事、花该花的钱 (1). 省小钱花大钱,乱省一通得不...

连续 30 天 玩玩看 ProtoPie - Day 3

影片继续看下去 选择 Rotate 底下的 Direction 就是顺时钟还是逆时钟旋转。 我们选择...

当责:概念篇

当责 第一次看到「当责」(Accountability) 是无意间瞥见的,当时满是疑惑,觉得这个词文...