Files
cocos/assets/scripts/level/TileLayout.ts
2026-07-14 16:02:20 +08:00

815 lines
28 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 {
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),
};
}
/** 精灵顶边 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): 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;
}
/** 足底中心 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 之间插值(骑乘时跟驱动方 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<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++;
}
// 只调 sibling不写 zUI_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<Node>();
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();
}