Cocos Creator: 实现A星寻路

在游戏开发中,路径寻找算法是实现角色自动导航(如NPC移动)的关键技术之一。A*(A-Star)算法因其高效和灵活而广泛应用于各类游戏中。本文用Cocos Creator实现一个简单A星寻路例子,老规矩,先上效果图。

Cocos Creator: 实现A星寻路

基本概念

在深入A*算法之前,首先需要理解它的三个关键概念:节点的G值、H值和F值。

  • G值(G Cost) :是从起点到当前节点的实际成本。
  • H值(H Cost) :是从当前节点到终点的预估成本,这部分是基于启发式方法估算的。
  • F值(F Cost) :是G值和H值的总和,即 F = G + H。算法在每一步都会尝试选择F值最小的节点继续前进,以期望找到最佳路径。

基础框架

A*算法的实现通常需要以下几个步骤:

  • 初始化:将起点添加到开放列表(Open List)中。

  • 循环以下步骤直到找到终点或开放列表为空

    • 从开放列表中选取F值最小的节点作为当前节点。

    • 将当前节点从开放列表移除,并加入到关闭列表(Closed List)中。

    • 对当前节点的每一个邻居进行以下操作:

      • 如果它不可通过或者已经在关闭列表中,忽略它。
      • 如果它不在开放列表中,计算它的G、H和F值,然后将其加入到开放列表中。
      • 如果它已经在开放列表中,检查通过当前节点到达它的路径是否更优,如果是,则更新其G、H和F值。
  • 重复上述过程,直到终点被加入到关闭列表(此时路径被找到),或者开放列表被清空(没有路径可以到达终点)。

这个基础框架涵盖了A*算法的核心逻辑,但要使算法有效运行,还需要合理地实现启发式函数和邻居节点的处理逻辑。

合适的启发式函数

启发式函数(Heuristic)对A*算法的效率有重大影响。常见的启发式函数有曼哈顿距离(Manhattan Distance)、欧几里得距离(Euclidean Distance)等。选择哪一个取决于问题的具体性质。

例如,在一个允许对角移动的地图上,欧几里得距离可能是一个更合适的选择;而在只允许四方向移动的地图上,曼哈顿距离可能更适用。正确的启发式函数会使得A*算法更快地找到最终路径。

本文地图只允许上下左右四方向移动,因此使用曼哈顿距离:

Cocos Creator: 实现A星寻路

第二步:搭建A*算法的类结构

export class Point {
    // 格子X坐标
    x: number;
    // 格子Y坐标
    y: number;
    // G值
    G: number = 0;
    // H值
    H: number = 0;
    // F值
    F: number = 0;
    // 父节点
    father: Point = null;
    // 是否关闭搜索
    is_close: boolean = false;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

Point代表地图上的一个节点,包含了节点的坐标(x, y),以及A*算法中的核心参数:G、H、F值,和其他辅助属性如父节点和是否已经被搜索过并被关闭搜索的标志。

@ccclass
export default class AStar extends cc.Component {
static start:Point = null;      //起点
static end:Point = null;        //终点
static map: Map<number, Point> = null;   //地图point
static size: cc.Size = null;    //地图尺寸
static arr_open:Array<Point> = [];  //开放队列
static pppp:Point = null;       //执行完寻路,它就有值了,除非没找到
static is_find = false;    //是否已经找到路线
/**
* 获取路线 (此寻路不走斜线)
*/
static getRoute(start: Point, end: Point, map: Map<number, Point>,size: cc.Size) {
//清空上次寻路,并赋值
this.is_find = false;
this.arr_open = [];
this.pppp = null;
this.start = {...start};
this.end = {...end};
this.map = new Map<number,Point>();     
map.forEach((value, key) => {
this.map.set(key,{...value});       //map 里放的是传过来的对象,使用深拷贝
});
this.size = size;
map.get(this.start.x+this.start.y*this.size.width).G = 0;       //起点的G是0
//开始寻路
let route = new Array<Point>();
try {
this.search(this.start);     //内存不够会报错,一般是起点或终点封闭
} catch (error) {
console.error("位置不对");
return route;
}
if(this.pppp){
this.getFather(this.pppp,route);
}
return route;
}
/**
* 寻路
*/    
static search(point:Point){
if(point.x==this.end.x&&point.y==this.end.y){
this.is_find = true;
this.pppp = point;
return ;
}
let arr = this.getAround(point);
arr.forEach(p => {
this.setFather(p,point);
});
//arr按照F排序 从小到大
this.arr_open.sort(this.compare);
//递归继续找
this.arr_open.forEach((pp,index,arr)=>{
if(pp.is_close){        //删除没用的
arr.splice(index,1);
}
if(!this.is_find){
this.search(pp);
}
});
}
/**
* 获取周围上下左右4个点
*/
static getAround(point:Point){
point.is_close = true;
let arr = new Array<Point>();
let index:number;
let p:Point;
//上
if(point.y!=0){     //到顶了,没有上
index = point.x+(point.y-1)*this.size.width;
p = this.map.get(index)
if(p&&!p.is_close){
arr.push(this.map.get(index));
this.arr_open.push(this.map.get(index));    //我也要一份
}
}
//下
if(point.y+1!=this.size.height){        //到底了,没有下
index = point.x+(point.y+1)*this.size.width;
p = this.map.get(index)
if(p&&!p.is_close){
arr.push(this.map.get(index));
this.arr_open.push(this.map.get(index));
}
}
//左
if(point.x!=0){            //同理
index = point.x-1+point.y*this.size.width;
p = this.map.get(index)
if(p&&!p.is_close){
arr.push(this.map.get(index));
this.arr_open.push(this.map.get(index));
}
}
//右
if(point.x+1!=this.size.width){             //同理
index = point.x+1+point.y*this.size.width;
p = this.map.get(index)
if(p&&!p.is_close){
arr.push(this.map.get(index));
this.arr_open.push(this.map.get(index));
} 
}
return arr;
}
/**
* point换父亲,并重新计算G、H、F
*/
static setFather(son:Point,father:Point){
if(!son.father||son.father.G>father.G){
son.father = father;
son.G = son.father.G+1;
son.H = Math.abs(son.x-this.end.x)+Math.abs(son.y-this.end.y);
son.F = son.G+son.H;
}
}
/**
* 比较器
*/
static compare(p1:Point,p2:Point){
if(p1.F>p2.F){
return 1;
}else{
return -1;
}
}
/**
* 递归 把祖宗放进route里面
*/
static getFather(point:Point,route:Array<Point>){
let father = point.father;
if(father){
this.getFather(father,route);
}
route.push(point);    
}
}

AStar类是算法的主体框架,包括地图(map)、起点(start)、终点(end)、以及用于控制搜索过程的开放列表(arr_open)。

getRoute()方法是算法的入口点。它首先清空上一次搜索的结果,然后初始化算法的起点、终点和地图。接着,它调用search()方法开始搜索。

search()是递归搜索的核心。它不断从开放列表中选择F值最小的节点,计算其邻居节点的G、H、F值,并根据这些值更新开放列表和关闭列表。

getAround()是一个辅助方法,用于获取当前节点周围的节点,这对于在网格中寻路尤其重要。而setFather()则是设置节点父关系和计算G、H、F值的方法。

集成到Cocos Creator

首先通过TileMap创建地图,并将底图导入到Cocos Creator中:

Cocos Creator: 实现A星寻路
读取地图数据:

/**
* 读取地图,得到可以走的全部地块索引   (地图的xy是从左上角到右下角)
*/
readMap(){
this.layer_road = this.Map.getLayer("road").getComponent(cc.TiledLayer);
this.layer_wall = this.Map.getLayer("wall").getComponent(cc.TiledLayer);
this.size = this.Map.getMapSize();
// 地板全部加进去
for(let x=0;x<this.size.width;x++){
for(let y=0;y<this.size.height;y++){
let tiled =this.layer_road.getTiledTileAt(x,y,true);
if(tiled.gid!=0){
let point = new Point(x,y);
this.map_point.set(x+y*this.size.width,point);
}
}
}
// 去掉墙
for(let x=0;x<this.size.width;x++){
for(let y=0;y<this.size.height;y++){
let tiled =this.layer_wall.getTiledTileAt(x,y,true);
if(tiled.gid!=0){
this.map_point.delete(x+y*this.size.width);
}
}
}
}

开始寻路按钮触发逻辑:随机设置红块起点位置->随机设置蓝块终点位置->生成绿色方块路径->起点方块移动到终点方块位置:

/** 寻路 */
startAStar(){
// 设置起点位置
this.start_point = this.getRandomPoint();
this.Node_start.setPosition(this.layer_road.getTiledTileAt(this.start_point.x,this.start_point.y,false).node.position);
this.Node_start.zIndex = 2;
// 设置终点位置
this.end_point = this.getRandomPoint();
this.Node_end.setPosition(this.layer_road.getTiledTileAt(this.end_point.x,this.end_point.y,false).node.position);
// 重新生成路线
setTimeout(() => {
this.generateRoute();
}, 300);        
// 移动
setTimeout(() => {
this.move();
}, 400);        
}

生成方块路线:

/**
* 生成路线
*/
generateRoute(){
if(!this.start_point){
console.error("你还没有设置起点");
return;
}
if(!this.end_point){
console.error("你还没有设置终点");
return;
}
//调用工具类获取路线
this.route = AStar.getRoute(this.start_point,this.end_point,this.map_point,this.size);
if(this.route.length==0){
console.log("没有找到路径");
}
//黑色方块提示出来看看,先清空之前的
this.Folder_route.removeAllChildren();
this._routeLineIndex = 0;
// 创建路线方块
this.route.forEach(point=>{
let tiled = this.layer_road.getTiledTileAt(point.x,point.y,false);
let node = cc.instantiate(this.Prefab_route);
this.Folder_route.addChild(node);
node.setPosition(tiled.node.position);
});
}

起点方块按照绿色方块路径的位置移动到终点:

/**
* 按照路线移动
*/
move(){
// 走完了
if(this._routeLineIndex==this.route.length){  
return;
}
let point = this.route[this._routeLineIndex++];
cc.tween(this.Node_start)
.to(.05,{ position: this.layer_road.getTiledTileAt(point.x,point.y,false).node.position })
.call(()=>{this.move();})
.start();
}

到此我们就实现开头效果图的功能。

A*算法的优化

虽然A*算法非常强大,但在某些情况下它的效率仍然可以被优化。常见的优化方法包括减少搜索空间、使用更有效的数据结构来管理开放和关闭列表、以及改进启发函数。

  1. 减少搜索空间:通过剪枝技术排除那些不可能是最优路径的分支。
  2. 数据结构优化:使用优先队列来管理开放列表可以大大提升寻找最小f(n)值的效率。
  3. 启发函数优化:选择或设计更适合具体问题的启发函数。
  4. 分帧计算优化:在多个更新周期内逐步完成,从而避免在单一帧中执行大量计算。

原文链接:https://juejin.cn/post/7320791256014651407 作者:指尖上的生活

(0)
上一篇 2024年1月8日 上午10:05
下一篇 2024年1月8日 下午4:06

相关推荐

发表回复

登录后才能评论