no message

This commit is contained in:
2026-07-06 09:43:46 +08:00
parent 4564ac5f47
commit ff1542c24f
22 changed files with 8771 additions and 309 deletions

View File

@@ -1,42 +1,52 @@
import { Node, UITransform, Vec3, Layers, Sprite } from 'cc';
import { _decorator, Component, Node, UITransform, Vec3, Layers, Sprite } from 'cc';
import { cellToWorldCenter, parseTileNodeName } from '../core/GridCoords';
import { CommonDefine } from '../core/Define';
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;
/**
* 遮挡分类(仅在关卡加载时按格子坐标排序一次)
* - walkable : Baseblock / JumpBlock永不在 actor 之上
* - wall : Border / WallBlock与 actor 按 iso 深度比较
* - actor : 角色 / 载具
* - pickable : 可拾取物Prop永不在墙砖/路径砖之下;玩家仍在其上
* - scenery : 其它
* Cocos 用 siblingIndex 模拟 Unity SortingOrder + CustomAxis 深度。
* 逻辑见 UnityDrawSort.ts对齐 GraphicsSettings + TilemapRenderer Individual
*/
export const DRAW_SORT_Y_SCALE = 100;
type DrawKind = 'walkable' | 'wall' | 'actor' | 'pickable' | 'scenery';
const DRAW_RANK = {
groundProp: 10,
groundTile: 20,
borderTile: 22,
vehicle: 28,
coin: 30,
propDecor: 30,
player: 40,
} as const;
interface DrawEntry {
interface TileDrawEntry {
node: Node;
x: number;
y: number;
kind: DrawKind;
tileName: string;
}
interface EntityDrawEntry {
node: Node;
x: number;
y: number;
rank: number;
kind: DrawKind;
}
type DrawEntry = TileDrawEntry | EntityDrawEntry;
function safeNodeName(node: Node | null | undefined): string {
return (node?.isValid ? node.name : '') || '';
}
@@ -80,21 +90,30 @@ function isWalkablePathTile(tileName: string): boolean {
}
function isWallTileName(tileName: string): boolean {
return tileName === 'WallBlock' || tileName === 'kuai11';
return tileName === CommonDefine.BlockWall;
}
function resolveGroundTileName(cellX: number, cellY: number, config?: LevelConfig): string {
return config?.ground?.[`${cellX},${cellY}`] ?? CommonDefine.BlockBase;
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 {
if (parsed.layer === 'border') return 'wall';
const name = resolveGroundTileName(parsed.x, parsed.y, config);
if (isWalkablePathTile(name)) return 'walkable';
const name = resolveTileName(parsed, config);
if (isWallTileName(name)) return 'wall';
if (isWalkablePathTile(name)) return 'walkable';
return 'scenery';
}
@@ -109,69 +128,254 @@ export interface EntityDrawOrderOptions {
preferVehicleOverPlayer?: boolean;
}
function entityRank(node: Node): number {
function unityEntitySortingOrder(node: Node): number {
const n = safeNodeName(node);
if (isPlayerEntityName(n)) return DRAW_RANK.player;
if (isVehicleEntityName(n)) return DRAW_RANK.vehicle;
if (isCoinEntityName(n)) return DRAW_RANK.coin;
if (n === 'PropDecor' || n.startsWith('PropDecor_')) return DRAW_RANK.propDecor;
return DRAW_RANK.groundProp;
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 tileRank(kind: DrawKind, layer: 'ground' | 'border'): number {
if (kind === 'walkable') return DRAW_RANK.groundTile;
if (kind === 'wall') return DRAW_RANK.borderTile;
return layer === 'border' ? DRAW_RANK.borderTile : DRAW_RANK.groundTile;
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 function compareIsoDrawOrder(cellX: number, cellY: number, otherX: number, otherY: number): number {
const ka = cellX + cellY;
const kb = otherX + otherY;
if (ka !== kb) return kb - ka;
return otherX - cellX;
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;
});
}
function wallFaceSamples(wallX: number, wallY: number): { x: number; y: number }[] {
return [{ x: wallX, y: wallY - 1 }, { x: wallX - 1, y: wallY }];
/** 精灵顶边 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;
}
function compareWallActorIso(actor: DrawEntry, wall: DrawEntry): number {
let actorAhead = 0;
for (const face of wallFaceSamples(wall.x, wall.y)) {
const iso = compareIsoDrawOrder(actor.x, actor.y, face.x, face.y);
if (iso < 0) return iso;
if (iso > actorAhead) actorAhead = iso;
}
return actorAhead;
/** 墙砖立面顶边 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 compareDrawEntries(a: DrawEntry, b: DrawEntry): number {
if (a.kind === 'walkable' && b.kind === 'actor') return -1;
if (b.kind === 'walkable' && a.kind === 'actor') return 1;
function resolveActorSortAnchorY(node: Node): number {
return isVehicleEntityName(safeNodeName(node)) ? DEFAULT_VEHICLE_ANCHOR_Y : DEFAULT_PLAYER_ANCHOR_Y;
}
// 可拾取物:永远在墙砖/路径砖/载具之上;玩家仍在其上
if (a.kind === 'pickable' && b.kind === 'actor') {
return isPlayerEntityName(safeNodeName(b.node)) ? -1 : 1;
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);
}
if (a.kind === 'actor' && b.kind === 'pickable') {
return isPlayerEntityName(safeNodeName(a.node)) ? 1 : -1;
}
if (a.kind === 'pickable' && b.kind !== 'pickable') return 1;
if (b.kind === 'pickable' && a.kind !== 'pickable') return -1;
const sizes = getEntityDisplaySizes(theme);
return role === 'vehicle' ? sizes.vehicle.height : sizes.player.height;
}
if (a.kind === 'actor' && b.kind === 'wall') {
const wallIso = compareWallActorIso(a, b);
if (wallIso !== 0) return wallIso;
/** 逻辑格站立点 + 固定参考锚点 → 排序底边 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);
}
if (a.kind === 'wall' && b.kind === 'actor') {
const wallIso = compareWallActorIso(b, a);
if (wallIso !== 0) return -wallIso;
}
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 iso = compareIsoDrawOrder(a.x, a.y, b.x, b.y);
if (iso !== 0) return iso;
return a.rank - b.rank;
const cell = mov?.getCommittedCell() ?? mov?.getSpawnCell();
if (!cell) return getNodeBottomY(node);
return getActorStandBottomY(cell, node, role, config, theme);
}
/** 实体所在关卡根Ground/Border/Tiles 的父节点,不是 Tiles 容器本身) */
@@ -187,7 +391,7 @@ export function resolveLevelDrawRoot(from: Node): Node | null {
return null;
}
/** 实体逻辑格(spawn / committed,移动中不重算 */
/** 实体逻辑格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?.();
@@ -205,10 +409,7 @@ function getEntityLogicCell(node: Node): { x: number; y: number } {
if (riderCell) return { x: riderCell.x, y: riderCell.y };
}
const mov = node.getComponent('Movement') as {
getCommittedCell?: () => Vec3 | null;
getSpawnCell?: () => Vec3 | null;
} | null;
const mov = node.getComponent(Movement);
if (mov) {
const cell = mov.getCommittedCell?.() ?? mov.getSpawnCell?.();
if (cell) return { x: cell.x, y: cell.y };
@@ -248,19 +449,20 @@ function resolveLevelConfig(): LevelConfig | undefined {
?? undefined;
}
function collectTileEntries(levelRoot: Node, tilesRoot: Node, seen: Set<Node>): DrawEntry[] {
function collectTileEntries(levelRoot: Node, tilesRoot: Node, seen: Set<Node>): TileDrawEntry[] {
const config = resolveLevelConfig();
const entries: DrawEntry[] = [];
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,
rank: tileRank(kind, parsed.layer),
kind,
tileName,
});
};
@@ -273,8 +475,8 @@ function collectTileEntries(levelRoot: Node, tilesRoot: Node, seen: Set<Node>):
return entries;
}
function collectEntityEntries(levelRoot: Node, tilesRoot: Node, seen: Set<Node>): DrawEntry[] {
const entries: DrawEntry[] = [];
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);
@@ -283,64 +485,86 @@ function collectEntityEntries(levelRoot: Node, tilesRoot: Node, seen: Set<Node>)
node,
x: cell.x,
y: cell.y,
rank: entityRank(node),
kind: classifyEntityKind(node),
});
};
for (const ch of levelRoot.children) pushEntity(ch);
for (const ch of tilesRoot.children) pushEntity(ch);
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 sorted = entries.slice().sort(compareDrawEntries);
const inSort = new Set<Node>();
const worldPos = new Vec3();
for (const { node } of sorted) {
for (const { node } of entries) {
if (!node.isValid) continue;
inSort.add(node);
if (isEntityDrawNode(node)) ensureEntityUILayer(node);
if (node.parent !== tilesRoot) {
const lp = node.position.clone();
node.getWorldPosition(worldPos);
node.parent = tilesRoot;
node.setPosition(lp);
node.setWorldPosition(worldPos);
}
}
for (let i = 0; i < sorted.length; i++) {
const node = sorted[i].node;
if (!node.isValid) continue;
if (node.getSiblingIndex() !== i) node.setSiblingIndex(i);
}
let tail = sorted.length;
// 未参与排序的节点保持在最底层,避免盖住角色
let head = 0;
for (const ch of [...tilesRoot.children]) {
if (!ch.isValid || inSort.has(ch)) continue;
if (ch.getSiblingIndex() !== tail) ch.setSiblingIndex(tail);
tail++;
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 && layer.children.length === 0) {
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;
}
/**
* 关卡加载 / 重置时按格子坐标排序一次;移动过程中不再改 sibling
* 砖块按格子深度排序,人物/载具/金币按底边 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, [...tileEntries, ...entityEntries]);
applyDrawOrder(tilesRoot, buildSortedDrawOrder(tileEntries, entityEntries));
finalizeLevelRootOrder(levelRoot, tilesRoot);
}
@@ -383,7 +607,7 @@ function ensureTilesRoot(levelRoot: Node): Node {
return tilesRoot;
}
const TILE_LAYER_NAMES = ['Ground', 'Border', 'Tiles'] as const;
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) {
@@ -429,9 +653,9 @@ function tileNameFromConfig(
return config.ground?.[key] ?? CommonDefine.BlockBase;
}
let v = config.border?.[key];
if (v === true || v === undefined) return 'WallBlock';
if (v === true || v === undefined) return CommonDefine.BlockWall;
if (typeof v === 'string') return v;
return 'WallBlock';
return CommonDefine.BlockWall;
}
export function layoutLevelTiles(levelRoot: Node, config: LevelConfig) {
@@ -470,3 +694,26 @@ export function layoutLevelTiles(levelRoot: Node, config: LevelConfig) {
}
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);
}