1-heuristic寻路简介
heuristic是启发式的意思,因此heuristic寻路就是启发式寻路,其所用的算法是贪心算法。
启发式寻路之所以是启发式的,是因为在寻路的过程中,我们会给它一个提示,比如你每迈出一步的时候,要朝离目标点最近的位置迈出。
在上图中,从start节点向end节点寻路的时候,它所迈出的每一步都是离end最近的。
可是每一步最近不代表整条路就是最近的,比如它从上面越过障碍物会更近。
这种只在乎眼前的利益,不考虑全局得失的算法,就是贪心算法。
启发式寻路虽然有很大的缺陷,但它是寻路的基础算法之一,以后我们可以以此为基础进行不断优化。
2-ts代码实现
以下代码是寻路逻辑的实现。至于效果的显示,我是用canvas做的,不算重点,我就不贴出来了,想要的可以微信我(1051904257)。
typescript
/* 地图尺寸 */
const mapSize = new Vector2(10, 10)
/* 起点 */
const start = 41
/* 终点 */
const end = 48
/* 障碍物 */
const obstruction: number[] = [33, 34, 44, 54, 64, 74]
/* 探索出的路 */
const road: number[] = []
/* 探索基点 */
let basic: number | null = start
/* 探索状态:finding探索中、fail探索失败、success探索成功 */
let state: 'finding' | 'fail' | 'success' = 'finding'
/* 开始探索 */
// 循环探索
while (state === 'finding') {
explorer()
}
// 逐步探索
/* let n = 0
while (n < 2) {
explorer()
n++
} */
/* 寻路 */
function explorer() {
if (basic === null) {
return
}
const { width, height } = mapSize
const p = getPos(basic)
const x1 = Math.max(0, p.x - 1)
const x2 = Math.min(width, p.x + 1)
const y1 = Math.max(0, p.y - 1)
const y2 = Math.min(height, p.y + 1)
let minPoint: number | undefined
let minDistance = Infinity
for (let y = y1; y <= y2; y++) {
for (let x = x1; x <= x2; x++) {
const current = y * width + x
// 判断是否是终点
if (current === end) {
state = 'success'
return
}
if (
current !== basic &&
current !== start &&
!road.includes(current) &&
!obstruction.includes(current)
) {
// 获取到目标最近的点
const distance = getPos(end).distanceToSquared(new Vector2(x, y))
if (distance < minDistance) {
minDistance = distance
minPoint = current
}
}
}
}
if (minPoint === undefined) {
state = 'fail'
} else {
/* 添加路线 */
road.push(minPoint)
//* 更新基点 */
basic = minPoint
}
}
/*获取点位 */
function getPos(n: number) {
const { width } = mapSize
return new Vector2(n % width, Math.floor(n / width))
}
解释一下上面的代码。
mapSize是地图尺寸,它没有单位的概念,不要将其理解为像素。
start起点、end终点的概念无需多说,其值是其在地图中的索引位。
我们可以通过getPos(n: number)方法获取索引位所对应的行列位。
arduino
function getPos(n: number) {
const { width } = mapSize
return new Vector2(n % width, Math.floor(n / width))
}
obstruction 是障碍物,其内元素是构成障碍物的节点的索引位。
road 是探索出的道路,其内元素是构成道路的节点的索引位。
basic 是探索基点,比如一开始的探索基点是起点,从起点迈出离终点最近的一步后,我们会以这一步的节点为基点,继续往前探索。
state 是探索状态,finding探索中、fail探索失败、success探索成功。
explorer() 是寻路方法,它会遍历基点周边的8个节点,然后找到离终点最近的点,并记下探索状态state。state为finding时,会继续探索,否则停止探索。
ini
while (state === 'finding') {
explorer()
}
当然,为了便于查看寻路的过程,我们也可以逐步寻路。
scss
let n = 0
while (n < 2) {
explorer()
n++
}
整体的heuristic寻路逻辑便是如此,不过它还有一个严重的bug,那就是可能会被死胡同堵死。
在数字为8的黑色格子处,已经无路可走了,便认为探索失败了。
在这种情况下,我可以尝试让它回头,回到上一个路口4,不再走这条死路,而是走离终点较近的另一条路。
接下来,我们就解决一下这个Bug。
3-回头是岸
突然想起一句话:学海无涯,回头是岸。人生路漫漫哪有一帆风顺的,遇到过不去的可以绕道试试。
言归正传,继续说一下heuristic遇到死胡同时,如何回头是岸,绕道而行。
整体代码如下:
typescript
/* 地图尺寸 */
const mapSize = new Vector2(10, 10)
/* 起点 */
const start = 40
/* 终点 */
const end = 49
/* 障碍物 */
const obstruction: number[] = [
31, 32, 42, 52, 62, 72, 33, 34, 44, 54, 64, 74, 36, 37, 38, 48, 58, 68, 78,
]
/* 探索过的基点集合 */
const explored: Map<number, number | undefined> = new Map([[start, undefined]])
/* 探索出的路 */
const road: number[] = []
/* 探索基点 */
let basic: number | null = start
/* 探索状态:finding探索中、fail探索失败、success探索成功 */
let state: 'finding' | 'fail' | 'success' = 'finding'
/* 开始探索 */
// 循环探索
while (state === 'finding') {
explorer()
}
// 逐步探索
/* let n = 0
while (n < 22) {
explorer()
n++
} */
/* 寻路 */
function explorer(extrude?: number) {
if (basic === null) {
return
}
const { width, height } = mapSize
const p = getPos(basic)
const x1 = Math.max(0, p.x - 1)
const x2 = Math.min(width, p.x + 1)
const y1 = Math.max(0, p.y - 1)
const y2 = Math.min(height, p.y + 1)
let minPoint: number | undefined
let minDistance = Infinity
for (let y = y1; y <= y2; y++) {
for (let x = x1; x <= x2; x++) {
const current = y * width + x
if (current === end) {
state = 'success'
return
}
if (
current !== basic &&
current !== start &&
!road.includes(current) &&
!obstruction.includes(current) &&
((extrude !== undefined && extrude !== current) ||
extrude === undefined)
) {
// 获取到目标最近的点
const distance = getPos(end).distanceToSquared(new Vector2(x, y))
if (distance < minDistance) {
minDistance = distance
minPoint = current
}
}
}
}
/* 基于前面点寻找除当前点之外的点 */
if (minPoint === undefined) {
const prev = explored.get(basic)
if (prev !== undefined) {
extrude = basic
basic = prev
explorer(extrude)
} else {
state = 'fail'
return
}
} else {
if (extrude !== undefined) {
const ind = road.indexOf(extrude)
if (ind !== -1) {
road.splice(ind)
}
}
/* 设置标记点 */
explored.set(minPoint, basic)
/* 添加路线 */
road.push(minPoint)
//* 更新基点 */
basic = minPoint
}
}
/* 获取点位 */
function getPos(n: number) {
const { width } = mapSize
return new Vector2(n % width, Math.floor(n / width))
}
效果如下:
说一下我们添加了哪些代码。
explored 是探索过的基点集合,其中元素的key就是节点的索引位,value 是其前面的点。
typescript
const explored: Map<number, number | undefined> = new Map([[start, undefined]])
explored 可以让我们记住来路,以便回头。
explorer(extrude?: number) 方法中有了一个extrude参数,表示要在遍历周围8点时,排除的点。此点用于在会回头时,不再走过去被堵死的老路,而是走一条新路。
其排除逻辑如下:
yaml
(extrude !== undefined && extrude !== current)
被堵时的回头逻辑如下:
ini
if (minPoint === undefined) {
const prev = explored.get(basic)
if (prev !== undefined) {
extrude = basic
basic = prev
explorer(extrude)
} else {
state = 'fail'
return
}
}else{
......
}
走出死胡同时,需要把死胡同里的路删掉。
scss
if (minPoint === undefined) {
......
} else {
if (extrude !== undefined) {
const ind = road.indexOf(extrude)
if (ind !== -1) {
road.splice(ind)
}
}
......
}
关于heuristic寻路咱们就说到这,下一章我们会再说更优良的寻路方法。