@@ -5,9 +5,9 @@ import { EDITOR, PREVIEW, BUILD } from 'cc/env';
|
||||
import {
|
||||
cellToWorldCenter, tileNodeName, parseTileNodeName, getHalfCellSize,
|
||||
} from '../core/GridCoords';
|
||||
import { CommonDefine } from '../core/Define';
|
||||
import { LevelMapData } from './LevelMapData';
|
||||
import { alignTileNode, setupLayerContainer, sortIsoTiles } from './TileLayout';
|
||||
import { resolveTileNameAtCell } from './TileKinds';
|
||||
|
||||
const { ccclass, property, executeInEditMode, executionOrder } = _decorator;
|
||||
|
||||
@@ -257,10 +257,7 @@ export class GridSnapHelper extends Component {
|
||||
groundMap: Record<string, string>,
|
||||
borderMap: Record<string, boolean | string>,
|
||||
): string {
|
||||
if (layer === 'ground') return groundMap[key] ?? CommonDefine.BlockBase;
|
||||
const v = borderMap[key];
|
||||
if (typeof v === 'string') return v;
|
||||
return 'WallBlock';
|
||||
return resolveTileNameAtCell(layer, key, groundMap, borderMap);
|
||||
}
|
||||
|
||||
private applySnap(
|
||||
|
||||
@@ -58,7 +58,8 @@ export function mergeLevelConfigWithMapData(config: LevelConfig, levelRoot: Node
|
||||
|
||||
return {
|
||||
...config,
|
||||
theme: prefabTheme || config.theme?.trim() || config.theme,
|
||||
// DB 主题优先(主站批次权威);预制体仅作缺省回退
|
||||
theme: config.theme?.trim() || prefabTheme || 'silu',
|
||||
ground: pickGround(
|
||||
prefabGround as LevelConfig['ground'],
|
||||
config.ground,
|
||||
|
||||
@@ -113,9 +113,9 @@ function shardKey(shard: { file: string }) {
|
||||
}
|
||||
|
||||
function validateIndex(index: LevelsDbIndex): void {
|
||||
if (index.total < 100 || index.min < LEVEL_ID_BASE) {
|
||||
if (index.total < 1 || index.min < LEVEL_ID_BASE) {
|
||||
throw new Error(
|
||||
`关卡库索引过旧 (${index.total} 关),请重新 package-for-project`,
|
||||
`关卡库索引无效 (${index.total} 关),请重新 package-for-project`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -133,9 +133,9 @@ function validateIngested(): void {
|
||||
}
|
||||
const total = sortedIds.length;
|
||||
const minId = sortedIds[0] ?? 0;
|
||||
if (total < 100 || minId < LEVEL_ID_BASE) {
|
||||
if (total < 1 || minId < LEVEL_ID_BASE) {
|
||||
throw new Error(
|
||||
`关卡库过旧 (${total} 关),请用主站导出的 levels-database.json;`
|
||||
`关卡库无效 (${total} 关),请用主站导出的 levels-database.json;`
|
||||
+ '运行 bash tools/sync-level-db.sh 后重新 package-for-project',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Node, UITransform, Layers, Sprite } from 'cc';
|
||||
import { LevelConfig } from './LevelTypes';
|
||||
import { CommonDefine } from '../core/Define';
|
||||
import { VisualAssets, normalizeTheme } from '../visual/VisualAssets';
|
||||
import { layoutLevelTiles, sortIsoTiles } from './TileLayout';
|
||||
import { LevelTileLayout } from './LevelTileLayout';
|
||||
@@ -9,6 +8,7 @@ import { GridSnapHelper } from './GridSnapHelper';
|
||||
import { getThemeBorderDecorKey } from '../theme/ThemeRegistry';
|
||||
import { centerLevelRoot, syncTileNodesFromConfig } from './LevelTileSync';
|
||||
import { mergeLevelConfigWithMapData } from './LevelConfigMerge';
|
||||
import { resolveTileNameAtCell, normalizeGroundTile, normalizeBorderTile } from './TileKinds';
|
||||
|
||||
const UI_LAYER = Layers.Enum.UI_2D;
|
||||
|
||||
@@ -70,13 +70,10 @@ export class LevelDisplay {
|
||||
const decorKey = getThemeBorderDecorKey(config.theme);
|
||||
if (decorKey) names.add(decorKey);
|
||||
for (const v of Object.values(config.ground ?? {})) {
|
||||
const tile = typeof v === 'string' ? v.trim() : (typeof v === 'number' ? String(v) : '');
|
||||
if (tile) names.add(tile);
|
||||
names.add(normalizeGroundTile(v));
|
||||
}
|
||||
for (const v of Object.values(config.border ?? {})) {
|
||||
if (v === true) continue;
|
||||
const tile = typeof v === 'string' ? v.trim() : (typeof v === 'number' ? String(v) : '');
|
||||
if (tile) names.add(tile);
|
||||
names.add(normalizeBorderTile(v));
|
||||
}
|
||||
return Array.from(names);
|
||||
}
|
||||
@@ -98,7 +95,7 @@ export class LevelDisplay {
|
||||
}
|
||||
|
||||
static sortIsoLayers(levelRoot: Node) {
|
||||
sortIsoTiles(levelRoot);
|
||||
sortIsoTiles(levelRoot, true);
|
||||
}
|
||||
|
||||
private static queueTileSprite(
|
||||
@@ -122,31 +119,24 @@ export class LevelDisplay {
|
||||
static async refreshTileSprites(levelRoot: Node, config: LevelConfig, theme: string) {
|
||||
const jobs: Promise<void>[] = [];
|
||||
|
||||
const processLayer = (layer: Node | null, isBorder: boolean) => {
|
||||
const processLayer = (layer: Node | null, layerKind: 'ground' | 'border') => {
|
||||
if (!layer) return;
|
||||
for (const ch of layer.children) {
|
||||
if (!ch?.active) continue;
|
||||
const m = isBorder
|
||||
const m = layerKind === 'border'
|
||||
? /^b_(-?\d+)_(-?\d+)$/.exec(ch.name)
|
||||
: /^g_(-?\d+)_(-?\d+)$/.exec(ch.name);
|
||||
if (!m) continue;
|
||||
const key = `${m[1]},${m[2]}`;
|
||||
const cx = parseInt(m[1], 10);
|
||||
const cy = parseInt(m[2], 10);
|
||||
let tileName: string;
|
||||
if (isBorder) {
|
||||
tileName = config.border?.[key] as string | boolean | undefined;
|
||||
if (tileName === true || tileName === undefined) tileName = 'WallBlock';
|
||||
if (typeof tileName !== 'string') tileName = 'WallBlock';
|
||||
} else {
|
||||
tileName = config.ground?.[key] ?? CommonDefine.BlockBase;
|
||||
}
|
||||
const tileName = resolveTileNameAtCell(layerKind, key, config.ground, config.border);
|
||||
this.queueTileSprite(jobs, ch, tileName, cx, cy, theme);
|
||||
}
|
||||
};
|
||||
|
||||
processLayer(levelRoot.getChildByName('Ground'), false);
|
||||
processLayer(levelRoot.getChildByName('Border'), true);
|
||||
processLayer(levelRoot.getChildByName('Ground'), 'ground');
|
||||
processLayer(levelRoot.getChildByName('Border'), 'border');
|
||||
|
||||
const tiles = levelRoot.getChildByName('Tiles');
|
||||
if (tiles) {
|
||||
@@ -158,16 +148,14 @@ export class LevelDisplay {
|
||||
const key = `${mg[1]},${mg[2]}`;
|
||||
this.queueTileSprite(
|
||||
jobs, ch,
|
||||
config.ground?.[key] ?? CommonDefine.BlockBase,
|
||||
resolveTileNameAtCell('ground', key, config.ground, config.border),
|
||||
parseInt(mg[1], 10), parseInt(mg[2], 10), theme,
|
||||
);
|
||||
} else if (mb) {
|
||||
const key = `${mb[1]},${mb[2]}`;
|
||||
let tileName = config.border?.[key];
|
||||
if (tileName === true || tileName === undefined) tileName = 'WallBlock';
|
||||
if (typeof tileName !== 'string') tileName = 'WallBlock';
|
||||
this.queueTileSprite(
|
||||
jobs, ch, tileName,
|
||||
jobs, ch,
|
||||
resolveTileNameAtCell('border', key, config.ground, config.border),
|
||||
parseInt(mb[1], 10), parseInt(mb[2], 10), theme,
|
||||
);
|
||||
}
|
||||
|
||||
54
assets/scripts/level/TileKinds.ts
Normal file
54
assets/scripts/level/TileKinds.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { CommonDefine } from '../core/Define';
|
||||
|
||||
/** 地砖:仅 Baseblock / JumpBlock */
|
||||
const GROUND_TILES = new Set<string>([CommonDefine.BlockBase, CommonDefine.BlockJump]);
|
||||
|
||||
/** 墙砖与墙饰(Border 层) */
|
||||
const BORDER_TILES = new Set<string>([
|
||||
CommonDefine.BlockWall,
|
||||
'kuai11',
|
||||
'Decor23',
|
||||
'素材切图-23',
|
||||
'素材切图2-23',
|
||||
'小游戏素材红色_03',
|
||||
]);
|
||||
|
||||
export function isGroundTileName(tileName: string): boolean {
|
||||
return GROUND_TILES.has(tileName);
|
||||
}
|
||||
|
||||
export function isWallBlockTileName(tileName: string): boolean {
|
||||
return tileName === CommonDefine.BlockWall;
|
||||
}
|
||||
|
||||
/** Ground 层:强制地砖属性,拒绝墙砖名 */
|
||||
export function normalizeGroundTile(value: unknown): string {
|
||||
return value === CommonDefine.BlockJump ? CommonDefine.BlockJump : CommonDefine.BlockBase;
|
||||
}
|
||||
|
||||
/** Border 层:强制墙砖属性,拒绝地砖名 */
|
||||
export function normalizeBorderTile(value: unknown): string {
|
||||
if (value === true || value == null) return CommonDefine.BlockWall;
|
||||
if (typeof value !== 'string') return CommonDefine.BlockWall;
|
||||
const v = value.trim();
|
||||
if (!v || GROUND_TILES.has(v)) return CommonDefine.BlockWall;
|
||||
if (v === CommonDefine.BlockWall || BORDER_TILES.has(v)) return v;
|
||||
return v;
|
||||
}
|
||||
|
||||
/** 按层级解析贴图 key(层级优先于裸值) */
|
||||
export function resolveTileNameForLayer(layer: 'ground' | 'border', value: unknown): string {
|
||||
return layer === 'ground' ? normalizeGroundTile(value) : normalizeBorderTile(value);
|
||||
}
|
||||
|
||||
export function resolveTileNameAtCell(
|
||||
layer: 'ground' | 'border',
|
||||
key: string,
|
||||
ground?: Record<string, string>,
|
||||
border?: Record<string, boolean | string>,
|
||||
): string {
|
||||
if (layer === 'ground') {
|
||||
return normalizeGroundTile(ground?.[key]);
|
||||
}
|
||||
return normalizeBorderTile(border?.[key]);
|
||||
}
|
||||
9
assets/scripts/level/TileKinds.ts.meta
Normal file
9
assets/scripts/level/TileKinds.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "a138ce1d-2313-4912-b184-cd59e9357e55",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
/**
|
||||
* Unity 2D 绘制排序(对齐主站 GraphicsSettings + 预制体 SortingOrder + TilemapRenderer Individual)
|
||||
* 等距绘制排序(重设计)
|
||||
*
|
||||
* - GraphicsSettings: TransparencySortMode=CustomAxis, axis=(0, 1, -0.26)
|
||||
* - TilemapRenderer: SortingOrder=2, Mode=Individual, SortOrder=TopRight → 每格 -(cellX+cellY)
|
||||
* - nProp=1, Tilemap/Vehicle=2, Prop=3, Player=4
|
||||
* - 仅贴图 WallBlock 可遮挡实体;可行走砖永在实体之下
|
||||
* - 砖↔砖:格子深度(南 y 小 → 前,同 y 东 x 大 → 前)→ JumpBlock 抬高 Y → Individual order
|
||||
* 单一全序标量,无成环、无特判互打:
|
||||
* frontKey = -(cellX + cellY) * CELL // 更小 x+y → 屏幕更靠下 → 更靠前
|
||||
* + typeRank // 同对角条带内:地板 < 金币 < 载具 < 角色 < 墙
|
||||
*
|
||||
* 移动中采样拆开:
|
||||
* - 角色:朝前落点 / 朝后起点
|
||||
* - 载具:朝前起点(防浮到前砖上)/ 朝后落点(防途中比落点更靠前而不被下方砖挡)
|
||||
* 贴墙南/西立面格上的实体:抬到该墙之上(露脚)。
|
||||
* 角色永远盖过同条带载具。
|
||||
*/
|
||||
|
||||
import { Vec3 } from 'cc';
|
||||
import { CommonDefine } from '../core/Define';
|
||||
import { cellToWorldCenter } from '../core/GridCoords';
|
||||
|
||||
/** Unity GraphicsSettings.asset m_TransparencySortAxis */
|
||||
export const UNITY_TRANSPARENCY_AXIS = { x: 0, y: 1, z: -0.26 } as const;
|
||||
|
||||
/** Unity SpriteRenderer / TilemapRenderer m_SortingOrder */
|
||||
export const UNITY_SORTING_ORDER = {
|
||||
nProp: 1,
|
||||
tilemap: 2,
|
||||
@@ -24,10 +26,13 @@ export const UNITY_SORTING_ORDER = {
|
||||
player: 4,
|
||||
} as const;
|
||||
|
||||
const ORDER_SCALE = 10_000;
|
||||
const DEPTH_SCALE = 100;
|
||||
/** JumpBlock 行走面比 Baseblock 抬高的最小 Y 差(px),超过则优先比 depthY */
|
||||
const TILE_ELEVATION_Y_THRESHOLD = 12;
|
||||
const CELL = 100;
|
||||
/** 同对角条带内层级:地板 < 金币 < 载具 < 角色 < 墙立面 */
|
||||
const RANK_FLOOR = 0;
|
||||
const RANK_PICKABLE = 20;
|
||||
const RANK_VEHICLE = 30;
|
||||
const RANK_PLAYER = 35;
|
||||
const RANK_WALL = 40;
|
||||
|
||||
export function isWallBlockTileName(tileName: string): boolean {
|
||||
return tileName === CommonDefine.BlockWall;
|
||||
@@ -37,7 +42,6 @@ export function isWalkableTileName(tileName: string): boolean {
|
||||
return tileName === CommonDefine.BlockBase || tileName === CommonDefine.BlockJump;
|
||||
}
|
||||
|
||||
/** Unity TilemapRenderer Individual + TopRight:baseOrder - (cellX+cellY) */
|
||||
export function unityTileIndividualSortingOrder(cellX: number, cellY: number): number {
|
||||
return UNITY_SORTING_ORDER.tilemap - cellX - cellY;
|
||||
}
|
||||
@@ -55,7 +59,7 @@ export function unityCompositeSortKey(
|
||||
bias = 0,
|
||||
): number {
|
||||
const depth = unityTransparencyDepth(0, depthWorldY, depthWorldZ);
|
||||
return sortingOrder * ORDER_SCALE + depth * DEPTH_SCALE + bias;
|
||||
return sortingOrder * 10_000 + depth * 100 + bias;
|
||||
}
|
||||
|
||||
export function unityCellCenterY(cellX: number, cellY: number): number {
|
||||
@@ -88,95 +92,95 @@ export function compareIsoDrawOrder(ax: number, ay: number, bx: number, by: numb
|
||||
return ax - bx;
|
||||
}
|
||||
|
||||
export function compareTileCellDepth(ax: number, ay: number, bx: number, by: number): number {
|
||||
return compareIsoDrawOrder(ax, ay, bx, by);
|
||||
}
|
||||
|
||||
function wallFaceSamples(wallX: number, wallY: number): { x: number; y: number }[] {
|
||||
return [{ x: wallX, y: wallY - 1 }, { x: wallX - 1, y: wallY }];
|
||||
}
|
||||
|
||||
function compareWallActorIso(
|
||||
actor: { x: number; y: number },
|
||||
wallX: number,
|
||||
wallY: number,
|
||||
): number {
|
||||
let actorAhead = 0;
|
||||
for (const face of wallFaceSamples(wallX, wallY)) {
|
||||
const iso = compareIsoDrawOrder(actor.x, actor.y, face.x, face.y);
|
||||
if (iso < 0) return iso;
|
||||
if (iso > actorAhead) actorAhead = iso;
|
||||
}
|
||||
return actorAhead;
|
||||
function typeRankForTile(tileName: string): number {
|
||||
return isWallBlockTileName(tileName) ? RANK_WALL : RANK_FLOOR;
|
||||
}
|
||||
|
||||
/** 返回值 > 0 实体更靠前;< 0 墙应挡住 */
|
||||
function compareUnityEntityToWall(
|
||||
function typeRankForEntity(entity: UnitySortableEntity): number {
|
||||
if (entity.isPickable) return RANK_PICKABLE;
|
||||
if (entity.isPlayer) return RANK_PLAYER;
|
||||
return RANK_VEHICLE;
|
||||
}
|
||||
|
||||
/** 越大越靠前(后绘制) */
|
||||
export function isoDrawFrontKey(cellX: number, cellY: number, typeRank: number): number {
|
||||
return -(cellX + cellY) * CELL + typeRank;
|
||||
}
|
||||
|
||||
export function tileDrawFrontKey(tile: UnitySortableTile): number {
|
||||
return isoDrawFrontKey(tile.cellX, tile.cellY, typeRankForTile(tile.tileName));
|
||||
}
|
||||
|
||||
export function entityDrawFrontKey(entity: UnitySortableEntity): number {
|
||||
return isoDrawFrontKey(entity.cellX, entity.cellY, typeRankForEntity(entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* 若实体踩在某墙南/西立面上,应露在该墙前面。
|
||||
* 返回需要超过的墙 frontKey,否则 null。
|
||||
*/
|
||||
export function entityFaceLiftOverWall(
|
||||
entity: UnitySortableEntity,
|
||||
tile: UnitySortableTile,
|
||||
): number {
|
||||
): number | null {
|
||||
if (!isWallBlockTileName(tile.tileName)) return null;
|
||||
const cell = { x: entity.cellX, y: entity.cellY };
|
||||
for (const face of wallFaceSamples(tile.cellX, tile.cellY)) {
|
||||
if (face.x === cell.x && face.y === cell.y) return 1;
|
||||
if (face.x === cell.x && face.y === cell.y) {
|
||||
return tileDrawFrontKey(tile);
|
||||
}
|
||||
}
|
||||
|
||||
const tileOrder = unityTileIndividualSortingOrder(tile.cellX, tile.cellY);
|
||||
if (tileOrder > entity.sortingOrder) return -1;
|
||||
|
||||
const wallIso = compareWallActorIso(cell, tile.cellX, tile.cellY);
|
||||
if (wallIso < 0) {
|
||||
if (tile.cellX === cell.x && tile.cellY === cell.y - 1) return -1;
|
||||
if (tile.cellY < cell.y) return 1;
|
||||
return -1;
|
||||
}
|
||||
|
||||
const tileAhead = (tile.cellX + tile.cellY) - (cell.x + cell.y);
|
||||
if (wallIso > 0 && tileAhead > 0) return -1;
|
||||
if (wallIso > 0) return wallIso;
|
||||
|
||||
const yCmp = Math.round(entity.depthY * DEPTH_SCALE) - Math.round(tile.depthY * DEPTH_SCALE);
|
||||
if (yCmp !== 0) return yCmp;
|
||||
if (tileAhead !== 0) return -tileAhead;
|
||||
return compareIsoDrawOrder(tile.cellX, tile.cellY, cell.x, cell.y);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 砖块格子深度:南侧(cellY 小)更靠前;同行西侧(cellX 小)更靠前 */
|
||||
export function compareTileCellDepth(ax: number, ay: number, bx: number, by: number): number {
|
||||
if (ay !== by) return by - ay;
|
||||
return bx - ax;
|
||||
/** 实体相对整张关卡的 frontKey(含贴墙抬升) */
|
||||
export function entityDrawFrontKeyWithWalls(
|
||||
entity: UnitySortableEntity,
|
||||
walls: UnitySortableTile[],
|
||||
): number {
|
||||
let key = entityDrawFrontKey(entity);
|
||||
for (const w of walls) {
|
||||
const lift = entityFaceLiftOverWall(entity, w);
|
||||
if (lift != null && key <= lift) key = lift + 1;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* 砖↔砖:格子深度优先(下方砖挡上方砖),JumpBlock 抬高再比 depthY。
|
||||
*/
|
||||
export function compareUnityTileToTile(a: UnitySortableTile, b: UnitySortableTile): number {
|
||||
const cellCmp = compareTileCellDepth(a.cellX, a.cellY, b.cellX, b.cellY);
|
||||
if (cellCmp !== 0) return cellCmp;
|
||||
const d = tileDrawFrontKey(a) - tileDrawFrontKey(b);
|
||||
if (d !== 0) return d;
|
||||
return a.cellX - b.cellX;
|
||||
}
|
||||
|
||||
const yCmp = Math.round(a.depthY * DEPTH_SCALE) - Math.round(b.depthY * DEPTH_SCALE);
|
||||
if (Math.abs(yCmp) >= TILE_ELEVATION_Y_THRESHOLD) return yCmp;
|
||||
|
||||
return unityTileIndividualSortingOrder(a.cellX, a.cellY)
|
||||
- unityTileIndividualSortingOrder(b.cellX, b.cellY);
|
||||
export function compareUnityEntityToEntity(a: UnitySortableEntity, b: UnitySortableEntity): number {
|
||||
const d = entityDrawFrontKey(a) - entityDrawFrontKey(b);
|
||||
if (d !== 0) return d;
|
||||
if (a.isPlayer && !b.isPlayer) return 1;
|
||||
if (b.isPlayer && !a.isPlayer) return -1;
|
||||
return a.sortingOrder - b.sortingOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体 vs 砖块
|
||||
* 非 WallBlock 永在实体之下;仅墙砖可遮挡。
|
||||
* 实体 vs 砖:统一用 frontKey。
|
||||
* (保留函数供外部/测试调用;排序主路径用 build 里的 key)
|
||||
*/
|
||||
export function compareUnityEntityToTile(
|
||||
entity: UnitySortableEntity,
|
||||
tile: UnitySortableTile,
|
||||
): number {
|
||||
if (!isWallBlockTileName(tile.tileName)) {
|
||||
// 地板永在实体下
|
||||
return 1;
|
||||
}
|
||||
return compareUnityEntityToWall(entity, tile);
|
||||
}
|
||||
|
||||
export function compareUnityEntityToEntity(a: UnitySortableEntity, b: UnitySortableEntity): number {
|
||||
const keyA = unityCompositeSortKey(a.sortingOrder, a.depthY);
|
||||
const keyB = unityCompositeSortKey(b.sortingOrder, b.depthY);
|
||||
if (keyA !== keyB) return keyA - keyB;
|
||||
const iso = (a.cellX + a.cellY) - (b.cellX + b.cellY);
|
||||
if (iso !== 0) return iso;
|
||||
if (a.isPlayer && !b.isPlayer) return 1;
|
||||
if (b.isPlayer && !a.isPlayer) return -1;
|
||||
return 0;
|
||||
const lift = entityFaceLiftOverWall(entity, tile);
|
||||
const eKey = lift != null ? lift + 1 : entityDrawFrontKey(entity);
|
||||
return eKey - tileDrawFrontKey(tile);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user