效果
每个单元格内文字:
(F) (Price)
(G) (H)
原理
原理是参考另一篇csdn博文,不过忘记收藏找不到了
- 初始化
open_set
和close_set
。 - 将 起点 加入
open_set
中,并设置优先级为0(优先级最高)。 - 如果
open_set
不为空,则从open_set
中选取优先级最高的节点n
- 如果节点
n
为终点,则:从终点开始逐步追踪prev
(前一个)节点,一直达到起点;返回找到的结果路径,算法结束; - 如果节点
n
不是终点,则:将节点n
从open_set
中删除,并加入close_set
中;遍历节点n
所有的邻近节点。- 如果邻近节点
m
在close_set
中,则:跳过,选取下一个邻近节点; - 如果邻近节点
m
在open_set
中,则:判断节点n
到节点m
的F(n) + cost[n,m]
值是否<
节点m
的F(m)
。来尝试更新该点,重新设置f值和父节点等数据; - 如果邻近节点
m
也不在open_set
中,则:设置节点m
的prev
为节点n
计算节点m的优先级将节点m
加入open_set
中。
- 如果邻近节点
- 如果节点
个人理解
1、不用考虑死胡同情况,不同于普通的按照方向搜索,a* 遍历临近节点,即使碰到死胡同,也不需要回归到非死胡同节点。
代码
直接保存以下为html执行
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>a*</title>
</head>
<body>
<canvas id="cv" width="1000px" height="1000px"></canvas>
<script>
/**
* 输入起始、目标 坐标,返回寻路结果。
* 每个单元格都有代价值
*/
//初始点
let sx, sy;
//目标点
let es, ey;
//网格数量
let mx = 20, my = 20;
//格子大小
let cell_size = 40;
//初始化网格数据
let cells = [];
//不可逾越代价
let notAllowPrice = 10;
//代价上限
let priceLimit = 11;
//节点对象
function cell(x, y, price) {
this.x = x;
this.y = y;
//关联的之前单元
this.prev = null;
//关联单元格坐标
this.relation = new Array(8);
this.relation[0] = y == 0 ? null : [x, y - 1];
this.relation[1] = (y == 0 || x == mx - 1) ? null : [x + 1, y - 1];
this.relation[2] = (x == mx - 1) ? null : [x + 1, y];
this.relation[3] = (x == mx - 1 || y == my - 1) ? null : [x + 1, y + 1];
this.relation[4] = (y == my - 1) ? null : [x, y + 1];
this.relation[5] = (x == 0 || y == my - 1) ? null : [x - 1, y + 1];
this.relation[6] = (x == 0) ? null : [x - 1, y];
this.relation[7] = (x == 0 || y == 0) ? null : [x - 1, y - 1];
//通行代价
this.price = ~~(price);
}
//初始化画布
let el = document.getElementById('cv');
let context = el.getContext("2d");
let canvas_width = el.width;
let canvas_height = el.height;
//初始化单元数据
function loadGridData() {
cells = [];
for (let i = 0; i < my; i++) {
for (let j = 0; j < mx; j++) {
cells.push(new cell(j, i, Math.random() * priceLimit))
}
}
}
//设置初始点,目标点
function setStartAndEnd(x0, y0, x1, y1) {
if (x0 != null && y0 != null && x1 != null && y1 != null) {
sx = x0;
sy = y0;
ex = x1;
ey = y1;
} else {
//设置初始点
sx = ~~(Math.random() * mx);
sy = ~~(Math.random() * my);
// sx = 1;
// sy = 17;
//设置目标点
ex = ~~(Math.random() * mx);
ey = ~~(Math.random() * my);
}
let start = cells[sy * my + sx],
end = cells[ey * my + ex];
//确保起始点\终点是可通行
start.price = 0;
end.price = 0;
start.f = 0;
end.f = 0;
start.g = 0;
return {start: start, end: end}
}
//绘制画布
function paint() {
//清空之前
context.fillStyle = '#fff';
context.fillRect(0,0,canvas_width,canvas_height);
context.stokeStyle = `#999`;
context.fillStyle = `#000`;
for (let i = 0, cell; (cell = cells[i]) != null; i++) {
if (cell.price < notAllowPrice) {
context.rect(cell.x * cell_size, cell.y * cell_size, cell_size, cell_size)
} else {
context.fillRect(cell.x * cell_size, cell.y * cell_size, cell_size, cell_size)
}
}
context.stroke();
}
//开始寻路
async function start(x0,y0,x1,y1) {
//初始单元数据
loadGridData();
//绘制画板
paint();
//初始
let {start, end} = setStartAndEnd(...arguments);
//绘制起始点
context.fillStyle = "green";
context.fillRect(start.x * cell_size, start.y * cell_size, cell_size, cell_size);
context.fillStyle = "red";
context.fillRect(end.x * cell_size, end.y * cell_size, cell_size, cell_size);
console.log('start:', start);
console.log('end:', end);
//寻路结果
let result = [];
//检测列表
let open_set = [];
//关闭列表
let close_set = [];
//从起始点开始检测
open_set.push(start);
let bestCell;
while (open_set.length) {
open_set.sort((a, b) => a.f - b.f);
bestCell = open_set[0];
if (bestCell == end) {
while (bestCell.prev) {
result.push(bestCell);
bestCell = bestCell.prev;
}
console.log('result:', result);
break;
} else {
close_set.push(open_set.shift());
let relCell;
for (let i = 0; i < bestCell.relation.length; i++) {
relCell = bestCell.relation[i];
if (relCell) {
relCell = cells[relCell[1] * my + relCell[0]];
//临近点不可通过|临近点已被关闭|临界点为夹角点不可到达
if (relCell.price >= notAllowPrice || close_set.includes(relCell) || !angleAllowCell(bestCell, relCell)) {
continue;
}
if (open_set.includes(relCell)) {
if (bestCell.f + cost(bestCell, relCell) < relCell.f) {
relCell.prev = bestCell;
relCell.g = ~~(relCell.prev.g+cost(relCell.prev,relCell));
relCell.f = ~~(relCell.g + cost(relCell, end)+relCell.price);
relCell.h = ~~cost(relCell,end);
}
continue
}
relCell.prev = bestCell;
//g的计算方式为 前一个单元 的G + 当前单元与前一个单元的距离
relCell.g = ~~(relCell.prev.g+cost(relCell.prev,relCell));
relCell.f = ~~(relCell.g + cost(relCell, end)+relCell.price);
relCell.h = ~~cost(relCell,end);
open_set.push(relCell);
//标记为检测
await delay(5).then(() => {
context.fillStyle = "rgba(50,238,255,0.1)";
context.fillRect(relCell.x * cell_size, relCell.y * cell_size, cell_size, cell_size);
paintRelArrow(relCell, relCell.prev);
paintCellText(relCell, relCell.f, 2, 10);//F
paintCellText(relCell,relCell.g,2,cell_size -2);//G
paintCellText(relCell,relCell.h,cell_size - 13,cell_size-2);//H
paintCellText(relCell,relCell.price,cell_size - 10,10);//price
})
}
}
}
}
if(result.length){
//标记路线
result.shift();//移出终点
result.forEach(d => {
context.fillStyle = "rgba(255,253,114,0.6)";
context.fillRect(d.x * cell_size, d.y * cell_size, cell_size, cell_size);
});
}else{
console.log('目标不可到达')
}
}
/*================================utils=======================================*/
/**
* 计算两点间曼哈顿距离(可以换成欧拉)
*/
function cost(a, b) {
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y)
}
/**
* 绘制先后关系指向箭头,由后者指向前者并在后者中标注
* @param child 子单元
* @param parent
*/
function paintRelArrow(after, prev) {
let directions = ['↑', '↗', '→', '↘', '↓', '↙', '←', '↖'];
for (let i = 0; i < 8; i++) {
if (after.relation[i] && after.relation[i][0] == prev.x && after.relation[i][1] == prev.y) {
//←↑→↓↖↙↗↘↕
let text = directions[i];
paintCellText(after, text, cell_size / 2 - 5, cell_size / 2 + 5);
break
}
}
}
/**
* tb 相对 fa 的方向t
* @param fa cell
* @param tb cell
* @param isStrict 是否严格要求相邻
*/
function directionType(fa, tb, isStrict) {
if (isStrict) {
for (let i = 0; i < 8; i++) {
if (fa.relation[i] && fa.relation[i][0] == tb.x && fa.relation[i][1] == tb.y) {
//←↑→↓↖↙↗↘↕
return i
}
}
} else {
let dx = tb.x - fa.x,
dy = tb.y - fa.y;
if (dx < 0) {
return dy < 0 ? 7 : (dy == 0 ? 6 : 5)
}
if (dx == 0) {
return dy < 0 ? 0 : (dy == 0 ? -1 : 4)
}
if (dx > 0) {
return dy < 0 ? 1 : (dy == 0 ? 2 : 3)
}
}
}
/**
* 根据x,y 坐标获取实际的cell对象
* @param x
* @param y
*/
function getCellByXY(x, y) {
return cells[y * my + x];
}
/**
* 目标临近点是否是当前节点的夹角点,判断标准为临近点为当前点的对角点且对角两侧为不可通过点
*/
function angleAllowCell(a, b) {
let relation = a.relation;
if (relation[1] && b == getCellByXY(...relation[1])) {
return relation[0].price < notAllowPrice || relation[2].price < notAllowPrice
}
if (relation[3] && b == getCellByXY(...relation[3])) {
return relation[2].price < notAllowPrice || relation[4].price < notAllowPrice
}
if (relation[5] && b == getCellByXY(...relation[5])) {
return relation[4].price < notAllowPrice || relation[6].price < notAllowPrice
}
if (relation[7] && b == getCellByXY(...relation[7])) {
return relation[6].price < notAllowPrice || relation[0].price < notAllowPrice
}
return true
}
/**
* 给指定单元格的指定位置绘制文字
* @param cell
* @param text
* @param x
* @param y
*/
function paintCellText(cell, text, x, y) {
context.font = '12px';
context.fillStyle = '#000';
context.fillText(text, cell.x * cell_size + x, cell.y * cell_size + y);
}
/**
* 延时
*/
function delay(time) {
time = time || 1000;
return new Promise((res, rej) => {
setTimeout(res, time)
})
}
start();
/**
* 7 0 1
* 6 * 2
* 5 4 3
*初始化open_set和close_set;
* 将起点加入open_set中,并设置优先级为0(优先级最高);
* 如果open_set不为空,则从open_set中选取优先级最高的节点n:
* 如果节点n为终点,则:
* 从终点开始逐步追踪parent节点,一直达到起点;
* 返回找到的结果路径,算法结束;
* 如果节点n不是终点,则:
* 将节点n从open_set中删除,并加入close_set中;
* 遍历节点n所有的邻近节点:
* 如果邻近节点m在close_set中,则:
* 跳过,选取下一个邻近节点
* 如果邻近节点m在open_set中,则:
* 判断节点n到节点m的 F(n) + cost[n,m] 值是否 < 节点m的 F(m) 。
* 来尝试更新该点,重新设置f值和父节点等数据
* 如果邻近节点m也不在open_set中,则:
* 设置节点m的parent为节点n
* 计算节点m的优先级
* 将节点m加入open_set中
*
*/
</script>
</body>
</html>