Files
cocos_zhuzhan/assets/scripts/level/TileLayout.ts

1011 lines
35 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 { 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<string, TileDrawEntry>();
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),
};
}
/** 精灵顶边 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;
}
/** 关卡内所有砖块/实体并入 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;
}
/** 足底中心 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 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<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 || !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<Node>): 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<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 会抖)。空载具和周围砖一起改 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<Node>();
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);
}