Cocos3.8 Tiled地图六合一脚本(含AI寻路绕障,续前五合一,新增AI基础寻路部分)
加载+校验+坐标互转+障碍物检测+角色避障移动+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));
}
}
更多推荐
所有评论(0)