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 { compareIsoDrawOrder, UNITY_SORTING_ORDER, tileDrawFrontKey, entityDrawFrontKeyWithWalls, type UnitySortableEntity, type UnitySortableTile, } from './UnityDrawSort'; import { resolveTileNameAtCell } from './TileKinds'; 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}`; return resolveTileNameAtCell(parsed.layer, key, config?.ground, config?.border); } /** 分类基于规范化后的瓦片名(贴图名已按层级锁定,避免墙/地属性互串) */ 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[]): { entries: DrawEntry[]; keys: number[]; } { const wallTiles = tiles .filter((t) => isWallTileName(t.tileName)) .map((t) => toUnitySortableTile(t)); const keyed: { entry: DrawEntry; key: number; name: string; isPlayer: boolean }[] = []; for (const t of tiles) { keyed.push({ entry: t, key: tileDrawFrontKey(toUnitySortableTile(t)), name: t.node.name, isPlayer: false, }); } for (const e of entities) { const ent = toUnitySortableEntity(e); keyed.push({ entry: e, key: entityDrawFrontKeyWithWalls(ent, wallTiles), name: e.node.name, isPlayer: ent.isPlayer, }); } // 正在骑乘:角色 frontKey 高于其载具(同格 typeRank 通常已够;此处兜住贴墙抬升后并列/短暂格子差) for (const p of keyed) { if (!p.isPlayer || p.entry.kind !== 'actor') continue; const ride = (p.entry.node.getComponent('PlayerController') as { getRideVehicle?: () => { node?: Node } | null; } | null)?.getRideVehicle?.(); const vehicleNode = ride?.node; if (!vehicleNode) continue; const v = keyed.find((k) => k.entry.node === vehicleNode); if (v && p.key <= v.key) p.key = v.key + 1; } keyed.sort((a, b) => { if (a.key !== b.key) return a.key - b.key; // 同 key:角色永远盖过载具(避免 Player 名字排在 Vehicle 前反而画在下) if (a.isPlayer !== b.isPlayer) return a.isPlayer ? 1 : -1; return a.name < b.name ? -1 : a.name > b.name ? 1 : 0; }); return { entries: keyed.map((k) => k.entry), keys: keyed.map((k) => k.key), }; } /** 精灵顶边 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): boolean { 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); let moved = false; for (const node of pending) { if (node.parent === tilesRoot) continue; node.getWorldPosition(worldPos); node.parent = tilesRoot; node.setWorldPosition(worldPos); moved = true; } return moved; } 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(); } } } } /** * 移动中采样格(图层用)——人物 / 载具拆开: * - 角色朝屏幕前(落点 x+y 更小):用落点,避免裁腿;朝后用起点 * - 载具朝屏幕前(下/南、右/东):用起点,避免提早浮到前侧地砖上 * - 载具朝屏幕后(上/北、左/西):用落点,否则钉在起点会比落点更靠前, * 途中会短暂「不被下方砖挡住」,落地写回后又正常 * - 骑乘跟驱动方取格,再按本节点人/车分叉 */ function resolveDrawSampleMovement(node: Node): Movement | null { const self = node.getComponent(Movement); const rider = (node.getComponent('VehicleController') as { getPlayer?: () => Movement | null; } | null)?.getPlayer?.() ?? null; if (rider?.isMoving()) return rider; const vehicle = (node.getComponent('PlayerController') as { getRideVehicle?: () => Movement | null; } | null)?.getRideVehicle?.() ?? null; if (vehicle?.isMoving()) return vehicle; return self; } function isVehicleSortNode(node: Node): boolean { return isVehicleEntityName(safeNodeName(node)) || !!node.getComponent('VehicleController'); } function sampleCellFromMovement( mov: Movement, node: Node, fallback: { x: number; y: number }, ): { x: number; y: number } { const committed = mov.getCommittedCell(); if (mov.isMoving()) { const landing = mov.getLandingCell(); if (committed && landing) { const fromSum = committed.x + committed.y; const toSum = landing.x + landing.y; const goingFront = toSum < fromSum; if (isVehicleSortNode(node)) { // 载具:朝前钉起点;朝后改落点(与落地静止同层,继续被下方砖挡) if (goingFront) { return { x: committed.x, y: committed.y }; } return { x: landing.x, y: landing.y }; } // 角色:朝前用落点,朝后用起点 if (goingFront) { return { x: landing.x, y: landing.y }; } return { x: committed.x, y: committed.y }; } if (committed) return { x: committed.x, y: committed.y }; return getEntitySortCell(node); } if (committed) return { x: committed.x, y: committed.y }; const spawn = mov.getSpawnCell(); if (spawn) return { x: spawn.x, y: spawn.y }; return fallback; } function getActorWallSampleCell(entity: EntityDrawEntry, node: Node): { x: number; y: number } { const mov = resolveDrawSampleMovement(node); if (!mov) return { x: entity.x, y: entity.y }; return sampleCellFromMovement(mov, node, { x: entity.x, y: entity.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 之间插值(骑乘时跟驱动方 Movement 同步)。 */ function getEntitySortBottomY(node: Node): number { if (!isActorEntity(node)) return getNodeBottomY(node); const mov = resolveDrawSampleMovement(node) ?? 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++; } // 只调 sibling,不写 z(UI_2D 改 z 会抖) 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); } } function drawOrderSignature(entries: DrawEntry[], keys: number[]): string { let s = ''; for (let i = 0; i < entries.length; i++) { if (i) s += '\n'; s += `${entries[i].node.uuid}:${keys[i]}`; } return s; } 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; } /** * 等距绘制排序:开局 force 全量;起步/落地 dirty 时若顺序未变则跳过。 */ export function sortIsoTiles(levelRoot: Node, force = false) { 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); const sorted = buildSortedDrawOrder(tileEntries, entityEntries); const updater = levelRoot.getComponent(IsoDrawOrderUpdater); if (force) updater?.resetOrderCache(); const sig = drawOrderSignature(sorted.entries, sorted.keys); const changed = !updater || updater.noteOrderChanged(sig); if (!force && !changed) { finalizeLevelRootOrder(levelRoot, tilesRoot); return; } applyDrawOrder(tilesRoot, sorted.entries); finalizeLevelRootOrder(levelRoot, tilesRoot); } /** @deprecated 与 sortIsoTiles 相同 */ export function refreshIsoEntityDrawOrder(levelRoot: Node) { sortIsoTiles(levelRoot, true); } /** @deprecated 与 sortIsoTiles 相同 */ export function refreshIsoDrawOrder(levelRoot: Node) { sortIsoTiles(levelRoot, true); } /** @deprecated 与 sortIsoTiles 相同 */ export function refreshIsoDrawOrderImmediate(levelRoot: Node) { sortIsoTiles(levelRoot, true); } export function bringEntityNodesToFront(levelRoot: Node, _opts?: EntityDrawOrderOptions) { sortIsoTiles(levelRoot, true); } 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 { return resolveTileNameAtCell(layer, key, config.ground, config.border); } 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, true); } @ccclass('IsoDrawOrderUpdater') class IsoDrawOrderUpdater extends Component { private _dirty = true; private _lastOrderSig = ''; markDirty() { this._dirty = true; } resetOrderCache() { this._lastOrderSig = ''; } /** @returns true 表示顺序相对缓存有变化 */ noteOrderChanged(sig: string): boolean { if (sig === this._lastOrderSig) return false; this._lastOrderSig = sig; return true; } lateUpdate() { if (!this.node?.isValid) return; const st = GameManager.instance?.gameState; if (st === GameState.ResultWin || st === GameState.ResultFail) return; if (!this._dirty) return; this._dirty = false; sortIsoTiles(this.node, false); } } function ensureDrawOrderUpdater(levelRoot: Node) { if (!levelRoot.getComponent(IsoDrawOrderUpdater)) { levelRoot.addComponent(IsoDrawOrderUpdater); } } export function markIsoDrawOrderDirty(levelRoot: Node) { if (!levelRoot?.isValid) return; ensureDrawOrderUpdater(levelRoot); levelRoot.getComponent(IsoDrawOrderUpdater)?.markDirty(); }