import { _decorator, Component, Node, Sorting2D, Tween, UITransform, Vec3, sp, tween } from 'cc'; import { createMembershipSet, membershipAdd, membershipFromArray, membershipDedupeArray, type MembershipSet, type ReadonlyMembershipSet } from '../../../../mx/tools/MembershipSet'; import { MapGrid } from './MapGrid'; import { MapAreaUnlock } from './MapAreaUnlock'; import { MapObstacle, ObstacleType } from './MapObstacle'; import { GBundle, GPath } from '../../../game/ConfigRes'; import { GEvent } from 'db://assets/mx/module/event/GEvent'; import { LOCAL_ENEMY_SPAWN_PORT, LOCAL_OWN_SPAWN_PORT, pickParatroopLandingGrids, pickRandomReachableWalkGrids, resolveAttackTargetGrid, resolveAttackTargetNode, } from '../MapPathQueries'; import type { Cell } from 'db://assets/mx/module/battle/battlefield/Cell'; import type { BattleSession } from 'db://assets/mx/module/battle'; import { mapGridToLogicCell } from '../BattleMapSpec'; import { isRowInRange, MapRowRange } from '../../mechanism/MechanismSpawnArea'; import { collectAndValidateMapCampPoints } from '../campAreaMapValidation'; const { ccclass, property } = _decorator; const SPAWN_PORTAL_NODE = '出怪口'; /** 我方出怪口(spawnPoint_2) */ const SPAWN_PORTAL_ANIM_ALLY = '蓝方'; /** 敌方出怪口(spawnPoint_1) */ const SPAWN_PORTAL_ANIM_ENEMY = '红方'; /** 出怪口出现/隐藏透明度渐变时长 */ const SPAWN_PORTAL_FADE_SEC = 0.12; /** Cocos 地图视图:格子/出怪口/攻击点/推荐点(寻路由 MX Pathfinder 负责) */ @ccclass('MapManager') export class CocosMapView extends Component { @property({ displayName: '地图行数' }) mapRows: number = 10; @property({ displayName: '地图列数' }) mapCols: number = 8; @property({ displayName: '格子大小' }) mapGridSize: number = 93; /**地图格子父节点 */ mapGridParent: Node = null; /** 按区域依次解锁 */ areaUnlock = new MapAreaUnlock(); /** 英雄表现层(格子节点的后一个兄弟,不挂在格子下) */ heroParent: Node = null; /**地图目标父节点 */ mapTargetParent: Node = null; /**出怪口父节点 */ spawnPointParent: Node = null; /**推荐点父节点 */ guidePosParent: Node = null; /**地图格子数组 */ mapGrids: MapGrid[] = []; /**根据行列号缓存地图格子 */ mapGridCache: Map = new Map(); //所有出怪口的格子 allSpawnPointGrids: Map = new Map(); //所有怪物攻击点的格子 allAttackPointGrids: Map = new Map(); /** 攻击点节点列表(与拓扑 attackTargets 导入顺序一致) */ attackPointNodes: Node[] = []; /** 出怪口 port(1-based) → 落点格阵营区域(initMapGrids 校验通过后写入) */ spawnPortCampArea = new Map(); /**所有推荐最佳障碍物摆放点 */ allBestObstaclePlacementGrids: MapGrid[] = []; /**所有免费推荐点 */ allFreeRecommendGrids: MapGrid[] = []; /**所有静态障碍物格子 */ allStaticObstacleGrids: MapGrid[] = []; /** initMapGrids 是否已完成(出怪口/攻击点可用) */ private _gridsInitialized = false; get gridsInitialized(): boolean { return this._gridsInitialized; } /** 通知:地图可走状态发生变化(由 MapPlaceValidator 同步至 MX) */ public notifyMapWalkStateChanged() { /* 寻路/可达性由 BattleSession.pathfinder 维护 */ } protected onLoad(): void { this.mapGridParent = this.node.getChildByName('格子'); this.mapTargetParent = this.node.getChildByName('目标点'); this.spawnPointParent = this.node.getChildByName('出怪口'); this.guidePosParent = new Node("guidePosParent"); this.guidePosParent.parent = this.node; this.guidePosParent.setPosition(0, 0, 0); this.guidePosParent.active = false; this.mapGrids = this.mapGridParent?.children?.map((node) => node.getOrAddComponent(MapGrid)) ?? []; this.ensureHeroParent(); } /** 格子后的英雄挂点,供 heroTemplate 使用(须始终压在格子层之上) */ ensureHeroParent(): Node { if (this.heroParent?.isValid) { this._ensureHeroParentAboveGrids(this.heroParent); return this.heroParent; } let n = this.node.getChildByName('英雄'); if (!n?.isValid) { n = new Node('英雄'); n.layer = this.mapGridParent?.layer ?? this.node.layer; const ut = n.addComponent(UITransform); const src = this.mapGridParent?.getComponent(UITransform); if (src) { ut.setContentSize(src.contentSize); ut.setAnchorPoint(src.anchorPoint); } n.setPosition(0, 0, 0); this.node.addChild(n); } this._ensureHeroParentAboveGrids(n); this.heroParent = n; return n; } /** 预制体里「英雄」若排在「格子」前,字块会盖住 HeroActor,邻格放字时像「单位消失」 */ private _ensureHeroParentAboveGrids(heroParent: Node): void { if (!heroParent?.isValid || !this.mapGridParent?.isValid) return; if (heroParent.parent !== this.node) { heroParent.parent = this.node; } const after = this.mapGridParent.getSiblingIndex() + 1; if (heroParent.getSiblingIndex() !== after) { heroParent.setSiblingIndex(after); } } /**初始化地图格子 */ initMapGrids() { this._gridsInitialized = false; this.allSpawnPointGrids.clear(); this.allAttackPointGrids.clear(); this.attackPointNodes = []; this.spawnPortCampArea.clear(); this.mapGridCache.clear(); this.allBestObstaclePlacementGrids = []; this.allStaticObstacleGrids = []; this.allFreeRecommendGrids = []; for (let row = 0; row < this.mapRows; row++) { for (let col = 0; col < this.mapCols; col++) { let index = this.getMapGridIndex(row, col); let mg = this.getMapGridByIndex(index); if (!mg) continue; mg.Row = row; mg.Col = col; mg.Index = index; this.mapGridCache.set(`${row}-${col}`, mg); if (mg.isBestPlace) { this.allBestObstaclePlacementGrids.push(mg); } if (mg.isFreePlace) { this.allFreeRecommendGrids.push(mg); } if (mg.getComponent(MapObstacle)?.type == ObstacleType.静态障碍物) { this.allStaticObstacleGrids.push(mg); } } } //获取所有出怪口和怪物攻击点的格子 this.spawnPointParent?.children?.forEach((n) => { let g = this.getMapGridByWorldPos(n.worldPosition); if (g) this.allSpawnPointGrids.set(g, n); }); this.mapTargetParent?.children?.forEach((n) => { n.children.map(c => { let s = c.getOrAddComponent(Sorting2D); s.sortingLayer = 3; s.sortingOrder = 2; }); const g = this.findNearestWalkableGridForTarget(n.worldPosition); if (g) { this.allAttackPointGrids.set(g, n); this.attackPointNodes.push(n); } }); //需要修改位置的出怪口 let needMoveSpawnPointGrids: MapGrid[] = []; //需要修改位置的怪物攻击点 let needMoveAttackPointGrids: MapGrid[] = []; for (let row = 0; row < this.mapRows; row++) { for (let col = 0; col < this.mapCols; col++) { let index = this.getMapGridIndex(row, col); let mg = this.getMapGridByIndex(index); //宝箱副本最后两列预留宝箱怪区域,禁止通行、放置、铲除 // 旧宝箱模式列锁已移除(塔防重构) if (false) { mg.isCanWalk = false; mg.isCanPutText = false; mg.isCanRemove = false; if (this.allSpawnPointGrids.has(mg)) { needMoveSpawnPointGrids.push(mg); } if (this.allAttackPointGrids.has(mg)) { needMoveAttackPointGrids.push(mg); } } } } //修改怪物出生位置 for (let i = 0; i < this.mapGrids.length; i++) { if (i < 6) continue; if (needMoveSpawnPointGrids.length == 0) break; let mapGrid = this.mapGrids[i]; if (mapGrid.isCanWalk) { let g = needMoveSpawnPointGrids.shift(); if (g) { let node = this.allSpawnPointGrids.get(g)!; node.worldPosition = mapGrid.node.worldPosition.clone(); this.allSpawnPointGrids.set(mapGrid, node); } } } //修改怪物攻击位置 if (this.allAttackPointGrids.size == needMoveAttackPointGrids.length) { for (let i = 0; i < needMoveAttackPointGrids.length; i++) { let mapGrid = needMoveAttackPointGrids[i]; let n = this.allAttackPointGrids.get(mapGrid); let mPg = this.mapGrids[188]; n.worldPosition = mPg.node.worldPosition.clone(); this.allAttackPointGrids.set(mPg, n); n.destroyAllChildren(); } } else { for (let i = 0; i < needMoveAttackPointGrids.length; i++) { let mapGrid = needMoveAttackPointGrids[i]; let n = this.allAttackPointGrids.get(mapGrid); n.destroy(); } } // 地图可走性/障碍状态初始化完成,通知寻路/可达性预计算失效 this.notifyMapWalkStateChanged(); const isPerimeter = !!gg.game.CurentBattle?.session?.topology?.enablePatrolLoop; const campValidation = collectAndValidateMapCampPoints(this, { requireAttackPerCampArea: !isPerimeter, }); if (campValidation.errors.length > 0) { const mapName = this.node?.name ?? 'MapManager'; for (const err of campValidation.errors) { console.error(String(err)); } console.error(`[MapManager] ${mapName}: 地图阵营/出怪口/攻击点校验失败,共 ${campValidation.errors.length} 项`); this._gridsInitialized = false; return; } this.spawnPortCampArea.clear(); for (const b of campValidation.spawnBindings) { this.spawnPortCampArea.set(b.port, b.campAreaId); } this.validateMapPoints(); this.areaUnlock.sync(this.mapGrids); this._gridsInitialized = true; this.hideRecommendPoints(); GEvent.Ins.emit(GEvent.MapGridsReady, this); } /** * 校验地图节点配置(制作规范,非运行时容错) * - 出怪口须落在可走格上 * - 普通模式须有「目标点」 * - 循环圈地图允许无目标点 */ validateMapPoints(): void { const mapName = this.node?.name ?? 'MapManager'; if (!this.spawnPointParent?.children?.length) { console.error(`[MapManager] ${mapName}: 缺少「出怪口」子节点`); } this.spawnPointParent?.children.forEach((n) => { const grid = this.getMapGridByWorldPos(n.worldPosition); if (!grid?.isCanWalk) { console.error( `[MapManager] ${mapName}: 出怪口 ${n.name} 不在可走格上`, n.worldPosition, ); } }); const isPerimeter = !!gg.game.CurentBattle?.session?.topology?.enablePatrolLoop; if (!this.mapTargetParent?.children?.length) { if (isPerimeter) { // console.log(`[MapManager] ${mapName}: 循环圈模式无「目标点」`); return; } console.error(`[MapManager] ${mapName}: 缺少「目标点」子节点`); return; } this.mapTargetParent.children.forEach((n) => { const grid = this.findNearestWalkableGridForTarget(n.worldPosition); if (!grid) { console.error( `[MapManager] ${mapName}: 攻击点 ${n.name} 无法映射到可走格`, n.worldPosition, ); } }); if (this.allAttackPointGrids.size === 0) { console.error(`[MapManager] ${mapName}: 没有有效的攻击点格子`); } } /** @deprecated 推荐点引导已移除 */ createRecommendPoints(_grid: MapGrid): void { } /** @deprecated 推荐点引导已移除 */ updateRecommendPoints(_count: number = 1): void { } /** @deprecated 推荐点引导已移除 */ showRecommendPoints(): void { } /** @deprecated 推荐点引导已移除 */ hideRecommendPoints(): void { } /**根据行列号获取地图索引 */ getMapGridIndex(row: number, col: number): number { return row * this.mapCols + col; } /**根据行列号获取地图格子 */ getMapGrid(row: number, col: number): MapGrid { let grid = this.mapGridCache.get(`${row}-${col}`); return grid; } /**根据地图索引获取地图格子 */ getMapGridByIndex(index: number): MapGrid { if (!this.mapGrids?.length) return null; if (index < 0 || index >= this.mapRows * this.mapCols) return null; return this.mapGrids[index]; } /**根据世界坐标获取mapGrid */ getMapGridByWorldPos(worldPos: Vec3): MapGrid { if (!worldPos) return null; const grids = this.mapGrids; if (!Array.isArray(grids) || grids.length === 0) return null; const p1 = worldPos; const offset = this.mapGridSize * 0.5; for (let i = 0, len = grids.length; i < len; i++) { const mapGrid = grids[i]; if (!mapGrid || !mapGrid.node || !mapGrid.node.isValid) continue; const p2 = mapGrid.node.worldPosition; let minX = p2.x - offset; let maxX = p2.x + offset; let minY = p2.y - offset; let maxY = p2.y + offset; if (p1.x >= minX && p1.x <= maxX && p1.y >= minY && p1.y <= maxY) { return mapGrid; } } return null; } /** * 攻击点/城墙节点常在不可走格上,寻路终点应取该格八邻域内最近的可走格(无全图回退)。 */ public findNearestWalkableGridForTarget(worldPos: Vec3): MapGrid | null { const direct = this.getMapGridByWorldPos(worldPos); if (!direct) return null; if (direct.isCanWalk) return direct; const queue: MapGrid[] = [direct]; const visited = membershipFromArray([direct.Index]); const dirs: [number, number][] = [ [-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [-1, 1], [1, -1], [1, 1], ]; let head = 0; while (head < queue.length) { const cur = queue[head++]; if (cur.isCanWalk) return cur; for (let i = 0; i < dirs.length; i++) { const [dr, dc] = dirs[i]; const nb = this.getMapGrid(cur.Row + dr, cur.Col + dc); if (!nb || visited.has(nb.Index)) continue; membershipAdd(visited, nb.Index); queue.push(nb); } } return null; } /**获取所有可通行的地图格子 */ getAllCanWalkMapGrids(): MapGrid[] { let canWalkGrids: MapGrid[] = []; for (let i = 0; i < this.mapGrids.length; i++) { let mapGrid = this.mapGrids[i]; if (mapGrid.isCanWalk) { canWalkGrids.push(mapGrid); } } return canWalkGrids; } /** 获取所有可被开挖的地图格子(运行时 isCanBeDig) */ getAllCanBeDigMapGrids(): MapGrid[] { const grids: MapGrid[] = []; for (let i = 0; i < this.mapGrids.length; i++) { const mapGrid = this.mapGrids[i]; if (mapGrid.isCanBeDig && mapGrid.unlockAreaId <= 0) { grids.push(mapGrid); } } return grids; } /** 随机获取 count 个不同的可被开挖地图格子 */ getRandomCanBeDigMapGrids(count: number): MapGrid[] { const candidates = this.getAllCanBeDigMapGrids(); const shuffled = candidates.slice(); for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled.slice(0, count); } /**获取所有可放置障碍物的地图格子 */ getAllCanPutObstacleMapGrids(): MapGrid[] { let canPutObstacleGrids: MapGrid[] = []; for (let i = 0; i < this.mapGrids.length; i++) { let mapGrid = this.mapGrids[i]; if (mapGrid.isCanPutText && mapGrid.node.active) { canPutObstacleGrids.push(mapGrid); } } return canPutObstacleGrids; } /**获取所有可铲除的地图格子 */ getAllCanRemoveMapGrids(): MapGrid[] { let canRemoveGrids: MapGrid[] = []; for (let i = 0; i < this.mapGrids.length; i++) { let mapGrid = this.mapGrids[i]; if (mapGrid.isCanRemove) { canRemoveGrids.push(mapGrid); } } return canRemoveGrids; } /** 随机获取 count 个可达攻击点的可行走格(MapGrid.isCanWalk,空投等) */ getRandomReachableWalkMapGrids(count: number, session: BattleSession, rowRange?: MapRowRange): MapGrid[] { return pickRandomReachableWalkGrids(this, session, count, rowRange); } /** 空投落点:可达优先,不足时用当前可行走格补足 */ getParatroopLandingGrids(count: number, session: BattleSession, rowRange?: MapRowRange): MapGrid[] { return pickParatroopLandingGrids(this, session, count, rowRange); } /**随机获取 count 个不同的可行走地图格子 */ getRandomCanWalkMapGrids(count: number, rowRange?: MapRowRange): MapGrid[] { let canWalkGrids = this.getAllCanWalkMapGrids(); if (rowRange) { canWalkGrids = canWalkGrids.filter(g => isRowInRange(g.Row, rowRange)); } const shuffled = canWalkGrids.slice(0); for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled.slice(0, count); } /**随机获取count个不同的可放置的地图格子 */ getRandomCanPutObstacleMapGrids(count: number, rowRange?: MapRowRange): MapGrid[] { let canPutObstacleGrids = this.getAllCanPutObstacleMapGrids(); if (rowRange) { canPutObstacleGrids = canPutObstacleGrids.filter(g => isRowInRange(g.Row, rowRange)); } let shuffled = canPutObstacleGrids.slice(0); for (let i = shuffled.length - 1; i > 0; i--) { let j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled.slice(0, count); } /** 根据世界坐标获取路径最短的终点格子(委托 MX Pathfinder) */ getPathShortestTargetGrid(startPos: Vec3, spawnPort?: number): MapGrid | null { const session = gg.game.CurentBattle?.session; return resolveAttackTargetGrid(this, startPos, session, spawnPort); } /** 根据世界坐标获取路径最短的目标城墙节点 */ getPathShortestTarget(startPos: Vec3, spawnPort?: number): Node | null { const session = gg.game.CurentBattle?.session; return resolveAttackTargetNode(this, startPos, session, spawnPort); } /** 拓扑攻击点索引 → 城墙节点(与 importTopology attackTargets 同序) */ getAttackPointNode(index: number): Node | null { const node = this.attackPointNodes[index]; return node?.isValid ? node : null; } /**随机雷击一个障碍物 */ randomThunderObstacle() { let grids = this.mapGrids.filter(grid => grid.isCanBeThunder); if (grids.length == 0) return null; let grid = grids[Math.floor(Math.random() * grids.length)]; grid.getComponent(MapObstacle).thunderAtk(); return grid; } /**找出 count 个最佳摆放点(type3:模拟放置后怪物绕路最远) */ getBestObstaclePlacementGrids(count: number): MapGrid[] { if (count <= 0 || this.allBestObstaclePlacementGrids.length === 0) return []; const canPlaceGrids = this.allBestObstaclePlacementGrids.filter( (grid) => grid.isCanWalk && grid.isCanPutGrid && !grid.ShowGuidePos && this.canPlaceType3At(grid), ); if (canPlaceGrids.length === 0) return []; const session = gg.game.CurentBattle?.session; if (!session) { return canPlaceGrids.slice(0, count); } const scored = canPlaceGrids.map((grid) => ({ grid, detour: this._measureTotalPathDistance(session, grid), })); scored.sort((a, b) => b.detour - a.detour); return scored.slice(0, count).map((item) => item.grid); } /** * 所有真实出怪口 × 攻击点的寻路距离之和。 * @param blocked 模拟在该格放置 type3 后统计 */ private _measureTotalPathDistance(session: BattleSession, blocked?: MapGrid): number { const blockCell = blocked ? mapGridToLogicCell(blocked.Row, blocked.Col, session.grid.rows, session.grid.cols) : null; return this._withSimulatedWalkBlock(session, blockCell, () => this._sumSpawnToTargetPathDistances(session), ); } /** 临时阻塞一格并重算寻路场,统计后立即还原(不触发 MovementSystem) */ private _withSimulatedWalkBlock( session: BattleSession, blockCell: Cell | null, measure: () => number, ): number { let restored = false; if (blockCell && session.grid.inBounds(blockCell) && session.grid.isWalkable(blockCell)) { session.grid.setWalkable(blockCell, false); session.pathfinder.invalidate(); session.pathfinder.rebuild(session.topology); restored = true; } try { return measure(); } finally { if (restored && blockCell) { session.grid.setWalkable(blockCell, true); session.pathfinder.invalidate(); session.pathfinder.rebuild(session.topology); } } } /** 全部出怪口到全部攻击点的 8 向路径世界距离累加 */ private _sumSpawnToTargetPathDistances(session: BattleSession): number { const spawns = session.topology.getAllSpawnCells(); const targets = session.topology.attackTargets; if (!spawns.length || !targets.length) return 0; let total = 0; const pf = session.pathfinder; for (const spawn of spawns) { for (const target of targets) { const path = pf.findPath(spawn, target, true); total += this._pathWorldLength(path); } } return total; } private _pathWorldLength(path: readonly Cell[]): number { if (path.length < 2) return 0; let total = 0; for (let k = 0; k < path.length - 1; k++) { const g0 = this.getMapGrid(path[k].row, path[k].col); const g1 = this.getMapGrid(path[k + 1].row, path[k + 1].col); if (!g0?.node?.isValid || !g1?.node?.isValid) continue; total += Vec3.distance(g0.node.worldPosition, g1.node.worldPosition); } return total; } /** 地图是否仍有可用的预设最佳摆放点 */ hasBestPlacementTargets(): boolean { return this.allBestObstaclePlacementGrids.some( (grid) => grid.isCanWalk && grid.isCanPutGrid, ); } /** 假设在该格放置 type3 扩展后路径是否仍通 */ canPlaceType3At(grid: MapGrid): boolean { if (!grid?.isCanWalk || !grid.isCanPutGrid) return false; const session = gg.game.CurentBattle?.session; if (!session) return false; const cell = { row: grid.Row, col: grid.Col }; const isPerimeter = !!gg.game.CurentBattle?.session?.topology?.enablePatrolLoop; if (isPerimeter || session.topology.enablePatrolLoop) { const loop = session.topology.patrolLoop; if (!loop.length) return false; for (let i = 0; i < loop.length; i++) { if (loop[i].row === cell.row && loop[i].col === cell.col) return false; } return true; } if (!session.topology.attackTargets.length) return false; try { return session.pathfinder.checkMultiSourceReachability( session.topology.getAllSpawnCells(), session.topology.attackTargets, cell, false, ); } catch { return false; } } /** 出怪口:我方播蓝方、敌方播红方,再渐显 → 保持 → 渐隐 */ playSpawnPointEffect(spawnPort: number, durationSec: number): void { const spawnNode = this.spawnPointParent?.getChildByName(`spawnPoint_${spawnPort}`); if (!spawnNode?.isValid) { console.error('[MapManager] spawn point missing for portal effect', spawnPort); return; } let portal = spawnNode.getChildByName(SPAWN_PORTAL_NODE); if (!portal?.isValid) { portal = gg.res.getNode(GPath.ObstaclePrefab(''), SPAWN_PORTAL_NODE, GBundle.BattleMap); if (!portal?.isValid) { console.error('[MapManager] 出怪口 prefab not loaded'); return; } portal.name = SPAWN_PORTAL_NODE; portal.setParent(spawnNode); portal.setPosition(0, 0, 0); } const skeleton = portal.getComponent(sp.Skeleton); if (!skeleton) { console.error('[MapManager] 出怪口 missing sp.Skeleton'); return; } const anim = spawnPort === LOCAL_OWN_SPAWN_PORT ? SPAWN_PORTAL_ANIM_ALLY : spawnPort === LOCAL_ENEMY_SPAWN_PORT ? SPAWN_PORTAL_ANIM_ENEMY : ''; if (!anim || !skeleton.findAnimation(anim)) { console.warn('[MapManager] 出怪口缺少阵营动画', spawnPort, anim); return; } const dur = Math.max(durationSec, SPAWN_PORTAL_FADE_SEC * 2); const fade = Math.min(SPAWN_PORTAL_FADE_SEC, dur * 0.25); const hold = Math.max(0, dur - fade * 2); Tween.stopAllByTarget(portal); skeleton.setCompleteListener(null); portal.active = true; portal.opacity = 0; skeleton.setAnimation(0, anim, true); tween(portal) .to(fade, { opacity: 255 }) .delay(hold) .to(fade, { opacity: 0 }) .call(() => { if (!portal.isValid) return; skeleton.clearTracks(); portal.active = false; }) .start(); } /**获取所有可移除的障碍物 */ getAllCanRemoveObstacleGrids(): MapGrid[] { return this.mapGrids.filter(grid => grid.isCanRemove); } } /** @deprecated 请使用 CocosMapView */ export { CocosMapView as MapManager };