import { _decorator, Component, Node, UITransform, Vec3, Layers, Sprite } from 'cc'; import { cellToWorldCenter, parseTileNodeName } from '../core/GridCoords'; import { CommonDefine, MoverRole, GameState } from '../core/Define'; import { GameManager } from '../manager/GameManager'; import { getTilePivot } from '../visual/TilePivots'; import { getTileDrawSize, resolveTilePixelSize } from '../visual/TileSizes'; import { LevelConfig } from './LevelTypes'; import { getLevelRuntimeContext } from './LevelRuntimeContext'; import { worldToMoverCellForRole, entityWorldPositionForRole } from './EntitySpawnPlacement'; import { Movement } from '../gameplay/Movement'; import { DEFAULT_PLAYER_ANCHOR_Y, DEFAULT_VEHICLE_ANCHOR_Y, getEntityDisplaySizes } from '../visual/EntityDisplayRefs'; import { cellHasTile } from './EntitySpawnPlacement'; import { compareUnityEntityToEntity, compareUnityEntityToTile, compareUnityTileToTile, compareIsoDrawOrder, UNITY_SORTING_ORDER, type UnitySortableEntity, type UnitySortableTile, } from './UnityDrawSort'; const { ccclass } = _decorator; const UI_LAYER = Layers.Enum.UI_2D; /** * Cocos 用 siblingIndex 模拟 Unity SortingOrder + CustomAxis 深度。 * 逻辑见 UnityDrawSort.ts(对齐 GraphicsSettings + TilemapRenderer Individual)。 */ export const DRAW_SORT_Y_SCALE = 100; type DrawKind = 'walkable' | 'wall' | 'actor' | 'pickable' | 'scenery'; interface TileDrawEntry { node: Node; x: number; y: number; kind: DrawKind; tileName: string; } interface EntityDrawEntry { node: Node; x: number; y: number; kind: DrawKind; } type DrawEntry = TileDrawEntry | EntityDrawEntry; function safeNodeName(node: Node | null | undefined): string { return (node?.isValid ? node.name : '') || ''; } function isPlayerEntityName(name: string | undefined | null): boolean { if (!name) return false; return name === 'Player' || /^Player[AB]\d$/.test(name); } function isVehicleEntityName(name: string | undefined | null): boolean { if (!name) return false; return name === 'Vehicle' || /^Vehicle[AB]\d$/.test(name); } function isCoinEntityName(name: string | undefined | null): boolean { if (!name) return false; return name === 'Prop' || name.startsWith('Prop_'); } function isActorEntity(node: Node): boolean { return isPlayerEntityName(safeNodeName(node)) || isVehicleEntityName(safeNodeName(node)); } /** 可拾取金币 / 道具(PropController) */ function isPickableProp(node: Node): boolean { if (!node?.isValid) return false; if (node.getComponent('PropController')) return true; return isCoinEntityName(safeNodeName(node)); } function isEntityDrawNode(node: Node): boolean { const n = safeNodeName(node); if (!n) return false; return isActorEntity(node) || isPickableProp(node) || n === 'PropDecor' || n.startsWith('PropDecor_'); } function isWalkablePathTile(tileName: string): boolean { return tileName === CommonDefine.BlockBase || tileName === CommonDefine.BlockJump; } function isWallTileName(tileName: string): boolean { return tileName === CommonDefine.BlockWall; } function resolveTileName( parsed: { layer: 'ground' | 'border'; x: number; y: number }, config?: LevelConfig, ): string { const key = `${parsed.x},${parsed.y}`; if (parsed.layer === 'border') { const v = config?.border?.[key]; if (v === true || v === undefined) return CommonDefine.BlockWall; if (typeof v === 'string') return v; return CommonDefine.BlockWall; } return config?.ground?.[key] ?? CommonDefine.BlockBase; } function classifyTileKind( parsed: { layer: 'ground' | 'border'; x: number; y: number }, config?: LevelConfig, ): DrawKind { const name = resolveTileName(parsed, config); if (isWallTileName(name)) return 'wall'; if (isWalkablePathTile(name)) return 'walkable'; return 'scenery'; } function classifyEntityKind(node: Node): DrawKind { if (isPickableProp(node)) return 'pickable'; if (isActorEntity(node)) return 'actor'; return 'scenery'; } export interface EntityDrawOrderOptions { /** @deprecated 保留字段避免旧调用报错 */ preferVehicleOverPlayer?: boolean; } function unityEntitySortingOrder(node: Node): number { const n = safeNodeName(node); if (isPlayerEntityName(n)) return UNITY_SORTING_ORDER.player; if (isVehicleEntityName(n)) return UNITY_SORTING_ORDER.vehicle; if (isPickableProp(node)) { const prop = node.getComponent('PropController') as { getSpawnCell?: () => Vec3 | null } | null; const cell = prop?.getSpawnCell?.(); const config = resolveLevelConfig(); if (cell && !cellHasTile(cell.x, cell.y, config)) { return UNITY_SORTING_ORDER.nProp; } return UNITY_SORTING_ORDER.prop; } if (n === 'PropDecor' || n.startsWith('PropDecor_')) return UNITY_SORTING_ORDER.prop; return UNITY_SORTING_ORDER.nProp; } function toUnitySortableTile(tile: TileDrawEntry): UnitySortableTile { const centerY = tile.node.position.y; let depthY = getNodeBottomY(tile.node); if (isWallTileName(tile.tileName)) { depthY = getWallSortFrontY(tile); } else if (isWalkablePathTile(tile.tileName)) { depthY = Math.max(depthY, centerY); } else { depthY = centerY; } return { cellX: tile.x, cellY: tile.y, tileName: tile.tileName, centerY, depthY, }; } function toUnitySortableEntity(entity: EntityDrawEntry): UnitySortableEntity { const node = entity.node; const cell = entity.kind === 'actor' ? getActorWallSampleCell(entity, node) : { x: entity.x, y: entity.y }; const depthY = entity.kind === 'actor' ? getEntitySortBottomY(node) : getNodeBottomY(node); return { sortingOrder: unityEntitySortingOrder(node), cellX: cell.x, cellY: cell.y, centerY: node.position.y, depthY, isPlayer: isPlayerEntityName(safeNodeName(node)), isPickable: entity.kind === 'pickable', }; } /** 等距深度:返回值 > 0 表示 a 应排在 b 之后(更靠前) */ export { compareIsoDrawOrder }; function buildSortedDrawOrder(tiles: TileDrawEntry[], entities: EntityDrawEntry[]): DrawEntry[] { const all: DrawEntry[] = [...tiles, ...entities]; return all.sort((a, b) => { const aEntity = isEntityDrawNode(a.node); const bEntity = isEntityDrawNode(b.node); if (!aEntity && !bEntity) { return compareUnityTileToTile( toUnitySortableTile(a as TileDrawEntry), toUnitySortableTile(b as TileDrawEntry), ); } if (aEntity && bEntity) { return compareUnityEntityToEntity( toUnitySortableEntity(a as EntityDrawEntry), toUnitySortableEntity(b as EntityDrawEntry), ); } const entity = (aEntity ? a : b) as EntityDrawEntry; const tile = (aEntity ? b : a) as TileDrawEntry; const cmp = compareUnityEntityToTile( toUnitySortableEntity(entity), toUnitySortableTile(tile), ); return aEntity ? cmp : -cmp; }); } /** 精灵顶边 Y(Tiles 本地) */ function getNodeTopY(node: Node): number { if (!node?.isValid) return 0; const ui = resolveSortTransform(node); const pos = node.position; if (!ui || ui.contentSize.height <= 0) return pos.y; const h = ui.contentSize.height * Math.abs(node.scale.y); return pos.y + (1 - ui.anchorPoint.y) * h; } /** 墙砖立面顶边 Y(WallBlock pivot 偏低,取格心与顶边较大值) */ function getWallSortFrontY(tile: TileDrawEntry): number { const centerY = cellToWorldCenter(new Vec3(tile.x, tile.y, 0)).y; return Math.max(centerY, getNodeTopY(tile.node)); } function resolveActorSortAnchorY(node: Node): number { return isVehicleEntityName(safeNodeName(node)) ? DEFAULT_VEHICLE_ANCHOR_Y : DEFAULT_PLAYER_ANCHOR_Y; } function resolveActorSortDisplayHeight(node: Node, role: MoverRole, theme?: string): number { const ui = resolveSortTransform(node); if (ui && ui.contentSize.height > 0) { return ui.contentSize.height * Math.abs(node.scale.y); } const sizes = getEntityDisplaySizes(theme); return role === 'vehicle' ? sizes.vehicle.height : sizes.player.height; } /** 逻辑格站立点 + 固定参考锚点 → 排序底边 Y */ function getActorStandBottomY( cell: Vec3, node: Node, role: MoverRole, config?: LevelConfig, theme?: string, ): number { const stand = entityWorldPositionForRole(cell, config, theme, role); const h = resolveActorSortDisplayHeight(node, role, theme); return stand.y - resolveActorSortAnchorY(node) * h; } /** 移动中反推逻辑格 */ function getEntitySortCell(node: Node): { x: number; y: number } { const gm = GameManager.instance; if (!gm) return { x: 0, y: 0 }; const config = resolveLevelConfig(); const theme = config?.theme ?? gm.uiStyle; const n = safeNodeName(node); let role: MoverRole = 'player'; if (isVehicleEntityName(n)) role = 'vehicle'; const cell = worldToMoverCellForRole(node.position, config, theme, role); return { x: cell.x, y: cell.y }; } /** 关卡内所有砖块/实体并入 Tiles(避免 Ground/Border 在 BFS 中整块早于 Tiles 绘制) */ function pullDrawablesIntoTiles(levelRoot: Node, tilesRoot: Node) { const worldPos = new Vec3(); const pending: Node[] = []; const walk = (parent: Node) => { for (const ch of parent.children) { if (!ch?.isValid || !ch.active) continue; if (parseTileNodeName(ch.name) || isEntityDrawNode(ch)) { pending.push(ch); continue; } if (ch.name === 'Ground' || ch.name === 'Border' || ch.name === 'Tiles') { walk(ch); } } }; walk(levelRoot); for (const node of pending) { if (node.parent === tilesRoot) continue; node.getWorldPosition(worldPos); node.parent = tilesRoot; node.setWorldPosition(worldPos); } } function dedupeLayerTileDuplicates(levelRoot: Node, tilesRoot: Node) { for (const layerName of ['Ground', 'Border'] as const) { const layer = levelRoot.getChildByName(layerName); if (!layer) continue; for (const ch of [...layer.children]) { if (!ch?.isValid) continue; if (!parseTileNodeName(ch.name)) continue; const canon = tilesRoot.getChildByName(ch.name); if (canon?.isValid && canon !== ch) { ch.destroy(); } } } } function getActorWallSampleCell(entity: EntityDrawEntry, node: Node): { x: number; y: number } { const mov = node.getComponent(Movement); if (!mov) return { x: entity.x, y: entity.y }; const committed = mov.getCommittedCell(); if (!mov.isMoving()) { if (committed) return { x: committed.x, y: committed.y }; const spawn = mov.getSpawnCell(); if (spawn) return { x: spawn.x, y: spawn.y }; return { x: entity.x, y: entity.y }; } if (!committed) return getEntitySortCell(node); const landing = mov.getLandingCell(); if (!landing) return { x: committed.x, y: committed.y }; if (mov.getMoveStepProgress() >= 0.5) return { x: landing.x, y: landing.y }; return { x: committed.x, y: committed.y }; } function resolveSortTransform(node: Node): UITransform | null { const rootUi = node.getComponent(UITransform); if (rootUi && rootUi.contentSize.height > 0) return rootUi; for (const ch of node.children) { if (!ch?.isValid) continue; const ui = ch.getComponent(UITransform); if (ui && ui.contentSize.height > 0) return ui; } return rootUi; } /** 足底中心 Y(Tiles 本地);排序依据底边而非锚点/中心 */ export function getNodeBottomY(node: Node): number { if (!node?.isValid) return 0; const ui = resolveSortTransform(node); const pos = node.position; if (!ui || ui.contentSize.height <= 0) return pos.y; const h = ui.contentSize.height * Math.abs(node.scale.y); return pos.y - ui.anchorPoint.y * h; } /** 画家算法 Order:round(底边Y × DRAW_SORT_Y_SCALE) */ export function getNodeSortOrder(node: Node): number { return Math.round(getNodeBottomY(node) * DRAW_SORT_Y_SCALE); } /** * 人物/载具排序底边 Y:始终按逻辑格站立点 + 固定参考锚点; * 移动中在起止格底边 Y 之间插值(忽略跳跃弧与序列帧 anchor 抖动)。 */ function getEntitySortBottomY(node: Node): number { if (!isActorEntity(node)) return getNodeBottomY(node); const mov = node.getComponent(Movement); const gm = GameManager.instance; const config = resolveLevelConfig(); const theme = config?.theme ?? gm?.uiStyle; const role: MoverRole = isVehicleEntityName(safeNodeName(node)) ? 'vehicle' : 'player'; if (mov?.isMoving()) { const from = mov.getCommittedCell(); const to = mov.getLandingCell(); if (from && to) { const t = mov.getMoveStepProgress(); const fromY = getActorStandBottomY(from, node, role, config, theme); const toY = getActorStandBottomY(to, node, role, config, theme); return fromY + (toY - fromY) * t; } } const cell = mov?.getCommittedCell() ?? mov?.getSpawnCell(); if (!cell) return getNodeBottomY(node); return getActorStandBottomY(cell, node, role, config, theme); } /** 实体所在关卡根(Ground/Border/Tiles 的父节点,不是 Tiles 容器本身) */ export function resolveLevelDrawRoot(from: Node): Node | null { let cur: Node | null = from; while (cur?.isValid) { if (cur.getChildByName('Ground') || cur.getChildByName('Border') || cur.getChildByName('Tiles')) { return cur; } if (/^Level_\d+/.test(cur.name)) return cur; cur = cur.parent; } return null; } /** 实体逻辑格(spawn / committed;实体互排序用,与砖块比较用底边 Y) */ function getEntityLogicCell(node: Node): { x: number; y: number } { const propCtrl = node.getComponent('PropController') as { getSpawnCell?: () => Vec3 | null } | null; const propCell = propCtrl?.getSpawnCell?.(); if (propCell) return { x: propCell.x, y: propCell.y }; const m = /^Prop_(-?\d+)_(-?\d+)$/.exec(safeNodeName(node)); if (m) return { x: Number(m[1]), y: Number(m[2]) }; const vehicle = node.getComponent('VehicleController') as { getPlayer?: () => { getCommittedCell?: () => Vec3 | null; getSpawnCell?: () => Vec3 | null } | null; } | null; const rider = vehicle?.getPlayer?.() ?? null; if (rider) { const riderCell = rider.getCommittedCell?.() ?? rider.getSpawnCell?.(); if (riderCell) return { x: riderCell.x, y: riderCell.y }; } const mov = node.getComponent(Movement); if (mov) { const cell = mov.getCommittedCell?.() ?? mov.getSpawnCell?.(); if (cell) return { x: cell.x, y: cell.y }; } const gm = GameManager.instance; if (gm) { const c = gm.worldToCell(node.position); return { x: c.x, y: c.y }; } return { x: 0, y: 0 }; } /** 关卡内实体(可能在 levelRoot 或 Tiles 下) */ export function forEachLevelEntityNode(levelRoot: Node, fn: (node: Node) => void) { if (!levelRoot?.isValid) return; for (const ch of levelRoot.children) { if (ch?.isValid && isEntityDrawNode(ch)) fn(ch); } const tiles = levelRoot.getChildByName('Tiles'); if (!tiles?.isValid) return; for (const ch of tiles.children) { if (ch?.isValid && isEntityDrawNode(ch)) fn(ch); } } export function findLevelChildByName(levelRoot: Node, name: string): Node | null { if (!levelRoot?.isValid) return null; const direct = levelRoot.getChildByName(name); if (direct) return direct; return levelRoot.getChildByName('Tiles')?.getChildByName(name) ?? null; } function resolveLevelConfig(): LevelConfig | undefined { return getLevelRuntimeContext()?.getCurLevel() ?? GameManager.instance?.getCurLevel?.() ?? undefined; } function collectTileEntries(levelRoot: Node, tilesRoot: Node, seen: Set): TileDrawEntry[] { const config = resolveLevelConfig(); const entries: TileDrawEntry[] = []; const pushTile = (node: Node, parsed: { layer: 'ground' | 'border'; x: number; y: number }) => { if (seen.has(node)) return; seen.add(node); const tileName = resolveTileName(parsed, config); const kind = classifyTileKind(parsed, config); entries.push({ node, x: parsed.x, y: parsed.y, kind, tileName, }); }; forEachTileChild(levelRoot, pushTile); for (const ch of tilesRoot.children) { if (!ch?.isValid || seen.has(ch)) continue; const parsed = parseTileNodeName(ch.name); if (parsed) pushTile(ch, parsed); } return entries; } function collectEntityEntries(levelRoot: Node, tilesRoot: Node, seen: Set): EntityDrawEntry[] { const entries: EntityDrawEntry[] = []; const pushEntity = (node: Node | null | undefined) => { if (!node?.isValid || seen.has(node) || !isEntityDrawNode(node)) return; seen.add(node); const cell = getEntityLogicCell(node); entries.push({ node, x: cell.x, y: cell.y, kind: classifyEntityKind(node), }); }; const walkEntities = (parent: Node) => { for (const ch of parent.children) { if (!ch?.isValid) continue; if (isEntityDrawNode(ch)) { pushEntity(ch); continue; } if (ch.name === 'Ground' || ch.name === 'Border' || ch.name === 'Tiles') { walkEntities(ch); } } }; walkEntities(levelRoot); return entries; } function applyDrawOrder(tilesRoot: Node, entries: DrawEntry[]) { const inSort = new Set(); const worldPos = new Vec3(); for (const { node } of entries) { if (!node.isValid) continue; inSort.add(node); if (isEntityDrawNode(node)) ensureEntityUILayer(node); if (node.parent !== tilesRoot) { node.getWorldPosition(worldPos); node.parent = tilesRoot; node.setWorldPosition(worldPos); } } // 未参与排序的节点保持在最底层,避免盖住角色 let head = 0; for (const ch of [...tilesRoot.children]) { if (!ch.isValid || inSort.has(ch)) continue; if (ch.getSiblingIndex() !== head) ch.setSiblingIndex(head); head++; } // 从高索引往低索引写入,避免 setSiblingIndex 互相覆盖 for (let i = entries.length - 1; i >= 0; i--) { const node = entries[i].node; if (!node.isValid) continue; const idx = head + i; if (node.getSiblingIndex() !== idx) node.setSiblingIndex(idx); const p = node.position; const z = -idx * 0.001; if (Math.abs(p.z - z) > 1e-6) node.setPosition(p.x, p.y, z); } } function finalizeLevelRootOrder(levelRoot: Node, tilesRoot: Node) { for (const name of ['Ground', 'Border'] as const) { const layer = levelRoot.getChildByName(name); if (!layer) continue; layer.active = layer.children.length > 0; if (layer.children.length === 0) { layer.setSiblingIndex(0); } } tilesRoot.setSiblingIndex(levelRoot.children.length - 1); tilesRoot.active = true; } /** * 砖块按格子深度排序,人物/载具/金币按底边 Y 插入砖块序列;移动中可重排。 */ export function sortIsoTiles(levelRoot: Node) { if (!levelRoot?.isValid) return; ensureDrawOrderUpdater(levelRoot); const tilesRoot = ensureTilesRoot(levelRoot); pullDrawablesIntoTiles(levelRoot, tilesRoot); dedupeLayerTileDuplicates(levelRoot, tilesRoot); const seen = new Set(); const tileEntries = collectTileEntries(levelRoot, tilesRoot, seen); const entityEntries = collectEntityEntries(levelRoot, tilesRoot, seen); applyDrawOrder(tilesRoot, buildSortedDrawOrder(tileEntries, entityEntries)); finalizeLevelRootOrder(levelRoot, tilesRoot); } /** @deprecated 与 sortIsoTiles 相同 */ export function refreshIsoEntityDrawOrder(levelRoot: Node) { sortIsoTiles(levelRoot); } /** @deprecated 与 sortIsoTiles 相同 */ export function refreshIsoDrawOrder(levelRoot: Node) { sortIsoTiles(levelRoot); } /** @deprecated 与 sortIsoTiles 相同 */ export function refreshIsoDrawOrderImmediate(levelRoot: Node) { sortIsoTiles(levelRoot); } export function bringEntityNodesToFront(levelRoot: Node, _opts?: EntityDrawOrderOptions) { sortIsoTiles(levelRoot); } export function ensureEntityUILayer(node: Node) { const walk = (n: Node) => { n.layer = UI_LAYER; for (const ch of n.children) walk(ch); }; walk(node); } function ensureTilesRoot(levelRoot: Node): Node { let tilesRoot = levelRoot.getChildByName('Tiles'); if (!tilesRoot) { tilesRoot = new Node('Tiles'); tilesRoot.layer = UI_LAYER; tilesRoot.parent = levelRoot; setupLayerContainer(tilesRoot); } tilesRoot.setSiblingIndex(levelRoot.children.length - 1); return tilesRoot; } const TILE_LAYER_NAMES = ['Tiles', 'Ground', 'Border'] as const; function forEachTileChild(levelRoot: Node, fn: (node: Node, parsed: { layer: 'ground' | 'border'; x: number; y: number }) => void) { for (const layerName of TILE_LAYER_NAMES) { const layer = levelRoot.getChildByName(layerName); if (!layer) continue; for (const ch of layer.children) { if (!ch?.isValid || !ch.active) continue; const parsed = parseTileNodeName(ch.name); if (!parsed) continue; fn(ch, parsed); } } } export function alignTileNode(node: Node, cellX: number, cellY: number, tileName: string, theme?: string) { const w = cellToWorldCenter(new Vec3(cellX, cellY, 0)); let ui = node.getComponent(UITransform); if (!ui) ui = node.addComponent(UITransform); const spr = node.getComponent(Sprite); const source = resolveTilePixelSize(tileName, spr?.spriteFrame ?? null, theme); const draw = getTileDrawSize(tileName, source.width, source.height, theme); const pivot = getTilePivot(tileName, theme); ui.setContentSize(draw.width, draw.height); ui.setAnchorPoint(pivot.x, pivot.y); node.setPosition(w.x, w.y, 0); node.setScale(1, 1, 1); } export function setupLayerContainer(layer: Node) { let ui = layer.getComponent(UITransform); if (!ui) ui = layer.addComponent(UITransform); ui.setAnchorPoint(0, 0); ui.setContentSize(1, 1); layer.setPosition(0, 0, 0); } function tileNameFromConfig( layer: 'ground' | 'border', key: string, config: LevelConfig, ): string { if (layer === 'ground') { return config.ground?.[key] ?? CommonDefine.BlockBase; } let v = config.border?.[key]; if (v === true || v === undefined) return CommonDefine.BlockWall; if (typeof v === 'string') return v; return CommonDefine.BlockWall; } export function layoutLevelTiles(levelRoot: Node, config: LevelConfig) { if (!levelRoot?.isValid) return; const theme = config.theme; const ground = levelRoot.getChildByName('Ground'); const border = levelRoot.getChildByName('Border'); if (ground) { setupLayerContainer(ground); for (const ch of ground.children) { const parsed = parseTileNodeName(ch.name); if (!parsed || parsed.layer !== 'ground') continue; const key = `${parsed.x},${parsed.y}`; alignTileNode(ch, parsed.x, parsed.y, tileNameFromConfig('ground', key, config), theme); } } if (border) { setupLayerContainer(border); for (const ch of border.children) { const parsed = parseTileNodeName(ch.name); if (!parsed || parsed.layer !== 'border') continue; const key = `${parsed.x},${parsed.y}`; alignTileNode(ch, parsed.x, parsed.y, tileNameFromConfig('border', key, config), theme); } } const tilesRoot = levelRoot.getChildByName('Tiles'); if (tilesRoot) { setupLayerContainer(tilesRoot); for (const ch of tilesRoot.children) { const parsed = parseTileNodeName(ch.name); if (!parsed) continue; const key = `${parsed.x},${parsed.y}`; const layer = parsed.layer; alignTileNode(ch, parsed.x, parsed.y, tileNameFromConfig(layer, key, config), theme); } } sortIsoTiles(levelRoot); } @ccclass('IsoDrawOrderUpdater') class IsoDrawOrderUpdater extends Component { /** 每帧 lateUpdate 重排(先移动、后排序,与 Update→LateUpdate 一致) */ lateUpdate() { if (!this.node?.isValid) return; const st = GameManager.instance?.gameState; if (st === GameState.ResultWin || st === GameState.ResultFail) return; sortIsoTiles(this.node); } } function ensureDrawOrderUpdater(levelRoot: Node) { if (!levelRoot.getComponent(IsoDrawOrderUpdater)) { levelRoot.addComponent(IsoDrawOrderUpdater); } } /** @deprecated 关卡加载后 Updater 每帧自动重排;保留 API 兼容 */ export function markIsoDrawOrderDirty(levelRoot: Node) { if (!levelRoot?.isValid) return; ensureDrawOrderUpdater(levelRoot); }