Files
cocos/assets/scripts/level/TileLayout.ts
2026-07-06 09:43:46 +08:00

720 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
});
}
/** 精灵顶边 YTiles 本地) */
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;
}
/** 墙砖立面顶边 YWallBlock 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;
}
/** 足底中心 YTiles 本地);排序依据底边而非锚点/中心 */
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;
}
/** 画家算法 Orderround(底边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<Node>): 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<Node>): 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<Node>();
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<Node>();
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);
}