寻路概述
如图,为了简便通常将整个寻路区域划分为方格。格子又称节点,也可以根据实际操作用其他形状或者直接用点位代替。绿色为起点A,红色为终点B,蓝色为不可行的障碍物节点。
寻路关键的是两个列表和一个路径排序公式,开放列表openList,封闭列表closeList,F = G + H。openList存放着待检查的节点,closeList存放着检查过的节点,参考值 = 起点到当前点的代价值 + 当前点到终点的预估值。
通过遍历开放列表,计算出从 A 到 B的最短路线需要走哪些节点就找到了路径。
流程如下:
- 将起点加入到openList中,开始搜索。
- 重复如下搜索步骤:
a. 遍历openList,查找F值最小的节点。
b. 将F值最小的点做为当前要处理的节点,加入到closeList中。
c. 对当前节点的相邻节点做如下操作:
I. 忽略掉不可同行和在closeList中的节点
II. 如果节点不在openList中,则加入到openList中,并且把当前节点设置为它的父节点,计算出该节点的 F ,G , H 值。否则,检查经由该节点到达终点是否更近。如果更近的话,将当前节点替换为该节点并重新计算它的F,G,H的值。 - 当把终点加入到openList中时,路径已经找到。或者没有节点可以加入到openList中时,则路径无法找到。
路径排序
路径排序的关键是这个公式:
F = G + H。
G : 从起点A移动到当前节点的移动代价值。
H: 从当前节点移动到终点B的预估代价值。H值的计算有很多方法。最简单的有“曼哈顿方法”:忽略障碍物和对角线移动,直接计算当前节点横向和纵向移动到终点的距离**(为了方便计算,取10为节点格子的边长。14为对角线长度)**。这是剩余距离的预估带价值,而不是实际值,所以又称为试探法。还有计算对角线和总长度等等方法。
通过曼哈顿方法计算出的值是这样的。每个节点的左下角是G值, 右下角是H值,左上角是F值。
开始搜索
- 首先将起点A放到openList,开始搜索。
如图,检查与A点相邻的节点。将可以行走的节点加到openList中,并设置为A点的子节点,这样就有了最初的9个节点。
然后将A点从openList中移除,加入到closeList中。封闭列表中的点不在需要关注的。
然后开始计算剩下8个节点的F,G,H值。
起点右边的节点(白框标注的点)的 F 值最小,因此我们选择这个节点作为当前要处理的节点。
继续搜索
将当前节点从openList移到closeList中,然后我们检查与它相邻的节点。忽略掉不可行节点和closeList中节点。剩下 4 个相邻的节点均在 openList 中,那就使用 G 值来判定经由这些节点到终点的距离是不是更近。先计算上面的节点,它现在的 G 值为 14 。如果我们经由当前节点, G 值将会变为 20 (其中 10 为到达当前方格的 G 值,此外还要加上从当前方格纵向移动到上面方格的 G 值 10)。显然 20 比 14 大,因此这不是最优的路径。所以将右下角的节点替换为当前的节点。
2. 这次在检查相邻节点的时候,忽略掉墙下一格的节点。这样会防止从墙角直接穿过的情况。*( 注意:穿越墙角的规则是可选的,依赖于你的节点是怎么放置的 )*当前方格下面的 2 个节点还没有加入 openList ,所以把它们加入,同时把当前节点设为他们的父节点。
3. 在剩下的3个节点中,有2个已经在 closeLlist 中 ( 一个是起点,一个是当前方格上面的方格 ) ,忽略它们。最后一个方格,也就是当前方格左边的方格,我们检查经由当前方格到达那里是否具有更小的 G 值。没有。因此我们从 openList 中选择下一个待处理的方格。
4. 不断重复这个过程,直到把终点也加入到了 openList 中。
Laya中的实现
Nodes代码
export default class Nodes{
/**列 */
public x : number;
/**行 */
public y : number;
/**
* 代价值 F = G + H;
*/
public f : number;
/**
* G 起点到当前点的代价值
*/
public g : number;
/**
* H 当前点到终点的预估代价值
*/
public h : number;
/**
* 允许行走
* default true
*/
public walkable : boolean = true;
/**
* 父节点
*/
public parentNode: Nodes;
/**
* 横竖方向每个节点的代价值
*/
public costMultiplier : number = 1;
public constructor ($x: number, $y: number){
this.x = $x;
this.y = $y;
}
}
Gird代码
import Nodes from "./Nodes";
export default class Gird{
/**起点 */
private _startNodes : Nodes;
/**终点 */
private _endNodes : Nodes;
/**Node数组 */
private _nodesArry : Array<any>;
/**网格列 */
private _columnNum : number;
/**网格行 */
private _rowsNum : number;
public constructor($column: number, $rows: number){
this._columnNum = $column;
this._rowsNum = $rows;
// 创建网格数组
this._nodesArry = new Array();
for (let i = 0; i < this._columnNum; i++) {
this._nodesArry[i] = new Array();
for (let j = 0; j < this._rowsNum; j++) {
this._nodesArry[i][j] = new Nodes(i, j);
}
}
}
public getNodes($x: number, $y: number){
return this._nodesArry[$x][$y];
}
public setStartNodes($x: number, $y: number){
this._startNodes = this._nodesArry[$x][$y];
}
public setEndNodes($x: number, $y: number){
this._endNodes = this._nodesArry[$x][$y];
}
public setWalkable($x: number, $y: number, $bool: boolean){
this._nodesArry[$x][$y].walkable = $bool;
}
public get columnNum(): number{
return this._columnNum;
}
public get rowsNum(): number{
return this._rowsNum;
}
public get startNodes(): Nodes{
return this._startNodes;
}
public get endNodes(): Nodes{
return this._endNodes;
}
}
寻路代码
import Gird from "./Gird";
import Nodes from "./Nodes";
export default class AStar{
/**待检查列表 */
private openList : Array<any>;
/**已检查列表 */
private closedList : Array<any>
/**节点网格 */
private gird : Gird;
/**起点Nodes */
private startNode : Nodes;
/**终点Nodes */
private endNode : Nodes;
/**路径 */
private pathList : Array<any>;
/**
* 计算预估代价的算法
*
*/
private heuristic : Function;
/**上下左右走的代价 */
private straightCost : number = 1.0;
/**斜着走的代价 */
private diagCost : number = 1.4;
public constructor(){
this.heuristic = this.diagonal;
}
private diagonal($node: Nodes){
let count_column = Math.abs($node.x - this.endNode.x);
let count_rows = Math.abs($node.y - this.endNode.y);
let diagon = Math.min(count_column, count_rows);
let staright = count_column + count_rows;
return this.diagCost * diagon + this.straightCost * (staright - 2 * diagon);
}
private euclidian($node: Nodes){
let count_column = Math.abs($node.x - this.endNode.x);
let count_rows = Math.abs($node.y - this.endNode.y);
// straightCost是上下走的代价,此处应该为斜着走的代价diagCost
return Math.sqrt(count_column * count_column + count_rows * count_rows) * this.diagCost;
}
// 曼哈顿算法
private manhattan($node: Nodes){
let count_column = Math.abs($node.x - this.endNode.x);
let count_rows = Math.abs($node.y - this.endNode.y);
// straightCost是上下走的代价,此处应该为斜着走的代价diagCost
return (count_column + count_rows) * this.straightCost;
}
// 未检查
private isOpen($node: Nodes){
for (let index = 0; index < this.openList.length; index++) {
if(this.openList[index] == $node){
return true;
}
}
return false;
}
// 已检查
private isClosed($node: Nodes){
for (let index = 0; index < this.closedList.length; index++) {
if (this.closedList[index] == $node){
return true;
}
}
return false;
}
public search(): boolean{
let nodes: Nodes = this.startNode;
while (nodes != this.endNode) {
let startX = Math.max(0, nodes.x - 1);
let endX = Math.min(this.gird.columnNum - 1, nodes.x + 1);
let startY = Math.max(0, nodes.y - 1);
let endY = Math.min(this.gird.rowsNum - 1, nodes.y + 1);
for (let i = startX; i <= endX; i++) {
for (let j = startY; j <= endY; j++) {
// 不让斜着走
// if(i != nodes.x && j != nodes.y) continue;
let testNodes : Nodes = this.gird.getNodes(i, j);
if (testNodes == nodes ||
!testNodes.walkable ||
!this.gird.getNodes(nodes.x, testNodes.y).walkable ||
!this.gird.getNodes(testNodes.x, nodes.y).walkable){
continue;
}
let cost : number = this.straightCost;
if(!((nodes.x == testNodes.x) || (nodes.y == testNodes.y))){
cost = this.diagCost;
}
let value_g = nodes.g + cost * testNodes.costMultiplier;
let value_h = this.heuristic(testNodes);
let value_f = value_g + value_h;
if (this.isOpen(testNodes) || this.isClosed(testNodes)){
if (testNodes.f > value_f){
testNodes.f = value_f;
testNodes.g = value_g;
testNodes.h = value_h;
testNodes.parentNode = nodes;
}
}else{
testNodes.f = value_f;
testNodes.g = value_g;
testNodes.h = value_h;
testNodes.parentNode = nodes;
this.openList.push(testNodes);
}
}
}
this.closedList.push(nodes);
if (this.openList.length <= 0){
console.error("AStar can`t find path");
return false;
}
for (let m = 0; m < this.openList.length; m++) {
for (let n = m + 1; n < this.openList.length; n++) {
if (this.openList[m].f > this.openList[n].f){
let temp = this.openList[m];
this.openList[m] = this.openList[n];
this.openList[n] = temp;
}
}
}
nodes = this.openList.shift() as Nodes;
}
this.buildPath();
return true;
}
private buildPath(): void{
this.pathList = new Array();
let nodes : Nodes = this.endNode;
this.pathList.push(nodes);
while(nodes != this.startNode){
nodes = nodes.parentNode;
this.pathList.unshift(nodes);
}
}
public findPath($gird: Gird): boolean{
this.gird = $gird;
this.openList = new Array();
this.closedList = new Array();
this.startNode = this.gird.startNodes;
this.endNode = this.gird.endNodes;
this.startNode.g = 0;
this.startNode.h = this.heuristic(this.startNode);
this.startNode.f = this.startNode.g + this.startNode.h;
return this.search();
}
get path(){
return this.pathList;
}
}
界面代码
import Grid from "../astar/Gird";
import Node from "../astar/Nodes";
import AStar from "../astar/AStar";
export default class GameUI extends Laya.Scene {
private _player : Laya.Sprite;
private _grid : Grid;
private _index : number;
private _path : Array<any>;
private _width_n : number;
private _height_n : number;
constructor(){
super();
}
onAwake(){
this._width_n = Math.floor(Laya.stage.width / 36);
this._height_n = Math.floor(Laya.stage.height / 84);
console.log("长宽比:", this._width_n, this._height_n);
this.makePlayer();
this.makeGrid();
Laya.stage.on(Laya.Event.CLICK, this, this.onGridClick);
}
makePlayer(){
console.log("makePlayer");
this._player = new Laya.Sprite();
this._player.graphics.drawCircle(0,0,5,"0xff0000");
this._player.x = Math.random() * 36 * this._width_n;
this._player.y = Math.random() * 84 * this._height_n;
Laya.stage.addChild(this._player);
}
makeGrid(){
this._grid = new Grid(36, 84);
for(let index = 0; index < 200; index++)
{
this._grid.setWalkable(Math.floor(Math.random() * 36),
Math.floor(Math.random() * 84), false);
}
this.drawGrid();
}
private drawGrid():void
{
this.graphics.clear();
for(let i = 0; i < this._grid.columnNum; i++)
{
for(let j = 0; j <this._grid.rowsNum; j++)
{
var node : Node =this._grid.getNodes(i, j);
let sp:Laya.Sprite = new Laya.Sprite();
sp.graphics.drawRect( 0, 0, this._width_n, this._height_n, this.getColor(node));
sp.x = i * this._width_n;
sp.y = j * this._height_n;
this.addChild(sp);
}
}
this.addChild(this._player);
}
private getColor(node : Node)
{
if(!node.walkable) return 0;
if(node == this._grid.startNodes) return 0xcccccc;
if(node == this._grid.endNodes) return 0xcccccc;
return 0xffffff;
}
private onGridClick(event:any):void
{
var xpos = Math.floor(event.stageX / this._width_n);
var ypos = Math.floor(event.stageY / this._height_n);
console.log("位置:", xpos, ypos);
if (xpos >= 36 || ypos >= 84) return;
this._grid.setEndNodes(xpos, ypos);
xpos = Math.floor(this._player.x / this._width_n);
ypos = Math.floor(this._player.y / this._height_n);
this._grid.setStartNodes(xpos, ypos);
this.drawGrid();
this.findPath();
}
private findPath():void
{
var aStar : AStar = new AStar();
if(aStar.findPath(this._grid))
{
this._path = aStar.path;
this._index = 0;
console.log("路径:", this._path, this._index);
Laya.timer.loop(100, this, this.onEnterFrame);
}
}
private onEnterFrame():void
{
var targetX = this._path[this._index].x * this._width_n + this._width_n / 2;
var targetY = this._path[this._index].y * this._height_n + this._height_n / 2;
var dx = targetX - this._player.x;
var dy = targetY - this._player.y;
var dist = Math.sqrt(dx * dx + dy * dy);
// console.log("距离:", dist);
if(dist < 1) {
this._index++;
if(this._index >= this._path.length)
{
Laya.timer.clear(this, this.onEnterFrame);
}
} else {
this._player.x += dx * .5;
this._player.y += dy * .5;
}
}
}