简单描述一下A*原理:
两个集合:
OpenList: 可到达的格子
CloseList: 已到达的格子
一个公式:
F = G + H
解释一下:F、G、H是格子的3个属性
G: 从起点走到当前格子的成本,也就是已经花费了多少步。
H: 在不考虑障碍的情况下,从当前格子走到目标格子的距离,也就是距离目标还有多远。
F: G 和 H的综合评估,也就是从起点到达当前格子,再从当前格子到达目标格子的总步数。
第1步,先把起点放入OpenList。
OpenList: Grid(1,2)
CloseList:
第2步,找出OpenList中F值最小的方格作为当前方格。把当前格子移除OpenList,放入CloseList。代表这个格子已经到达并且检查过了。
OpenList:
CloseList: Grid(1,2)
第3步,找出当前格子(刚刚检查过的格子)上、下、左、右所有可到达的格子,看看它们是否在OpenList或者CloseList当中。如果不在,则把它们加入OpenList当中,并计算出相应的G、H、F的值,并把当前格子作为它们的“父节点”。
OpenList: Grid(1,1) Grid(0,2) Grid(2,2) Grid(1,3)
CloseList: Grid(1,2)
Grid(1,2) 是 Grid(1,1) Grid(0,2) Grid(2,2) Grid(1,3) 的父节点
找出F值最小的(F=4)进入下一轮操作,操作方式同上述三步,知道OpenList中出现终点格子为止,最终CloseList就是最终结果。
源码:
AStarAlgorithm.ts
export class Grid {
x: number = 0;
y: number = 0;
f: number = 0; // f = g + h
g: number = 0; // 从起点走到当前格子的成本
h: number = 0; // 不考虑障碍物的情况下,从当前位置走到终点的成本
parent: Grid = null;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
initGrid(parent: Grid, end: Grid) {
this.parent = parent;
if (this.parent !== null) {
this.g = parent.g + 1;
} else {
this.g = 1;
}
this.h = Math.abs(this.x - end.x) + Math.abs(this.y - end.y);
this.f = this.g + this.h;
}
}
export class AStarAlgorithm {
MAZE: Array<Array<number>> = [];
private _aStarSearch(start: Grid, end: Grid): Grid {
let openList: Array<Grid> = [];
let closeList: Array<Grid> = [];
openList.push(start); // 把起点假如openList
while (openList.length > 0) {
let currentGrid = this._findMinGrid(openList);
let index = openList.indexOf(currentGrid);
openList.splice(index, 1);
closeList.push(currentGrid);
let neighbors: Array<Grid> = this._findNeighbors(currentGrid, openList, closeList);
for (let key in neighbors) {
if (openList.indexOf(neighbors[key]) === -1) {
neighbors[key].initGrid(currentGrid, end);
openList.push(neighbors[key]);
}
}
for (let key in openList) {
if (openList[key].x === end.x && openList[key].y === end.y) {
return openList[key];
}
}
}
return null;
}
private _findMinGrid(openList: Array<Grid>): Grid {
let tempGrid = openList[0];
for (let key in openList) {
if (openList[key].f < tempGrid.f) {
tempGrid = openList[key];
}
}
return tempGrid;
}
private _findNeighbors(grid: Grid, openList: Array<Grid>, closeList: Array<Grid>): Array<Grid> {
let gridList: Array<Grid> = [];
if (this._isValidGrid(grid.x, grid.y - 1, openList, closeList)) {
gridList.push(new Grid(grid.x, grid.y - 1));
}
if (this._isValidGrid(grid.x, grid.y + 1, openList, closeList)) {
gridList.push(new Grid(grid.x, grid.y + 1));
}
if (this._isValidGrid(grid.x - 1, grid.y, openList, closeList)) {
gridList.push(new Grid(grid.x - 1, grid.y));
}
if (this._isValidGrid(grid.x + 1, grid.y, openList, closeList)) {
gridList.push(new Grid(grid.x + 1, grid.y));
}
return gridList;
}
private _isValidGrid(x: number, y: number, openList: Array<Grid>, closeList: Array<Grid>): boolean {
// 是否超过边界
if (x < 0 || x >= this.MAZE.length || y < 0 || y >= this.MAZE[0].length) {
return false;
}
// 是否有障碍物
if (this.MAZE[x][y] === 1) {
return false;
}
// 是否已经在openList中
if (this._containGrid(openList, x, y)) {
return false;
}
// 是否已经在closeList中
if (this._containGrid(closeList, x, y)) {
return false;
}
return true;
}
private _containGrid(grids: Array<Grid>, x: number, y: number): boolean {
for (let key in grids) {
if (grids[key].x === x && grids[key].y === y) {
return true;
}
}
return false;
}
public testMain(maze: Array<Array<number>>, sp: cc.Vec2, ep: cc.Vec2): Array<Array<number>> {
this.MAZE = maze;
let startGrid: Grid = new Grid(sp.x, sp.y);
let endGrid: Grid = new Grid(ep.x, ep.y);
let resultGrid: Grid = this._aStarSearch(startGrid, endGrid);
let path: Array<Grid> = [];
while (resultGrid) {
path.push(new Grid(resultGrid.x, resultGrid.y));
resultGrid = resultGrid.parent;
}
for (let i = 0; i < this.MAZE.length; i++) {
let str = "";
for (let j = 0; j < this.MAZE[0].length; j++) {
if (this._containGrid(path, i, j)) {
// str += "*, ";
this.MAZE[i][j] = 4;
} else {
// str += "" + this.MAZE[i][j] + ", ";
}
}
console.log(str);
}
return this.MAZE;
}
}
AStar.ts
import { AStarAlgorithm } from "./AStarAlgorithm";
const { ccclass, property } = cc._decorator;
@ccclass
export default class AStar extends cc.Component {
@property(cc.Node)
nodeMap: cc.Node = null;
@property(cc.Prefab)
prfTile: cc.Prefab = null;
@property
rowNum: number = 10;
@property
columnNum: number = 10;
@property
startRow: number = 1;
@property
startColumn: number = 1;
@property(cc.Color)
colorNormal: cc.Color = cc.Color.WHITE;
@property(cc.Color)
colorStart: cc.Color = cc.Color.GREEN;
@property(cc.Color)
colorEnd: cc.Color = cc.Color.RED;
@property(cc.Color)
colorWall: cc.Color = cc.Color.BLUE;
// 保存地图的二维数组
mapArr: Array<Array<number>> = [];
clickCount: number = 0; // 记录开始/结束位置
startPos: cc.Vec2 = null; // 开始行-列
endPos: cc.Vec2 = null; // 结束行-列
MAZE = [
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
];
onLoad() {
this.node.on('clickCell', this.cellClick.bind(this), this);
}
start() {
this.createMap();
}
private cellClick(data: any) {
let tag = data.detail.tag;
let type = data.detail.type;
let row = Math.floor(tag / this.columnNum);
let col = tag % this.columnNum;
if (type === 0) {
++this.clickCount;
if (this.clickCount % 2 === 1) {
this.refreshMap();
// 设置开始位置
let tile = this.nodeMap.getChildByName("" + tag);
if (tile) {
let tileCtrl = tile.getComponent("Tile");
this.mapArr[row][col] = 2;
tileCtrl.setType(2);
this.startPos = cc.v2(row, col);
}
} else {
// 设置结束位置
let tile = this.nodeMap.getChildByName("" + tag);
if (tile) {
let tileCtrl = tile.getComponent("Tile");
this.mapArr[row][col] = 3;
tileCtrl.setType(3);
this.endPos = cc.v2(row, col);
this.findWay();
}
}
} else {
console.log('4不4洒!!!');
}
}
// 初始化地图
public createMap() {
this.nodeMap.removeAllChildren(true);
let mapSize = this.nodeMap.getContentSize();
let tileWidth = mapSize.width / this.columnNum;
let tileHeight = mapSize.height / this.rowNum;
let startX = -mapSize.width / 2 + tileWidth / 2;
let startY = mapSize.height / 2 - tileHeight / 2;
this.mapArr = [];
let wallArr = this.createWall();
for (let i = 0; i < this.rowNum; i++) {
let arr: Array<number> = [];
for (let j = 0; j < this.columnNum; j++) {
let tile = cc.instantiate(this.prfTile);
tile.setContentSize(cc.size(tileWidth, tileHeight));
let pos = cc.v2(startX + j * tileWidth, startY - i * tileHeight);
tile.setPosition(pos);
let tileCtrl = tile.getComponent("Tile");
let tag = i * this.columnNum + j;
let type = 0;
if (wallArr.indexOf(tag) != -1) {
type = 1;
}
tileCtrl.init(tag, type);
arr.push(type);
this.nodeMap.addChild(tile);
}
this.mapArr.push(arr);
}
}
// 创建墙
public createWall(wallNum: number = 0): Array<number> {
let mapSize = this.rowNum * this.columnNum;
let wn = (wallNum > 0 && wallNum < mapSize) ? wallNum : mapSize / 10;
let wallArr: Array<number> = [];
for (let i = 0; i < wn; i++) {
let rn = Math.floor(Math.random() * mapSize);
if (wallArr.indexOf(rn) != -1)
continue;
wallArr.push(rn);
}
return wallArr;
}
// 更新地图
public onBtnNewMap(event: cc.Event, data: string) {
if (event.type === cc.Node.EventType.TOUCH_END) {
this.createMap();
}
}
// 重置地图:即重置开始以及结束位置
public onBtnResetMap(event: cc.Event, data: string) {
if (event.type === cc.Node.EventType.TOUCH_END) {
this.refreshMap();
}
}
private refreshMap() {
for (let i = 0; i < this.mapArr.length; i++) {
for (let j = 0; j < this.mapArr[0].length; j++) {
let index = i * this.columnNum + j;
let tile = this.nodeMap.getChildByName('' + index);
if (tile) {
let tileCtrl = tile.getComponent("Tile");
if (tileCtrl.type !== 0 && tileCtrl.type !== 1) {
tileCtrl.setType(0);
this.mapArr[i][j] = 0;
}
}
}
}
}
public findWay() {
let asa = new AStarAlgorithm();
let maze = asa.testMain(this.mapArr, this.startPos, this.endPos);
for (let i = 0; i < maze.length; i++) {
for (let j = 0; j < maze[i].length; j++) {
let tag = i * this.columnNum + j;
let tile = this.nodeMap.getChildByName('' + tag);
if (tile) {
let tileCtrl = tile.getComponent("Tile");
if (maze[i][j] === 4 && tileCtrl.type === 0) {
tileCtrl.setType(4);
this.mapArr[i][j] = 4;
}
}
}
}
}
}
Tile.ts
const { ccclass, property } = cc._decorator;
@ccclass
export default class Tile extends cc.Component {
@property(cc.Sprite)
sprTile: cc.Sprite = null;
tag: number = 0; // 唯一标识
type: number = 0; // 0:可走 1:墙 2:起始位置 3:终点位置 4:寻路路径
onLoad() {
}
start() {
this.node.on(cc.Node.EventType.TOUCH_START, this.onTouchStart, this);
this.node.on(cc.Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
this.node.on(cc.Node.EventType.TOUCH_END, this.onTouchEnd, this);
}
private onTouchStart() {
}
private onTouchMove() {
}
private onTouchEnd() {
console.log("[Tile] onTouchEnd tag = " + this.tag);
// this.node.emit('clickCell', this.tag);
var e = new cc.Event.EventCustom("clickCell", true);
e.detail = { tag: this.tag, type: this.type };
this.node.dispatchEvent(e);
}
public init(tag: number, type: number) {
this.tag = tag;
this.type = type;
this.node.name = "" + this.tag;
this.setTileColor();
}
public setType(type: number) {
this.type = type
this.setTileColor();
}
private setTileColor() {
if (this.type === 0) {
this.sprTile.node.color = cc.Color.WHITE;
} else if (this.type === 1) {
this.sprTile.node.color = cc.Color.BLACK;
} else if (this.type === 2) {
this.sprTile.node.color = cc.Color.GREEN;
} else if (this.type === 3) {
this.sprTile.node.color = cc.Color.RED;
} else if (this.type === 4) {
this.sprTile.node.color = cc.Color.YELLOW;
}
}
}