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. 分帧计算优化:在多个更新周期内逐步完成,从而避免在单一帧中执行大量计算。
相关推荐
LcGero9 小时前
Cocos Creator 3.x 高维护性打字机对话系统设计与实现
cocos creator·打字机
LcGero18 小时前
Cocos Creator 三端接入穿山甲 SDK
sdk·cocos creator·穿山甲
LcGero2 天前
Cocos Creator平台适配层框架设计
cocos creator·平台·框架设计
LcGero3 天前
Cocos Creator 业务与原生通信详解
android·ios·cocos creator·游戏开发·jsb
LcGero4 天前
TypeScript 快速上手:前言
typescript·cocos creator·游戏开发
Setsuna_F_Seiei4 天前
CocosCreator 游戏开发 - 多维度状态机架构设计与实现
前端·cocos creator·游戏开发
CodeCaptain3 个月前
cocoscreator 2.4.x 场景运行时的JS生命周期浅析
cocos creator·开发经验
CodeCaptain3 个月前
CocosCreator 3.8.x [.gitignore]文件内容,仅供参考
经验分享·cocos creator
VaJoy4 个月前
Cocos Creator Shader 入门 (21) —— 高斯模糊的高性能实现
前端·cocos creator
烛阴5 个月前
Luban集成CocosCreator完整教程
前端·typescript·cocos creator