加载+校验+坐标互转+障碍物检测+角色避障移动+AI智能寻路,直接用不踩坑

import { _decorator, Component, Node, TiledMap, TiledLayer, Vec2, Input, input, EventTouch, resources, error, log, Vec3 } from 'cc';
const { ccclass, property } = _decorator;

// A*寻路 节点类(内部使用)
class AStarNode {
    public x: number = 0;          // 瓦片X坐标
    public y: number = 0;          // 瓦片Y坐标
    public g: number = 0;          // 起点到当前节点代价
    public h: number = 0;          // 当前节点到终点预估代价
    public f: number = 0;          // 总代价 f=g+h
    public parent: AStarNode = null;// 父节点(回溯路径用)
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
    // 计算总代价
    public calcF(): void {
        this.f = this.g + this.h;
    }
}

@ccclass('TiledMapFullTool')
export class TiledMapFullTool extends Component {
    @property({tooltip: "resources下地图路径(无.tmx后缀,例map/gameMap)"})
    mapPath: string = "";
    @property({tooltip: "地图节点名称"})
    mapNodeName: string = "GameTiledMap";
    @property({tooltip: "障碍物图层名(和Tiled一致,英文)"})
    obstacleLayerName: string = "Obstacle";
    @property({tooltip: "需移动的角色节点"})
    roleNode: Node = null;
    @property({tooltip: "角色移动速度"})
    moveSpeed: number = 150;
    @property({tooltip: "寻路是否允许斜向移动(默认禁止,避免穿墙)"})
    allowDiagonal: boolean = false;

    private tiledMap: TiledMap = null;
    private obstacleLayer: TiledLayer = null;
    private targetWorldPos: Vec3 = null;
    private isMoving: boolean = false;
    private movePath: Vec2[] = []; // A*寻路路径队列

    start() {
        this.loadAndInitMap();
    }

    // 1. 地图加载+全初始化
    async loadAndInitMap() {
        if(!this.mapPath || !this.roleNode) {error("请补全地图路径和角色节点!"); return;}
        log("地图初始化中...");
        try {
            let mapNode = this.node.getChildByName(this.mapNodeName);
            if(!mapNode) {
                mapNode = new Node(this.mapNodeName);
                this.node.addChild(mapNode);
            }
            mapNode.setAnchorPoint(0, 0);
            this.tiledMap = mapNode.getComponent(TiledMap) || mapNode.addComponent(TiledMap);
            
            const tmxRes = await resources.load<TiledMap>(this.mapPath);
            this.tiledMap.tmxAsset = tmxRes;
            this.initObstacleLayer();
            this.checkMapCompatibility();
            this.bindClickGetTileAndMove(); // 绑定点击A*寻路移动
            log("✅ 地图+角色+A*寻路初始化完成");
        } catch (e) {
            error(`❌ 初始化失败:${e}`);
        }
    }

    // 障碍物图层初始化
    initObstacleLayer() {
        if(!this.tiledMap) return;
        this.obstacleLayer = this.tiledMap.getLayer(this.obstacleLayerName);
        this.obstacleLayer ? log(`✅ 障碍物图层【${this.obstacleLayerName}】加载成功`) : error(`❌ 未找到障碍物图层`);
    }

    // 2. 兼容性校验
    checkMapCompatibility() {
        if(!this.tiledMap) return;
        this.tiledMap.tmxAsset?.rawXml ? log("✅ 地图格式(XML)合规") : error("❌ 地图需导出XML格式");
        const invalidLayers = this.tiledMap.getLayers().filter(l=>l.name.includes(" ")||/[\u4e00-\u9fa5]/.test(l.name));
        invalidLayers.length>0 ? error(`❌ 非法图层名:${invalidLayers.map(l=>l.name)}`) : log("✅ 图层名称合规");
        this.tiledMap.getTileSets().length>0 ? log("✅ 瓦片集正常") : error("❌ 瓦片集缺失");
        /[\u4e00-\u9fa5]/.test(this.mapPath) ? error("❌ 路径含中文") : log("✅ 路径合规");
    }

    // 3. 坐标互转(核心)
    worldToTile(worldPos: Vec2): Vec2 {
        if(!this.tiledMap) return Vec2.ZERO;
        const tileSize = this.tiledMap.tileSize;
        const mapWPos = this.tiledMap.node.worldPosition;
        return new Vec2(Math.floor((worldPos.x-mapWPos.x)/tileSize.width),Math.floor((mapWPos.y-worldPos.y)/tileSize.height));
    }

    tileToWorld(tilePos: Vec2): Vec2 {
        if(!this.tiledMap) return Vec2.ZERO;
        const tileSize = this.tiledMap.tileSize;
        const mapWPos = this.tiledMap.node.worldPosition;
        return new Vec2(mapWPos.x+tilePos.x*tileSize.width+tileSize.width/2,mapWPos.y-tilePos.y*tileSize.height-tileSize.height/2);
    }

    // 4. 障碍物检测(2种)
    isObstacleAtTile(tilePos: Vec2): boolean {
        if(!this.obstacleLayer) return false;
        const mapSize = this.tiledMap.getMapSize();
        if(tilePos.x<0||tilePos.x>=mapSize.width||tilePos.y<0||tilePos.y>=mapSize.height) return true;
        return this.obstacleLayer.getTileGIDAt(tilePos) > 0;
    }

    isObstacleAtWorld(worldPos: Vec2): boolean {
        return this.isObstacleAtTile(this.worldToTile(worldPos));
    }

    // 5. 角色沿路径移动(适配A*寻路)
    update(deltaTime: number) {
        if(!this.isMoving || !this.roleNode || this.movePath.length === 0) return;
        
        // 取当前路径第一个目标点
        const targetTile = this.movePath[0];
        const targetWorld = this.tileToWorld(targetTile);
        this.targetWorldPos = new Vec3(targetWorld.x, targetWorld.y, this.roleNode.worldPosition.z);

        const dir = this.targetWorldPos.subtract(this.roleNode.worldPosition).normalize();
        const moveStep = dir.multiplyScalar(this.moveSpeed * deltaTime);

        // 到达当前目标点,移除路径队列第一个元素
        if(this.roleNode.worldPosition.distance(this.targetWorldPos) < moveStep.length()) {
            this.roleNode.worldPosition = this.targetWorldPos;
            this.movePath.shift();
            // 路径走完,停止移动
            if(this.movePath.length === 0) {
                this.isMoving = false;
                log("角色寻路移动到位");
            }
            return;
        }
        this.roleNode.worldPosition = this.roleNode.worldPosition.add(moveStep);
    }

    // 6. A*寻路核心算法(新增核心)
    findAStarPath(startTile: Vec2, endTile: Vec2): Vec2[] {
        const mapSize = this.tiledMap.getMapSize();
        // 边界/障碍物判断
        if(this.isObstacleAtTile(startTile) || this.isObstacleAtTile(endTile)) return [];
        if(startTile.x<0||startTile.x>=mapSize.width||startTile.y<0||startTile.y>=mapSize.height) return [];
        if(endTile.x<0||endTile.x>=mapSize.width||endTile.y<0||endTile.y>=mapSize.height) return [];

        const openList: AStarNode[] = []; // 待检测节点
        const closeList: AStarNode[] = []; // 已检测节点
        const startNode = new AStarNode(startTile.x, startTile.y);
        const endNode = new AStarNode(endTile.x, endTile.y);
        openList.push(startNode);

        // 寻路方向(上下左右+可选斜向)
        const dirs = this.allowDiagonal ? 
        [[0,-1],[1,0],[0,1],[-1,0],[-1,-1],[1,-1],[1,1],[-1,1]] : // 8方向
        [[0,-1],[1,0],[0,1],[-1,0]]; // 4方向(推荐)

        while(openList.length > 0) {
            // 取openList中f值最小的节点
            let curNode = openList[0];
            openList.forEach(node => {if(node.f < curNode.f) curNode = node;});
            // 移除当前节点到关闭列表
            openList.splice(openList.indexOf(curNode), 1);
            closeList.push(curNode);

            // 到达终点,回溯路径
            if(curNode.x === endNode.x && curNode.y === endNode.y) {
                const path: Vec2[] = [];
                let tempNode = curNode;
                while(tempNode.parent) {
                    path.unshift(new Vec2(tempNode.x, tempNode.y));
                    tempNode = tempNode.parent;
                }
                return path;
            }

            // 遍历相邻节点
            for(let dir of dirs) {
                const newX = curNode.x + dir[0];
                const newY = curNode.y + dir[1];
                const newTile = new Vec2(newX, newY);
                // 越界/障碍物/已在关闭列表,跳过
                if(this.isObstacleAtTile(newTile) || closeList.some(node=>node.x===newX&&node.y===newY)) continue;
                
                const neighborNode = new AStarNode(newX, newY);
                const isInOpen = openList.some(node=>node.x===newX&&node.y===newY);
                // 计算代价:g=移动代价(斜向14,直向10),h=曼哈顿距离
                neighborNode.g = curNode.g + (dir[0]!==0&&dir[1]!==0 ? 14 : 10);
                neighborNode.h = Math.abs(newX - endNode.x)*10 + Math.abs(newY - endNode.y)*10;
                neighborNode.calcF();
                neighborNode.parent = curNode;

                // 不在开放列表,直接加入
                if(!isInOpen) {
                    openList.push(neighborNode);
                } else {
                    // 已在开放列表,判断是否更优路径
                    const oldNode = openList.find(node=>node.x===newX&&node.y===newY);
                    if(neighborNode.g < oldNode.g) {
                        oldNode.g = neighborNode.g;
                        oldNode.calcF();
                        oldNode.parent = curNode;
                    }
                }
            }
        }
        log("❌ 无可用寻路路径");
        return []; // 无路径返回空
    }

    // 角色A*寻路移动到目标瓦片(新增)
    moveRoleByAStar(endTile: Vec2) {
        if(!this.tiledMap || !this.roleNode) return;
        // 获取角色当前瓦片坐标
        const roleWorldPos = new Vec2(this.roleNode.worldPosition.x, this.roleNode.worldPosition.y);
        const startTile = this.worldToTile(roleWorldPos);
        // A*寻路获取路径
        this.movePath = this.findAStarPath(startTile, endTile);
        if(this.movePath.length === 0) return;
        this.isMoving = true;
        log(`A*寻路成功,路径长度:${this.movePath.length} 步`);
    }

    // 绑定点击地图:A*寻路移动(避障+绕路)
    bindClickGetTileAndMove() {
        input.on(Input.EventTouch.END, (event: EventTouch) => {
            if(!this.tiledMap || !this.roleNode) return;
            const worldPos = event.getUILocation();
            const targetTile = this.worldToTile(worldPos);
            const isObs = this.isObstacleAtTile(targetTile);
            log(`点击瓦片(${targetTile.x},${targetTile.y}) | 障碍物:${isObs?"是":"否"}`);
            if(!isObs) this.moveRoleByAStar(targetTile);
        }, this);
    }
}

AI寻路版 完整教程(适配以上六合一脚本)

一、 前置准备(和之前五合一一致,新增1项)

1. 工具版本:Cocos3.8.0 + Tiled1.4.x(必用,避免兼容问题)

2. 地图/角色资源准备:同上,地图资源放assets/resources/map

3. Tiled关键配置:新增障碍物图层必须封闭(否则A*会直接穿),其他同上

二、 脚本配置(3步,新增1个关键属性)

1. 脚本挂Canvas,填好mapPath、obstacleLayerName、roleNode、moveSpeed

2. 新增属性allowDiagonal:默认false(4方向寻路),推荐不开启(避免斜向穿墙)

3. 角色锚点必须设(0.5,0.5),地图锚点自动(0,0),无需改

三、 运行测试(2步)

1. 运行后点击地图可通行点:角色自动算最优路径,绕开障碍物移动

2. 测试边界场景:

◦ 点击障碍物:控制台提示,不移动

◦ 点击无路径区域:控制台提示「无可用寻路路径」

◦ 点击地图外:判定为障碍物,不移动

四、 AI寻路专属避坑(必看)

1. 推荐用4方向寻路(allowDiagonal=false),8方向易穿窄缝障碍物

2. Tiled障碍物瓦片要连续封闭,否则A*会从缝隙穿过去

3. 移动速度建议120-180,太快会导致角色卡顿在路径点

4. 无寻路路径时,脚本直接返回空,不会报错

五、 AI寻路单独调用示例(角色脚本内)

// 角色脚本中调用A*寻路到指定瓦片(10,6)
import { _decorator, Component, Node, Vec2 } from 'cc';
import { TiledMapFullTool } from './TiledMapFullTool';
const { ccclass, property } = _decorator;

@ccclass('RoleController')
export class RoleController extends Component {
    @property(TiledMapFullTool) mapTool: TiledMapFullTool = null;

    start() {
        // 直接调用A*寻路移动
        this.mapTool.moveRoleByAStar(new Vec2(10, 6));
    }
}

更多推荐