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 { 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, isShortFloorTile, 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 isVehicleDrawNode(node: Node): boolean { return isVehicleEntityName(safeNodeName(node)) || !!node.getComponent('VehicleController'); } /** 空载具:自身没在走,也没有骑手在走 */ function isParkedVehicleNode(node: Node): boolean { if (!isVehicleDrawNode(node)) return false; const self = node.getComponent(Movement); if (self?.isMoving()) return false; const rider = (node.getComponent('VehicleController') as { getPlayer?: () => Movement | null; } | null)?.getPlayer?.() ?? null; return !rider?.isMoving(); } function actorDrawKeyAtCell( entity: EntityDrawEntry, cell: { x: number; y: number }, wallTiles: UnitySortableTile[], ): { key: number; isPlayer: boolean } { const node = entity.node; const ent = { sortingOrder: unityEntitySortingOrder(node), cellX: cell.x, cellY: cell.y, centerY: node.position.y, depthY: getEntitySortBottomY(node), isPlayer: isPlayerEntityName(safeNodeName(node)), isPickable: entity.kind === 'pickable', }; return { key: entityDrawFrontKeyWithWalls(ent, wallTiles), isPlayer: ent.isPlayer }; } function resolveEntityDrawKey( entity: EntityDrawEntry, wallTiles: UnitySortableTile[], _force: boolean, ): { key: number; isPlayer: boolean } { if (entity.kind !== 'actor') { const ent = toUnitySortableEntity(entity); return { key: entityDrawFrontKeyWithWalls(ent, wallTiles), isPlayer: ent.isPlayer }; } const mov = resolveDrawSampleMovement(entity.node); const from = mov?.getCommittedCell(); const to = mov?.isMoving() ? mov.getLandingCell() : null; if (mov?.isMoving() && from && to) { const a = actorDrawKeyAtCell(entity, { x: from.x, y: from.y }, wallTiles); // 远离镜头:整步钉在当前格,落地后再改,否则红墙会切屁股 if (to.x + to.y > from.x + from.y) { return a; } const b = actorDrawKeyAtCell(entity, { x: to.x, y: to.y }, wallTiles); const t = mov.getMoveStepProgress(); return { key: a.key + (b.key - a.key) * t, isPlayer: a.isPlayer }; } const cell = from ? { x: from.x, y: from.y } : getActorWallSampleCell(entity, entity.node); return actorDrawKeyAtCell(entity, cell, wallTiles); } type DrawKeyed = { entry: DrawEntry; key: number; name: string; isPlayer: boolean; isShortFloor: boolean; }; function tileSetSignature(tiles: TileDrawEntry[]): string { const ids = tiles.map((t) => t.node.uuid); ids.sort(); return ids.join(','); } function sortTilesFrozen(tiles: TileDrawEntry[]): DrawKeyed[] { const keyed: DrawKeyed[] = tiles.map((t) => ({ entry: t, key: tileDrawFrontKey(toUnitySortableTile(t)), name: t.node.name, isPlayer: false, isShortFloor: isShortFloorTile(t.tileName), })); keyed.sort((a, b) => { if (a.key !== b.key) return a.key - b.key; return a.name < b.name ? -1 : a.name > b.name ? 1 : 0; }); return keyed; } function bindFrozenTiles(frozen: DrawKeyed[], tiles: TileDrawEntry[]): DrawKeyed[] | null { if (frozen.length !== tiles.length) return null; const byUuid = new Map(); for (const t of tiles) byUuid.set(t.node.uuid, t); const out: DrawKeyed[] = []; for (const f of frozen) { const t = byUuid.get(f.entry.node.uuid); if (!t?.node.isValid) return null; out.push({ entry: t, key: f.key, name: f.name, isPlayer: false, isShortFloor: f.isShortFloor, }); } return out; } /** 角色对矮砖永远在后画;载具/其它实体仍按 key。砖与砖不在这里比较。 */ function entityBeforeTile(entity: DrawKeyed, tile: DrawKeyed): boolean { if (entity.isPlayer && tile.isShortFloor) return false; if (entity.key !== tile.key) return entity.key < tile.key; if (entity.isPlayer) return false; return entity.name < tile.name; } function stripStalePlayerOcclusion(entities: EntityDrawEntry[]) { for (const e of entities) { const stale = e.node.getChildByName('__WallOcclusion'); if (stale?.isValid) stale.destroy(); } } function isKeyedWallTile(item: DrawKeyed): boolean { const tileName = (item.entry as TileDrawEntry).tileName; return !!tileName && isWallTileName(tileName); } /** * 合并时角色一旦先插入,后面冻结列表里的矮砖不会再与角色比较,会盖住脚。 * 只把角色挪到后面漏掉的矮砖之上;遇到 frontKey 更大的墙就停,不越过墙。 */ function placePlayerAfterTrailingShortFloors(merged: DrawKeyed[]) { const pi = merged.findIndex((k) => k.isPlayer); if (pi < 0) return; const playerKey = merged[pi].key; let insertAfter = pi; for (let i = pi + 1; i < merged.length; i++) { const item = merged[i]; if (isKeyedWallTile(item) && item.key > playerKey) break; if (item.isShortFloor) insertAfter = i; } if (insertAfter === pi) return; const player = merged.splice(pi, 1)[0]; merged.splice(insertAfter, 0, player); } function isTileKeyed(item: DrawKeyed): boolean { return (item.entry as TileDrawEntry).tileName != null; } function compareEntityKeyed(a: DrawKeyed, b: DrawKeyed): number { if (a.key !== b.key) return a.key - b.key; if (a.isPlayer !== b.isPlayer) return a.isPlayer ? 1 : -1; if (a.name === b.name) return 0; return a.name < b.name ? -1 : 1; } /** * 空载具先嵌进冻结砖块,再插走动的人或车。 * 人物插入时穿过空载具继续扫矮砖,避免转角处立面挡腿。 */ function insertEntityInto(list: DrawKeyed[], entity: DrawKeyed) { let i = 0; for (; i < list.length; i++) { const cur = list[i]; if (isTileKeyed(cur)) { if (entityBeforeTile(entity, cur)) break; } else if (entity.isPlayer && isParkedVehicleNode(cur.entry.node)) { continue; } else if (compareEntityKeyed(entity, cur) < 0) { break; } } list.splice(i, 0, entity); } function buildSortedDrawOrder( tiles: TileDrawEntry[], entities: EntityDrawEntry[], frozenTiles: DrawKeyed[], force = false, ): { entries: DrawEntry[]; keys: number[]; } { stripStalePlayerOcclusion(entities); const wallTiles = tiles .filter((t) => isWallTileName(t.tileName)) .map((t) => toUnitySortableTile(t)); const parked: DrawKeyed[] = []; const live: DrawKeyed[] = []; for (const e of entities) { const resolved = resolveEntityDrawKey(e, wallTiles, force); const item: DrawKeyed = { entry: e, key: resolved.key, name: e.node.name, isPlayer: resolved.isPlayer, isShortFloor: false, }; if (e.kind === 'actor' && isParkedVehicleNode(e.node)) parked.push(item); else live.push(item); } for (const p of live) { 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 = live.find((k) => k.entry.node === vehicleNode) ?? parked.find((k) => k.entry.node === vehicleNode); if (!v) continue; p.key = v.key + 1; } parked.sort(compareEntityKeyed); live.sort(compareEntityKeyed); const merged = frozenTiles.slice(); for (const item of parked) insertEntityInto(merged, item); for (const item of live) insertEntityInto(merged, item); placePlayerAfterTrailingShortFloors(merged); return { entries: merged.map((k) => k.entry), keys: merged.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; } /** 关卡内所有砖块/实体并入 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) continue; // 进关时实体先 inactive,仍要拉进 Tiles,否则空载载具排不上 if (!ch.active && !isEntityDrawNode(ch)) 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(); } } } } /** * 人车共用同一套采样(各自算各自的 key): * - 朝镜头:当前格与下一格 frontKey 按进度插值 * - 远离镜头:钉在当前格,位移结束落地后再改(避免墙切屁股) * - 静止:committed / spawn * - 骑手在动:载具跟骑手格;载具在动:角色跟载具格 */ 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 sampleCellFromMovement( mov: Movement, fallback: { x: number; y: number }, _sortNode: Node, ): { x: number; y: number } { const committed = mov.getCommittedCell(); const spawn = mov.getSpawnCell(); if (committed) return { x: committed.x, y: committed.y }; 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, { x: entity.x, y: entity.y }, node); } 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 mov = node.getComponent(Movement); if (mov) { const own = mov.getCommittedCell?.() ?? mov.getSpawnCell?.(); if (own) { const sampled = sampleCellFromMovement(mov, { x: own.x, y: own.y }, node); return { x: sampled.x, y: sampled.y }; } } 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 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 || !ch.active || 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; // 已拾取道具会先 inactive 再销毁;进关角色/载具 inactive 仍要参与排序 if (!node.active && isPickableProp(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 会抖)。空载具和周围砖一起改 index,相对层不变 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 updater = levelRoot.getComponent(IsoDrawOrderUpdater); const tileSig = tileSetSignature(tileEntries); let frozen = updater?.getFrozenTiles(tileSig) ?? null; if (frozen) frozen = bindFrozenTiles(frozen, tileEntries); if (!frozen) { frozen = sortTilesFrozen(tileEntries); updater?.setFrozenTiles(tileSig, frozen); } const sorted = buildSortedDrawOrder(tileEntries, entityEntries, frozen, force); if (force) updater?.resetOrderCache(); const sig = drawOrderSignature(sorted.entries, sorted.keys); const changed = !updater || updater.noteOrderChanged(sig); if (!force && !changed) { finalizeLevelRootOrder(levelRoot, tilesRoot); updater?.clearDirty(); return; } applyDrawOrder(tilesRoot, sorted.entries); finalizeLevelRootOrder(levelRoot, tilesRoot); updater?.clearDirty(); } /** @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 = ''; /** 进关后强制全量的剩余帧数(等 start / 贴图 / 绑乘落稳) */ private _primeFrames = 0; private _frozenTileSig = ''; private _frozenTiles: DrawKeyed[] = []; markDirty() { this._dirty = true; } clearDirty() { this._dirty = false; this._primeFrames = 0; } prime(frames = 2) { this._primeFrames = Math.max(this._primeFrames, frames); this._dirty = true; this._lastOrderSig = ''; this._frozenTileSig = ''; this._frozenTiles = []; } resetOrderCache() { this._lastOrderSig = ''; } getFrozenTiles(sig: string): DrawKeyed[] | null { if (!sig || sig !== this._frozenTileSig || this._frozenTiles.length < 1) return null; return this._frozenTiles; } setFrozenTiles(sig: string, tiles: DrawKeyed[]) { this._frozenTileSig = sig; this._frozenTiles = tiles; } /** @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; const force = this._primeFrames > 0; if (force) this._primeFrames -= 1; else if (!this._dirty) return; this._dirty = false; sortIsoTiles(this.node, force); } } 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(); } /** 开局/重置后强制排 2 帧,避免第一帧用到未绑乘、未刷贴图的顺序 */ export function primeIsoDrawOrder(levelRoot: Node, frames = 2) { if (!levelRoot?.isValid) return; ensureDrawOrderUpdater(levelRoot); levelRoot.getComponent(IsoDrawOrderUpdater)?.prime(frames); }