图层修改

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 16:02:20 +08:00
parent ff1542c24f
commit ae75fce80b
5121 changed files with 17930 additions and 16467861 deletions

View File

@@ -11,14 +11,14 @@ 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,
tileDrawFrontKey,
entityDrawFrontKeyWithWalls,
type UnitySortableEntity,
type UnitySortableTile,
} from './UnityDrawSort';
import { resolveTileNameAtCell } from './TileKinds';
const { ccclass } = _decorator;
@@ -98,15 +98,10 @@ function resolveTileName(
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;
return resolveTileNameAtCell(parsed.layer, key, config?.ground, config?.border);
}
/** 分类基于规范化后的瓦片名(贴图名已按层级锁定,避免墙/地属性互串) */
function classifyTileKind(
parsed: { layer: 'ground' | 'border'; x: number; y: number },
config?: LevelConfig,
@@ -186,31 +181,53 @@ function toUnitySortableEntity(entity: EntityDrawEntry): UnitySortableEntity {
/** 等距深度:返回值 > 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;
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 本地) */
@@ -269,7 +286,7 @@ function getEntitySortCell(node: Node): { x: number; y: number } {
}
/** 关卡内所有砖块/实体并入 Tiles避免 Ground/Border 在 BFS 中整块早于 Tiles 绘制) */
function pullDrawablesIntoTiles(levelRoot: Node, tilesRoot: Node) {
function pullDrawablesIntoTiles(levelRoot: Node, tilesRoot: Node): boolean {
const worldPos = new Vec3();
const pending: Node[] = [];
const walk = (parent: Node) => {
@@ -285,12 +302,15 @@ function pullDrawablesIntoTiles(levelRoot: Node, tilesRoot: Node) {
}
};
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) {
@@ -307,21 +327,69 @@ function dedupeLayerTileDuplicates(levelRoot: Node, tilesRoot: Node) {
}
}
function getActorWallSampleCell(entity: EntityDrawEntry, node: Node): { x: number; y: number } {
const mov = node.getComponent(Movement);
if (!mov) return { x: entity.x, y: entity.y };
/**
* 移动中采样格(图层用)——人物 / 载具拆开:
* - 角色朝屏幕前(落点 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()) {
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 };
const spawn = mov.getSpawnCell();
if (spawn) return { x: spawn.x, y: spawn.y };
return { x: entity.x, y: entity.y };
return getEntitySortCell(node);
}
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 };
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 {
@@ -352,11 +420,11 @@ export function getNodeSortOrder(node: Node): number {
/**
* 人物/载具排序底边 Y始终按逻辑格站立点 + 固定参考锚点;
* 移动中在起止格底边 Y 之间插值(忽略跳跃弧与序列帧 anchor 抖动)。
* 移动中在起止格底边 Y 之间插值(骑乘时跟驱动方 Movement 同步)。
*/
function getEntitySortBottomY(node: Node): number {
if (!isActorEntity(node)) return getNodeBottomY(node);
const mov = node.getComponent(Movement);
const mov = resolveDrawSampleMovement(node) ?? node.getComponent(Movement);
const gm = GameManager.instance;
const config = resolveLevelConfig();
const theme = config?.theme ?? gm?.uiStyle;
@@ -519,7 +587,6 @@ function applyDrawOrder(tilesRoot: Node, entries: DrawEntry[]) {
}
}
// 未参与排序的节点保持在最底层,避免盖住角色
let head = 0;
for (const ch of [...tilesRoot.children]) {
if (!ch.isValid || inSort.has(ch)) continue;
@@ -527,18 +594,24 @@ function applyDrawOrder(tilesRoot: Node, entries: DrawEntry[]) {
head++;
}
// 从高索引往低索引写入,避免 setSiblingIndex 互相覆盖
// 只调 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);
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 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);
@@ -553,9 +626,9 @@ function finalizeLevelRootOrder(levelRoot: Node, tilesRoot: Node) {
}
/**
* 砖块按格子深度排序,人物/载具/金币按底边 Y 插入砖块序列;移动中可重排
* 等距绘制排序:开局 force 全量;起步/落地 dirty 时若顺序未变则跳过
*/
export function sortIsoTiles(levelRoot: Node) {
export function sortIsoTiles(levelRoot: Node, force = false) {
if (!levelRoot?.isValid) return;
ensureDrawOrderUpdater(levelRoot);
const tilesRoot = ensureTilesRoot(levelRoot);
@@ -564,27 +637,36 @@ export function sortIsoTiles(levelRoot: Node) {
const seen = new Set<Node>();
const tileEntries = collectTileEntries(levelRoot, tilesRoot, seen);
const entityEntries = collectEntityEntries(levelRoot, tilesRoot, seen);
applyDrawOrder(tilesRoot, buildSortedDrawOrder(tileEntries, entityEntries));
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);
sortIsoTiles(levelRoot, true);
}
/** @deprecated 与 sortIsoTiles 相同 */
export function refreshIsoDrawOrder(levelRoot: Node) {
sortIsoTiles(levelRoot);
sortIsoTiles(levelRoot, true);
}
/** @deprecated 与 sortIsoTiles 相同 */
export function refreshIsoDrawOrderImmediate(levelRoot: Node) {
sortIsoTiles(levelRoot);
sortIsoTiles(levelRoot, true);
}
export function bringEntityNodesToFront(levelRoot: Node, _opts?: EntityDrawOrderOptions) {
sortIsoTiles(levelRoot);
sortIsoTiles(levelRoot, true);
}
export function ensureEntityUILayer(node: Node) {
@@ -649,13 +731,7 @@ function tileNameFromConfig(
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;
return resolveTileNameAtCell(layer, key, config.ground, config.border);
}
export function layoutLevelTiles(levelRoot: Node, config: LevelConfig) {
@@ -692,17 +768,36 @@ export function layoutLevelTiles(levelRoot: Node, config: LevelConfig) {
alignTileNode(ch, parsed.x, parsed.y, tileNameFromConfig(layer, key, config), theme);
}
}
sortIsoTiles(levelRoot);
sortIsoTiles(levelRoot, true);
}
@ccclass('IsoDrawOrderUpdater')
class IsoDrawOrderUpdater extends Component {
/** 每帧 lateUpdate 重排(先移动、后排序,与 Update→LateUpdate 一致) */
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;
sortIsoTiles(this.node);
if (!this._dirty) return;
this._dirty = false;
sortIsoTiles(this.node, false);
}
}
@@ -712,8 +807,8 @@ function ensureDrawOrderUpdater(levelRoot: Node) {
}
}
/** @deprecated 关卡加载后 Updater 每帧自动重排;保留 API 兼容 */
export function markIsoDrawOrderDirty(levelRoot: Node) {
if (!levelRoot?.isValid) return;
ensureDrawOrderUpdater(levelRoot);
levelRoot.getComponent(IsoDrawOrderUpdater)?.markDirty();
}