Cocos Creator: 实现A星寻路

在游戏开发中,路径寻找算法是实现角色自动导航(如NPC移动)的关键技术之一。A*(A-Star)算法因其高效和灵活而广泛应用于各类游戏中。本文用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*算法更快地找到最终路径。

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

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

typescript 复制代码
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值,和其他辅助属性如父节点和是否已经被搜索过并被关闭搜索的标志。

typescript 复制代码
@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中:

读取地图数据:

typescript 复制代码
/**
 * 读取地图,得到可以走的全部地块索引   (地图的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);
            }
        }
    }
}

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

typescript 复制代码
/** 寻路 */
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);        
}

生成方块路线:

typescript 复制代码
/**
 * 生成路线
 */
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);
    });
}

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

typescript 复制代码
/**
 * 按照路线移动
 */
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. 分帧计算优化:在多个更新周期内逐步完成,从而避免在单一帧中执行大量计算。
相关推荐
布鲁克零三四四18 天前
Cocos Creator导出obj文件用于后端寻路
cocos creator
烧仙草奶茶21 天前
【cocos creator】输入框滑动条联动小组建
cocos creator·cocos-creator
烧仙草奶茶3 个月前
【cocos creator】2.x里,使用3D射线碰撞检测
3d·cocos creator·cocos-creator·2.x
仅此而已7293 个月前
Cocos Creator倒计时
游戏引擎·cocos creator
仅此而已7293 个月前
cocosUI多分辨率适配
游戏引擎·cocos creator·多分辩率适配
SilenceJude3 个月前
cocos creator 3学习记录01——如何替换图片
前端·cocos creator
GrimRaider4 个月前
[Cocos Creator] v3.8开发知识点记录(持续更新)
开发·cocos creator
S_clifftop5 个月前
cocos creator如何使用cryptojs加解密(及引入方法)
cocos creator·加密解密·cryptojs
平淡风云5 个月前
cocosCreator获取手机剪切板内容
java·智能手机·typescript·android studio·cocos creator
zhenmu5 个月前
【cocos creator 3.x】 修改builtin-unlit 加了一个类似流光显示的mask参数
cocos creator·shader·effect