@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
/** 精灵顶边 Y(Tiles 本地) */
|
||||
@@ -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,不写 z(UI_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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user