no message

This commit is contained in:
2026-07-06 09:43:46 +08:00
parent 4564ac5f47
commit ff1542c24f
22 changed files with 8771 additions and 309 deletions

View File

@@ -0,0 +1,222 @@
[
{
"__type__": "cc.Prefab",
"_name": "Level132536",
"_objFlags": 0,
"_native": "",
"data": {
"__id__": 1
},
"optimizationPolicy": 0,
"persistent": false
},
{
"__type__": "cc.Node",
"_name": "Level132536",
"_objFlags": 0,
"_parent": null,
"_children": [
{
"__id__": 3
},
{
"__id__": 2
}
],
"_active": true,
"_components": [
{
"__id__": 5
},
{
"__id__": 7
}
],
"_prefab": {
"__id__": 8
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "Border",
"_objFlags": 0,
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [],
"_prefab": {
"__id__": 9
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "Ground",
"_objFlags": 0,
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [],
"_prefab": {
"__id__": 10
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "jQr1xjY74TQBBFloLJzgfX"
},
{
"__type__": "d4e5fanuMlNDh8qO0xdbn+K",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 4
},
"levelID": 132536,
"groundJson": "{}",
"borderJson": "{}",
"theme": "silu",
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "VlPNbe6BOatO7GuURbErd5"
},
{
"__type__": "739b2brZKNN16CEy+hU2Yo2",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 6
},
"snapEnabled": true,
"showGrid": true,
"gridRadius": 12,
"gridPadding": 2,
"highlightOccupied": true,
"syncNodeNames": true,
"_id": ""
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "P1f6o97Mg65fECx9VNmCMH"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "feuQ6oITiZ0RBdaANNZoC3"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "Y9ZuF0pYPpFZ2UnT3iDVEv"
}
]

View File

@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "da966895-698a-4c63-b46f-159ea2d26660",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "Level132536"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
{ {
"132536": "da966895-698a-4c63-b46f-159ea2d26660",
"91601": "18396660-9a51-4503-8d42-c52d4bfb51e6", "91601": "18396660-9a51-4503-8d42-c52d4bfb51e6",
"91602": "6c0fe2aa-0b6f-4582-b3bf-f021d2ad32b2", "91602": "6c0fe2aa-0b6f-4582-b3bf-f021d2ad32b2",
"91603": "9157606c-1248-4878-b61b-d7d74030fdc6", "91603": "9157606c-1248-4878-b61b-d7d74030fdc6",

View File

@@ -1,6 +1,6 @@
{ {
"version": 2, "version": 2,
"generatedAt": "2026-06-15T14:45:03.186359+00:00", "generatedAt": "2026-06-30T07:29:45.699756+00:00",
"source": "Unity Levels91601.cs + prefab maps (91600-91609 import) | levelIdBase=91601 (Unity aligned)", "source": "Unity Levels91601.cs + prefab maps (91600-91609 import) | levelIdBase=91601 (Unity aligned)",
"levelIdBase": 91601, "levelIdBase": 91601,
"stats": { "stats": {
@@ -598,30 +598,30 @@
"-3,-2": "WallBlock", "-3,-2": "WallBlock",
"-3,-3": "WallBlock", "-3,-3": "WallBlock",
"-3,-4": "WallBlock", "-3,-4": "WallBlock",
"0,-2": "WallBlock",
"0,-3": "WallBlock", "0,-3": "WallBlock",
"0,-4": "WallBlock", "0,-4": "WallBlock",
"0,0": "WallBlock",
"0,1": "WallBlock",
"0,2": "WallBlock", "0,2": "WallBlock",
"1,-2": "WallBlock", "1,-2": "WallBlock",
"1,-3": "WallBlock", "1,-3": "WallBlock",
"1,-4": "WallBlock", "1,-4": "WallBlock",
"1,0": "WallBlock",
"1,1": "WallBlock",
"1,2": "WallBlock",
"2,-2": "WallBlock", "2,-2": "WallBlock",
"2,-3": "WallBlock", "2,-3": "WallBlock",
"2,-4": "WallBlock", "2,-4": "WallBlock",
"2,0": "WallBlock",
"2,1": "WallBlock",
"2,2": "WallBlock", "2,2": "WallBlock",
"3,-2": "WallBlock", "3,-2": "WallBlock",
"3,-3": "WallBlock", "3,-3": "WallBlock",
"3,-4": "WallBlock", "3,-4": "WallBlock",
"3,0": "WallBlock", "3,2": "WallBlock",
"1,2": "WallBlock",
"0,1": "WallBlock",
"0,0": "WallBlock",
"1,0": "WallBlock",
"1,1": "WallBlock",
"2,0": "WallBlock",
"2,1": "WallBlock",
"3,1": "WallBlock", "3,1": "WallBlock",
"3,2": "WallBlock" "3,0": "WallBlock",
"0,-2": "WallBlock"
}, },
"theme": "silu" "theme": "silu"
}, },
@@ -422356,7 +422356,10 @@
} }
], ],
"cocosPrefab": "level-prefabs/Level132536", "cocosPrefab": "level-prefabs/Level132536",
"unityPrefab": "Assets/Prefabs/Level/Level40936.prefab" "unityPrefab": "Assets/Prefabs/Level/Level132536.prefab",
"ground": {},
"border": {},
"theme": "silu"
} }
} }
} }

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "582480fc-2689-4db8-ad14-eb68b592174b",
"files": [],
"subMetas": {},
"userData": {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "a0722d2d-f054-4cf7-93f5-c6c65880e140",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "Level91614"
}
}

View File

@@ -19,7 +19,7 @@ import {
moverLogicalGridPositionForRole, moverLogicalGridPositionForRole,
worldToMoverCellForRole, worldToMoverCellForRole,
} from '../level/EntitySpawnPlacement'; } from '../level/EntitySpawnPlacement';
import { forEachLevelEntityNode } from '../level/TileLayout'; import { forEachLevelEntityNode, resolveLevelDrawRoot, sortIsoTiles } from '../level/TileLayout';
const { ccclass } = _decorator; const { ccclass } = _decorator;
@@ -583,6 +583,9 @@ export class PlayerController extends Movement {
externalCallResult(isWin: boolean) { externalCallResult(isWin: boolean) {
const gm = GameManager.instance!; const gm = GameManager.instance!;
if (gm.gameState === GameState.Run) {
gm.setGameState(isWin ? GameState.ResultWin : GameState.ResultFail);
}
if (this.node.name === 'Player') { if (this.node.name === 'Player') {
if (isWin) { if (isWin) {
GameAudio.playSfx('success', this.node); GameAudio.playSfx('success', this.node);
@@ -601,7 +604,7 @@ export class PlayerController extends Movement {
} satisfies ExternalResult)); } satisfies ExternalResult));
this.sendFinally = true; this.sendFinally = true;
} }
if (gm.gameState !== GameState.Run) return; const root = resolveLevelDrawRoot(this.node);
gm.setGameState(isWin ? GameState.ResultWin : GameState.ResultFail); if (root) sortIsoTiles(root);
} }
} }

View File

@@ -76,6 +76,8 @@ export const CommonDefine = {
Vehicle: 'Vehicle', Vehicle: 'Vehicle',
BlockBase: 'Baseblock', BlockBase: 'Baseblock',
BlockJump: 'JumpBlock', BlockJump: 'JumpBlock',
/** 墙砖贴图 keyBorder 默认瓦片;仅此类参与墙遮挡) */
BlockWall: 'WallBlock',
}; };
export function addDirection(dir: Direction, delta: number): Direction { export function addDirection(dir: Direction, delta: number): Direction {

View File

@@ -80,7 +80,7 @@ export function getHalfCellSize(): { halfW: number; halfH: number } {
return { halfW: HALF_W, halfH: HALF_H }; return { halfW: HALF_W, halfH: HALF_H };
} }
/** 等距绘制深度(连续值,移动插值用;与 compareIsoDrawOrder 配套 */ /** 等距绘制深度(连续值;关卡 sibling 排序已改用底边 Y + pivot */
export function isoDrawDepthFromWorld(world: Vec3): { x: number; y: number } { export function isoDrawDepthFromWorld(world: Vec3): { x: number; y: number } {
const cx = (world.y / HALF_H + world.x / HALF_W) * 0.5; const cx = (world.y / HALF_H + world.x / HALF_W) * 0.5;
const cy = (world.y / HALF_H - world.x / HALF_W) * 0.5; const cy = (world.y / HALF_H - world.x / HALF_W) * 0.5;

View File

@@ -3,6 +3,7 @@ import {
Direction, GameState, GridType, MoveState, MoverRole, addDirection, Direction, GameState, GridType, MoveState, MoverRole, addDirection,
} from '../core/Define'; } from '../core/Define';
import { GameManager } from '../manager/GameManager'; import { GameManager } from '../manager/GameManager';
import { markIsoDrawOrderDirty, resolveLevelDrawRoot } from '../level/TileLayout';
import { scaledMoveSpeed, UNITY_VEHICLE_MOVE_SPEED } from '../core/GridConstants'; import { scaledMoveSpeed, UNITY_VEHICLE_MOVE_SPEED } from '../core/GridConstants';
import { cellToWorldCenter } from '../core/GridCoords'; import { cellToWorldCenter } from '../core/GridCoords';
import { import {
@@ -199,6 +200,7 @@ export class Movement extends Component {
/** 主题 entityDisplay 变更后重算站立 Y缩放刷新不会自动更新节点坐标 */ /** 主题 entityDisplay 变更后重算站立 Y缩放刷新不会自动更新节点坐标 */
reapplyCellStandPosition() { reapplyCellStandPosition() {
this.snapMoverToCellStand(); this.snapMoverToCellStand();
this.markDrawOrderDirty();
} }
resetMoveRuntime() { resetMoveRuntime() {
@@ -216,15 +218,22 @@ export class Movement extends Component {
Vec3.moveTowards(next, pos, this.targetPosition, this.moveSpeed * dt * speedMul); Vec3.moveTowards(next, pos, this.targetPosition, this.moveSpeed * dt * speedMul);
this.node.setPosition(next); this.node.setPosition(next);
this.onMoving(); this.onMoving();
this.markDrawOrderDirty();
this.syncRideAfterMoveStep(); this.syncRideAfterMoveStep();
if (Vec3.distance(next, this.targetPosition) < 0.005) { if (Vec3.distance(next, this.targetPosition) < 0.005) {
this.node.setPosition(this.targetPosition); this.node.setPosition(this.targetPosition);
this.moveState = MoveState.Idle; this.moveState = MoveState.Idle;
this.moveWait = false; this.moveWait = false;
this.onMoveToTarget(); this.onMoveToTarget();
this.markDrawOrderDirty();
} }
} }
private markDrawOrderDirty() {
const root = resolveLevelDrawRoot(this.node);
if (root) markIsoDrawOrderDirty(root);
}
protected syncRideAfterMoveStep() {} protected syncRideAfterMoveStep() {}
setDirection(dir: Direction) { setDirection(dir: Direction) {

View File

@@ -16,16 +16,29 @@ function ensureLayer(root: Node, name: 'Ground' | 'Border'): Node {
return layer; return layer;
} }
function ensureTile(layer: Node, layerKind: 'ground' | 'border', x: number, y: number): Node { /** 砖块可能在 Tiles排序合并后优先 Tiles 避免在 Ground/Border 重复创建 */
function findTileNode(levelRoot: Node, layerKind: 'ground' | 'border', x: number, y: number): Node | null {
const nodeName = tileNodeName(layerKind, x, y); const nodeName = tileNodeName(layerKind, x, y);
let node = layer.getChildByName(nodeName); for (const parentName of ['Tiles', 'Ground', 'Border'] as const) {
if (!node) { const parent = levelRoot.getChildByName(parentName);
node = new Node(nodeName); const node = parent?.getChildByName(nodeName) ?? null;
if (node?.isValid) return node;
}
return null;
}
function ensureTile(levelRoot: Node, layer: Node, layerKind: 'ground' | 'border', x: number, y: number): Node {
const existing = findTileNode(levelRoot, layerKind, x, y);
if (existing) {
existing.active = true;
return existing;
}
const nodeName = tileNodeName(layerKind, x, y);
const node = new Node(nodeName);
node.parent = layer; node.parent = layer;
node.layer = UI_LAYER; node.layer = UI_LAYER;
node.addComponent(UITransform); node.addComponent(UITransform);
node.addComponent(Sprite); node.addComponent(Sprite);
}
node.active = true; node.active = true;
return node; return node;
} }
@@ -41,13 +54,13 @@ export function syncTileNodesFromConfig(levelRoot: Node, config: LevelConfig): n
for (const key of wantGround) { for (const key of wantGround) {
const p = parseCellKey(key); const p = parseCellKey(key);
if (!p) continue; if (!p) continue;
ensureTile(ground, 'ground', p.x, p.y); ensureTile(levelRoot, ground, 'ground', p.x, p.y);
n++; n++;
} }
for (const key of wantBorder) { for (const key of wantBorder) {
const p = parseCellKey(key); const p = parseCellKey(key);
if (!p) continue; if (!p) continue;
ensureTile(border, 'border', p.x, p.y); ensureTile(levelRoot, border, 'border', p.x, p.y);
n++; n++;
} }

View File

@@ -1,42 +1,52 @@
import { Node, UITransform, Vec3, Layers, Sprite } from 'cc'; import { _decorator, Component, Node, UITransform, Vec3, Layers, Sprite } from 'cc';
import { cellToWorldCenter, parseTileNodeName } from '../core/GridCoords'; import { cellToWorldCenter, parseTileNodeName } from '../core/GridCoords';
import { CommonDefine } from '../core/Define'; import { CommonDefine, MoverRole, GameState } from '../core/Define';
import { GameManager } from '../manager/GameManager'; import { GameManager } from '../manager/GameManager';
import { getTilePivot } from '../visual/TilePivots'; import { getTilePivot } from '../visual/TilePivots';
import { getTileDrawSize, resolveTilePixelSize } from '../visual/TileSizes'; import { getTileDrawSize, resolveTilePixelSize } from '../visual/TileSizes';
import { LevelConfig } from './LevelTypes'; import { LevelConfig } from './LevelTypes';
import { getLevelRuntimeContext } from './LevelRuntimeContext'; 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 {
compareUnityEntityToEntity,
compareUnityEntityToTile,
compareUnityTileToTile,
compareIsoDrawOrder,
UNITY_SORTING_ORDER,
type UnitySortableEntity,
type UnitySortableTile,
} from './UnityDrawSort';
const { ccclass } = _decorator;
const UI_LAYER = Layers.Enum.UI_2D; const UI_LAYER = Layers.Enum.UI_2D;
/** /**
* 遮挡分类(仅在关卡加载时按格子坐标排序一次) * Cocos 用 siblingIndex 模拟 Unity SortingOrder + CustomAxis 深度。
* - walkable : Baseblock / JumpBlock永不在 actor 之上 * 逻辑见 UnityDrawSort.ts对齐 GraphicsSettings + TilemapRenderer Individual
* - wall : Border / WallBlock与 actor 按 iso 深度比较
* - actor : 角色 / 载具
* - pickable : 可拾取物Prop永不在墙砖/路径砖之下;玩家仍在其上
* - scenery : 其它
*/ */
export const DRAW_SORT_Y_SCALE = 100;
type DrawKind = 'walkable' | 'wall' | 'actor' | 'pickable' | 'scenery'; type DrawKind = 'walkable' | 'wall' | 'actor' | 'pickable' | 'scenery';
const DRAW_RANK = { interface TileDrawEntry {
groundProp: 10, node: Node;
groundTile: 20, x: number;
borderTile: 22, y: number;
vehicle: 28, kind: DrawKind;
coin: 30, tileName: string;
propDecor: 30, }
player: 40,
} as const; interface EntityDrawEntry {
interface DrawEntry {
node: Node; node: Node;
x: number; x: number;
y: number; y: number;
rank: number;
kind: DrawKind; kind: DrawKind;
} }
type DrawEntry = TileDrawEntry | EntityDrawEntry;
function safeNodeName(node: Node | null | undefined): string { function safeNodeName(node: Node | null | undefined): string {
return (node?.isValid ? node.name : '') || ''; return (node?.isValid ? node.name : '') || '';
} }
@@ -80,21 +90,30 @@ function isWalkablePathTile(tileName: string): boolean {
} }
function isWallTileName(tileName: string): boolean { function isWallTileName(tileName: string): boolean {
return tileName === 'WallBlock' || tileName === 'kuai11'; return tileName === CommonDefine.BlockWall;
} }
function resolveGroundTileName(cellX: number, cellY: number, config?: LevelConfig): string { function resolveTileName(
return config?.ground?.[`${cellX},${cellY}`] ?? CommonDefine.BlockBase; parsed: { layer: 'ground' | 'border'; x: number; y: number },
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;
} }
function classifyTileKind( function classifyTileKind(
parsed: { layer: 'ground' | 'border'; x: number; y: number }, parsed: { layer: 'ground' | 'border'; x: number; y: number },
config?: LevelConfig, config?: LevelConfig,
): DrawKind { ): DrawKind {
if (parsed.layer === 'border') return 'wall'; const name = resolveTileName(parsed, config);
const name = resolveGroundTileName(parsed.x, parsed.y, config);
if (isWalkablePathTile(name)) return 'walkable';
if (isWallTileName(name)) return 'wall'; if (isWallTileName(name)) return 'wall';
if (isWalkablePathTile(name)) return 'walkable';
return 'scenery'; return 'scenery';
} }
@@ -109,69 +128,254 @@ export interface EntityDrawOrderOptions {
preferVehicleOverPlayer?: boolean; preferVehicleOverPlayer?: boolean;
} }
function entityRank(node: Node): number { function unityEntitySortingOrder(node: Node): number {
const n = safeNodeName(node); const n = safeNodeName(node);
if (isPlayerEntityName(n)) return DRAW_RANK.player; if (isPlayerEntityName(n)) return UNITY_SORTING_ORDER.player;
if (isVehicleEntityName(n)) return DRAW_RANK.vehicle; if (isVehicleEntityName(n)) return UNITY_SORTING_ORDER.vehicle;
if (isCoinEntityName(n)) return DRAW_RANK.coin; if (isPickableProp(node)) {
if (n === 'PropDecor' || n.startsWith('PropDecor_')) return DRAW_RANK.propDecor; const prop = node.getComponent('PropController') as { getSpawnCell?: () => Vec3 | null } | null;
return DRAW_RANK.groundProp; 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 tileRank(kind: DrawKind, layer: 'ground' | 'border'): number { function toUnitySortableTile(tile: TileDrawEntry): UnitySortableTile {
if (kind === 'walkable') return DRAW_RANK.groundTile; const centerY = tile.node.position.y;
if (kind === 'wall') return DRAW_RANK.borderTile; let depthY = getNodeBottomY(tile.node);
return layer === 'border' ? DRAW_RANK.borderTile : DRAW_RANK.groundTile; 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 之后(更靠前) */ /** 等距深度:返回值 > 0 表示 a 应排在 b 之后(更靠前) */
export function compareIsoDrawOrder(cellX: number, cellY: number, otherX: number, otherY: number): number { export { compareIsoDrawOrder };
const ka = cellX + cellY;
const kb = otherX + otherY; function buildSortedDrawOrder(tiles: TileDrawEntry[], entities: EntityDrawEntry[]): DrawEntry[] {
if (ka !== kb) return kb - ka; const all: DrawEntry[] = [...tiles, ...entities];
return otherX - cellX; 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 wallFaceSamples(wallX: number, wallY: number): { x: number; y: number }[] { /** 精灵顶边 YTiles 本地) */
return [{ x: wallX, y: wallY - 1 }, { x: wallX - 1, y: wallY }]; 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;
} }
function compareWallActorIso(actor: DrawEntry, wall: DrawEntry): number { /** 墙砖立面顶边 YWallBlock pivot 偏低,取格心与顶边较大值) */
let actorAhead = 0; function getWallSortFrontY(tile: TileDrawEntry): number {
for (const face of wallFaceSamples(wall.x, wall.y)) { const centerY = cellToWorldCenter(new Vec3(tile.x, tile.y, 0)).y;
const iso = compareIsoDrawOrder(actor.x, actor.y, face.x, face.y); return Math.max(centerY, getNodeTopY(tile.node));
if (iso < 0) return iso;
if (iso > actorAhead) actorAhead = iso;
}
return actorAhead;
} }
function compareDrawEntries(a: DrawEntry, b: DrawEntry): number { function resolveActorSortAnchorY(node: Node): number {
if (a.kind === 'walkable' && b.kind === 'actor') return -1; return isVehicleEntityName(safeNodeName(node)) ? DEFAULT_VEHICLE_ANCHOR_Y : DEFAULT_PLAYER_ANCHOR_Y;
if (b.kind === 'walkable' && a.kind === 'actor') return 1;
// 可拾取物:永远在墙砖/路径砖/载具之上;玩家仍在其上
if (a.kind === 'pickable' && b.kind === 'actor') {
return isPlayerEntityName(safeNodeName(b.node)) ? -1 : 1;
}
if (a.kind === 'actor' && b.kind === 'pickable') {
return isPlayerEntityName(safeNodeName(a.node)) ? 1 : -1;
}
if (a.kind === 'pickable' && b.kind !== 'pickable') return 1;
if (b.kind === 'pickable' && a.kind !== 'pickable') return -1;
if (a.kind === 'actor' && b.kind === 'wall') {
const wallIso = compareWallActorIso(a, b);
if (wallIso !== 0) return wallIso;
}
if (a.kind === 'wall' && b.kind === 'actor') {
const wallIso = compareWallActorIso(b, a);
if (wallIso !== 0) return -wallIso;
} }
const iso = compareIsoDrawOrder(a.x, a.y, b.x, b.y); function resolveActorSortDisplayHeight(node: Node, role: MoverRole, theme?: string): number {
if (iso !== 0) return iso; const ui = resolveSortTransform(node);
return a.rank - b.rank; 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) {
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);
for (const node of pending) {
if (node.parent === tilesRoot) continue;
node.getWorldPosition(worldPos);
node.parent = tilesRoot;
node.setWorldPosition(worldPos);
}
}
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();
}
}
}
}
function getActorWallSampleCell(entity: EntityDrawEntry, node: Node): { x: number; y: number } {
const mov = node.getComponent(Movement);
if (!mov) return { x: entity.x, y: entity.y };
const committed = mov.getCommittedCell();
if (!mov.isMoving()) {
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 };
}
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 };
}
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 之间插值(忽略跳跃弧与序列帧 anchor 抖动)。
*/
function getEntitySortBottomY(node: Node): number {
if (!isActorEntity(node)) return getNodeBottomY(node);
const mov = 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 容器本身) */ /** 实体所在关卡根Ground/Border/Tiles 的父节点,不是 Tiles 容器本身) */
@@ -187,7 +391,7 @@ export function resolveLevelDrawRoot(from: Node): Node | null {
return null; return null;
} }
/** 实体逻辑格(spawn / committed,移动中不重算 */ /** 实体逻辑格spawn / committed;实体互排序用,与砖块比较用底边 Y */
function getEntityLogicCell(node: Node): { x: number; y: number } { function getEntityLogicCell(node: Node): { x: number; y: number } {
const propCtrl = node.getComponent('PropController') as { getSpawnCell?: () => Vec3 | null } | null; const propCtrl = node.getComponent('PropController') as { getSpawnCell?: () => Vec3 | null } | null;
const propCell = propCtrl?.getSpawnCell?.(); const propCell = propCtrl?.getSpawnCell?.();
@@ -205,10 +409,7 @@ function getEntityLogicCell(node: Node): { x: number; y: number } {
if (riderCell) return { x: riderCell.x, y: riderCell.y }; if (riderCell) return { x: riderCell.x, y: riderCell.y };
} }
const mov = node.getComponent('Movement') as { const mov = node.getComponent(Movement);
getCommittedCell?: () => Vec3 | null;
getSpawnCell?: () => Vec3 | null;
} | null;
if (mov) { if (mov) {
const cell = mov.getCommittedCell?.() ?? mov.getSpawnCell?.(); const cell = mov.getCommittedCell?.() ?? mov.getSpawnCell?.();
if (cell) return { x: cell.x, y: cell.y }; if (cell) return { x: cell.x, y: cell.y };
@@ -248,19 +449,20 @@ function resolveLevelConfig(): LevelConfig | undefined {
?? undefined; ?? undefined;
} }
function collectTileEntries(levelRoot: Node, tilesRoot: Node, seen: Set<Node>): DrawEntry[] { function collectTileEntries(levelRoot: Node, tilesRoot: Node, seen: Set<Node>): TileDrawEntry[] {
const config = resolveLevelConfig(); const config = resolveLevelConfig();
const entries: DrawEntry[] = []; const entries: TileDrawEntry[] = [];
const pushTile = (node: Node, parsed: { layer: 'ground' | 'border'; x: number; y: number }) => { const pushTile = (node: Node, parsed: { layer: 'ground' | 'border'; x: number; y: number }) => {
if (seen.has(node)) return; if (seen.has(node)) return;
seen.add(node); seen.add(node);
const tileName = resolveTileName(parsed, config);
const kind = classifyTileKind(parsed, config); const kind = classifyTileKind(parsed, config);
entries.push({ entries.push({
node, node,
x: parsed.x, x: parsed.x,
y: parsed.y, y: parsed.y,
rank: tileRank(kind, parsed.layer),
kind, kind,
tileName,
}); });
}; };
@@ -273,8 +475,8 @@ function collectTileEntries(levelRoot: Node, tilesRoot: Node, seen: Set<Node>):
return entries; return entries;
} }
function collectEntityEntries(levelRoot: Node, tilesRoot: Node, seen: Set<Node>): DrawEntry[] { function collectEntityEntries(levelRoot: Node, tilesRoot: Node, seen: Set<Node>): EntityDrawEntry[] {
const entries: DrawEntry[] = []; const entries: EntityDrawEntry[] = [];
const pushEntity = (node: Node | null | undefined) => { const pushEntity = (node: Node | null | undefined) => {
if (!node?.isValid || seen.has(node) || !isEntityDrawNode(node)) return; if (!node?.isValid || seen.has(node) || !isEntityDrawNode(node)) return;
seen.add(node); seen.add(node);
@@ -283,64 +485,86 @@ function collectEntityEntries(levelRoot: Node, tilesRoot: Node, seen: Set<Node>)
node, node,
x: cell.x, x: cell.x,
y: cell.y, y: cell.y,
rank: entityRank(node),
kind: classifyEntityKind(node), kind: classifyEntityKind(node),
}); });
}; };
for (const ch of levelRoot.children) pushEntity(ch); const walkEntities = (parent: Node) => {
for (const ch of tilesRoot.children) pushEntity(ch); 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; return entries;
} }
function applyDrawOrder(tilesRoot: Node, entries: DrawEntry[]) { function applyDrawOrder(tilesRoot: Node, entries: DrawEntry[]) {
const sorted = entries.slice().sort(compareDrawEntries);
const inSort = new Set<Node>(); const inSort = new Set<Node>();
const worldPos = new Vec3();
for (const { node } of sorted) { for (const { node } of entries) {
if (!node.isValid) continue; if (!node.isValid) continue;
inSort.add(node); inSort.add(node);
if (isEntityDrawNode(node)) ensureEntityUILayer(node); if (isEntityDrawNode(node)) ensureEntityUILayer(node);
if (node.parent !== tilesRoot) { if (node.parent !== tilesRoot) {
const lp = node.position.clone(); node.getWorldPosition(worldPos);
node.parent = tilesRoot; node.parent = tilesRoot;
node.setPosition(lp); node.setWorldPosition(worldPos);
} }
} }
for (let i = 0; i < sorted.length; i++) { // 未参与排序的节点保持在最底层,避免盖住角色
const node = sorted[i].node; let head = 0;
if (!node.isValid) continue;
if (node.getSiblingIndex() !== i) node.setSiblingIndex(i);
}
let tail = sorted.length;
for (const ch of [...tilesRoot.children]) { for (const ch of [...tilesRoot.children]) {
if (!ch.isValid || inSort.has(ch)) continue; if (!ch.isValid || inSort.has(ch)) continue;
if (ch.getSiblingIndex() !== tail) ch.setSiblingIndex(tail); if (ch.getSiblingIndex() !== head) ch.setSiblingIndex(head);
tail++; head++;
}
// 从高索引往低索引写入,避免 setSiblingIndex 互相覆盖
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 finalizeLevelRootOrder(levelRoot: Node, tilesRoot: Node) { function finalizeLevelRootOrder(levelRoot: Node, tilesRoot: Node) {
for (const name of ['Ground', 'Border'] as const) { for (const name of ['Ground', 'Border'] as const) {
const layer = levelRoot.getChildByName(name); const layer = levelRoot.getChildByName(name);
if (layer && layer.children.length === 0) { if (!layer) continue;
layer.active = layer.children.length > 0;
if (layer.children.length === 0) {
layer.setSiblingIndex(0); layer.setSiblingIndex(0);
} }
} }
tilesRoot.setSiblingIndex(levelRoot.children.length - 1); tilesRoot.setSiblingIndex(levelRoot.children.length - 1);
tilesRoot.active = true;
} }
/** /**
* 关卡加载 / 重置时按格子坐标排序一次;移动过程中不再改 sibling * 砖块按格子深度排序,人物/载具/金币按底边 Y 插入砖块序列;移动中可重排
*/ */
export function sortIsoTiles(levelRoot: Node) { export function sortIsoTiles(levelRoot: Node) {
if (!levelRoot?.isValid) return; if (!levelRoot?.isValid) return;
ensureDrawOrderUpdater(levelRoot);
const tilesRoot = ensureTilesRoot(levelRoot); const tilesRoot = ensureTilesRoot(levelRoot);
pullDrawablesIntoTiles(levelRoot, tilesRoot);
dedupeLayerTileDuplicates(levelRoot, tilesRoot);
const seen = new Set<Node>(); const seen = new Set<Node>();
const tileEntries = collectTileEntries(levelRoot, tilesRoot, seen); const tileEntries = collectTileEntries(levelRoot, tilesRoot, seen);
const entityEntries = collectEntityEntries(levelRoot, tilesRoot, seen); const entityEntries = collectEntityEntries(levelRoot, tilesRoot, seen);
applyDrawOrder(tilesRoot, [...tileEntries, ...entityEntries]); applyDrawOrder(tilesRoot, buildSortedDrawOrder(tileEntries, entityEntries));
finalizeLevelRootOrder(levelRoot, tilesRoot); finalizeLevelRootOrder(levelRoot, tilesRoot);
} }
@@ -383,7 +607,7 @@ function ensureTilesRoot(levelRoot: Node): Node {
return tilesRoot; return tilesRoot;
} }
const TILE_LAYER_NAMES = ['Ground', 'Border', 'Tiles'] as const; 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) { function forEachTileChild(levelRoot: Node, fn: (node: Node, parsed: { layer: 'ground' | 'border'; x: number; y: number }) => void) {
for (const layerName of TILE_LAYER_NAMES) { for (const layerName of TILE_LAYER_NAMES) {
@@ -429,9 +653,9 @@ function tileNameFromConfig(
return config.ground?.[key] ?? CommonDefine.BlockBase; return config.ground?.[key] ?? CommonDefine.BlockBase;
} }
let v = config.border?.[key]; let v = config.border?.[key];
if (v === true || v === undefined) return 'WallBlock'; if (v === true || v === undefined) return CommonDefine.BlockWall;
if (typeof v === 'string') return v; if (typeof v === 'string') return v;
return 'WallBlock'; return CommonDefine.BlockWall;
} }
export function layoutLevelTiles(levelRoot: Node, config: LevelConfig) { export function layoutLevelTiles(levelRoot: Node, config: LevelConfig) {
@@ -470,3 +694,26 @@ export function layoutLevelTiles(levelRoot: Node, config: LevelConfig) {
} }
sortIsoTiles(levelRoot); sortIsoTiles(levelRoot);
} }
@ccclass('IsoDrawOrderUpdater')
class IsoDrawOrderUpdater extends Component {
/** 每帧 lateUpdate 重排(先移动、后排序,与 Update→LateUpdate 一致) */
lateUpdate() {
if (!this.node?.isValid) return;
const st = GameManager.instance?.gameState;
if (st === GameState.ResultWin || st === GameState.ResultFail) return;
sortIsoTiles(this.node);
}
}
function ensureDrawOrderUpdater(levelRoot: Node) {
if (!levelRoot.getComponent(IsoDrawOrderUpdater)) {
levelRoot.addComponent(IsoDrawOrderUpdater);
}
}
/** @deprecated 关卡加载后 Updater 每帧自动重排;保留 API 兼容 */
export function markIsoDrawOrderDirty(levelRoot: Node) {
if (!levelRoot?.isValid) return;
ensureDrawOrderUpdater(levelRoot);
}

View File

@@ -0,0 +1,182 @@
/**
* 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
*/
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,
vehicle: 2,
prop: 3,
player: 4,
} as const;
const ORDER_SCALE = 10_000;
const DEPTH_SCALE = 100;
/** JumpBlock 行走面比 Baseblock 抬高的最小 Y 差px超过则优先比 depthY */
const TILE_ELEVATION_Y_THRESHOLD = 12;
export function isWallBlockTileName(tileName: string): boolean {
return tileName === CommonDefine.BlockWall;
}
export function isWalkableTileName(tileName: string): boolean {
return tileName === CommonDefine.BlockBase || tileName === CommonDefine.BlockJump;
}
/** Unity TilemapRenderer Individual + TopRightbaseOrder - (cellX+cellY) */
export function unityTileIndividualSortingOrder(cellX: number, cellY: number): number {
return UNITY_SORTING_ORDER.tilemap - cellX - cellY;
}
export function unityTransparencyDepth(worldX: number, worldY: number, worldZ = 0): number {
return worldX * UNITY_TRANSPARENCY_AXIS.x
+ worldY * UNITY_TRANSPARENCY_AXIS.y
+ worldZ * UNITY_TRANSPARENCY_AXIS.z;
}
export function unityCompositeSortKey(
sortingOrder: number,
depthWorldY: number,
depthWorldZ = 0,
bias = 0,
): number {
const depth = unityTransparencyDepth(0, depthWorldY, depthWorldZ);
return sortingOrder * ORDER_SCALE + depth * DEPTH_SCALE + bias;
}
export function unityCellCenterY(cellX: number, cellY: number): number {
return cellToWorldCenter(new Vec3(cellX, cellY, 0)).y;
}
export interface UnitySortableTile {
cellX: number;
cellY: number;
tileName: string;
centerY: number;
depthY: number;
}
export interface UnitySortableEntity {
sortingOrder: number;
cellX: number;
cellY: number;
centerY: number;
depthY: number;
isPlayer: boolean;
isPickable: boolean;
}
/** 返回值 > 0 表示 (ax,ay) 应排在 (bx,by) 之后(更靠前) */
export function compareIsoDrawOrder(ax: number, ay: number, bx: number, by: number): number {
const ka = ax + ay;
const kb = bx + by;
if (ka !== kb) return kb - ka;
return ax - bx;
}
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;
}
/** 返回值 > 0 实体更靠前;< 0 墙应挡住 */
function compareUnityEntityToWall(
entity: UnitySortableEntity,
tile: UnitySortableTile,
): number {
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;
}
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);
}
/** 砖块格子深度南侧cellY 小更靠前同行西侧cellX 小)更靠前 */
export function compareTileCellDepth(ax: number, ay: number, bx: number, by: number): number {
if (ay !== by) return by - ay;
return bx - ax;
}
/**
* 砖↔砖格子深度优先下方砖挡上方砖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 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);
}
/**
* 实体 vs 砖块
* 非 WallBlock 永在实体之下;仅墙砖可遮挡。
*/
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;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "150bb469-a447-44ff-b4c8-3c02cd2fb949",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -10,6 +10,7 @@ import {
import { VisualAssets } from './VisualAssets'; import { VisualAssets } from './VisualAssets';
import { GameManager } from '../manager/GameManager'; import { GameManager } from '../manager/GameManager';
import { Movement } from '../gameplay/Movement'; import { Movement } from '../gameplay/Movement';
import { resolveLevelDrawRoot, sortIsoTiles } from '../level/TileLayout';
import { resolveThemeId } from '../theme/ThemeDatabase'; import { resolveThemeId } from '../theme/ThemeDatabase';
import { import {
PlayerAction, PlayerAnimPaths, actionFolder, resolvePlayerAnimPaths, PlayerAction, PlayerAnimPaths, actionFolder, resolvePlayerAnimPaths,
@@ -205,6 +206,7 @@ export class PlayerActionAnimator extends Component {
setAction(action: PlayerAction, force = false) { setAction(action: PlayerAction, force = false) {
if (!force && this.action === action && this.useSequence && this.frames.length > 0) return; if (!force && this.action === action && this.useSequence && this.frames.length > 0) return;
const actionChanged = this.action !== action;
this.action = action; this.action = action;
this.frameIdx = 0; this.frameIdx = 0;
this.frameTimer = 0; this.frameTimer = 0;
@@ -212,9 +214,12 @@ export class PlayerActionAnimator extends Component {
VisualAssets.applyPlayerSprite( VisualAssets.applyPlayerSprite(
this.spriteNode, this.direction, { theme: this.theme }, this.scaleMul, this.spriteNode, this.direction, { theme: this.theme }, this.scaleMul,
); );
if (actionChanged && (action === PlayerAction.Win || action === PlayerAction.Fail)) {
this.refreshResultDrawOrder();
}
return; return;
} }
void this.applyActionFrames(action); void this.applyActionFrames(action, actionChanged);
} }
getAction(): PlayerAction { getAction(): PlayerAction {
@@ -279,7 +284,12 @@ export class PlayerActionAnimator extends Component {
return this.displayLockPromise; return this.displayLockPromise;
} }
private async applyActionFrames(action: PlayerAction) { private refreshResultDrawOrder() {
const root = resolveLevelDrawRoot(this.node);
if (root) sortIsoTiles(root);
}
private async applyActionFrames(action: PlayerAction, sortOnLoad = false) {
if (!this.animPaths) return; if (!this.animPaths) return;
const gen = ++this.loadingGen; const gen = ++this.loadingGen;
const isFront = this.direction === Direction.South || this.direction === Direction.East; const isFront = this.direction === Direction.South || this.direction === Direction.East;
@@ -305,6 +315,9 @@ export class PlayerActionAnimator extends Component {
this.frameIdx = 0; this.frameIdx = 0;
this.frameTimer = 0; this.frameTimer = 0;
this.showFrame(frames[0]!); this.showFrame(frames[0]!);
if (sortOnLoad && (action === PlayerAction.Win || action === PlayerAction.Fail)) {
this.refreshResultDrawOrder();
}
} }
private showFrame(sf: SpriteFrame) { private showFrame(sf: SpriteFrame) {

View File

@@ -70,8 +70,7 @@ exports.template = `
<ui-button id="btn-next">下一关</ui-button> <ui-button id="btn-next">下一关</ui-button>
<ui-button id="btn-load">加载</ui-button> <ui-button id="btn-load">加载</ui-button>
<ui-button id="btn-new">新建</ui-button> <ui-button id="btn-new">新建</ui-button>
<ui-button id="btn-save" type="primary">保存 JSON</ui-button> <ui-button id="btn-save" type="primary">保存地图</ui-button>
<ui-button id="btn-bake">烘焙预制体</ui-button>
<ui-button id="btn-import-prefab">从预制体同步</ui-button> <ui-button id="btn-import-prefab">从预制体同步</ui-button>
<ui-button id="btn-open-prefab">打开 Level 预制体</ui-button> <ui-button id="btn-open-prefab">打开 Level 预制体</ui-button>
<ui-button id="btn-scene-snap">场景选中吸附</ui-button> <ui-button id="btn-scene-snap">场景选中吸附</ui-button>
@@ -118,7 +117,7 @@ exports.template = `
<div class="lme-canvas-wrap"> <div class="lme-canvas-wrap">
<canvas id="lme-canvas" width="720" height="520"></canvas> <canvas id="lme-canvas" width="720" height="520"></canvas>
</div> </div>
<div class="lme-status" id="status">地图/出生点编辑 · 自动保存 JSON 并烘焙 · 60×60 网格 · 滚轮缩放 · 长按右键拖动</div> <div class="lme-status" id="status">地图/出生点编辑 · 绘制后自动暂存 · 点「保存地图」写入 JSON 并烘焙预制体 · 滚轮缩放 · 长按右键拖动</div>
</div> </div>
<div class="lme-palette"> <div class="lme-palette">
<h4 id="palette-title">平铺调色板</h4> <h4 id="palette-title">平铺调色板</h4>
@@ -135,7 +134,6 @@ exports.$ = {
btnLoad: '#btn-load', btnLoad: '#btn-load',
btnNew: '#btn-new', btnNew: '#btn-new',
btnSave: '#btn-save', btnSave: '#btn-save',
btnBake: '#btn-bake',
btnImportPrefab: '#btn-import-prefab', btnImportPrefab: '#btn-import-prefab',
btnOpenPrefab: '#btn-open-prefab', btnOpenPrefab: '#btn-open-prefab',
btnSceneSnap: '#btn-scene-snap', btnSceneSnap: '#btn-scene-snap',
@@ -561,11 +559,12 @@ exports.ready = function () {
persistMapChanges({ quiet: true, skipBake: true }); persistMapChanges({ quiet: true, skipBake: true });
try { try {
bakeIgnore.markBakePending(state.levelId); bakeIgnore.markBakePending(state.levelId);
const outDir = prefabSync.resolvePrefabDirRel(projectPath());
execSync( execSync(
`python3 tools/bake_cocos_level_prefabs.py --db ${DB_REL} --out-dir assets/resources/level-prefabs --level-id ${state.levelId} --theme ${state.theme}`, `python3 tools/bake_cocos_level_prefabs.py --db ${DB_REL} --out-dir ${outDir} --level-id ${state.levelId} --theme ${state.theme}`,
{ cwd: projectPath(), stdio: 'pipe' }, { cwd: projectPath(), stdio: 'pipe' },
); );
const prefabUrl = `db://assets/resources/level-prefabs/Level${state.levelId}.prefab`; const prefabUrl = prefabSync.prefabDbUrl(state.levelId, projectPath());
try { try {
Editor.Message.request('asset-db', 'refresh-asset', prefabUrl); Editor.Message.request('asset-db', 'refresh-asset', prefabUrl);
} catch (e) { } catch (e) {
@@ -574,7 +573,8 @@ exports.ready = function () {
if (options.auto) { if (options.auto) {
setStatus(`关卡 ${state.levelId} 已自动保存并烘焙 → Level${state.levelId}.prefab`); setStatus(`关卡 ${state.levelId} 已自动保存并烘焙 → Level${state.levelId}.prefab`);
} else { } else {
setStatus(`关卡 ${state.levelId} 已烘焙(主题 ${state.theme})→ level-prefabs/Level${state.levelId}.prefab`); const outDir = prefabSync.resolvePrefabDirRel(projectPath());
setStatus(`关卡 ${state.levelId} 已保存地图 → JSON + ${outDir}/Level${state.levelId}.prefab`);
} }
console.log(`[level-map-editor] baked Level${state.levelId}.prefab`); console.log(`[level-map-editor] baked Level${state.levelId}.prefab`);
} catch (e) { } catch (e) {
@@ -587,25 +587,25 @@ exports.ready = function () {
} }
}; };
const saveLevel = () => { /** 保存 JSON + 烘焙预制体(运行时分包 bundle-level-prefabs */
const saveMapLevel = () => {
cancelAutoBake(); cancelAutoBake();
persistMapChanges({ quiet: true, skipBake: true }); if (!state.config || !state.db) {
setStatus(`已保存关卡 ${state.levelId}(主题 ${state.theme}`); setStatus('请先加载或新建关卡');
return;
}
if (!persistMapChanges({ quiet: true, skipBake: true })) return;
runBake.call(this, { auto: false });
}; };
/** 每次改地图立即写 JSON */ /** 绘制时暂存 JSON延迟自动烘焙 */
const scheduleMapPersist = () => { const scheduleMapPersist = () => {
persistMapChanges(); persistMapChanges();
}; };
const bakeLevel = () => {
cancelAutoBake();
runBake.call(this, { auto: false });
};
const openLevelPrefab = async () => { const openLevelPrefab = async () => {
const id = state.levelId; const id = state.levelId;
const uuid = await Editor.Message.request('asset-db', 'query-uuid', `db://assets/resources/level-prefabs/Level${id}.prefab`); const uuid = await Editor.Message.request('asset-db', 'query-uuid', prefabSync.prefabDbUrl(id, projectPath()));
if (uuid) { if (uuid) {
await Editor.Message.request('asset-db', 'open-asset', uuid); await Editor.Message.request('asset-db', 'open-asset', uuid);
setStatus(`已打开 Level${id}.prefabGround / Border 子节点,贴图主题 ${state.theme}`); setStatus(`已打开 Level${id}.prefabGround / Border 子节点,贴图主题 ${state.theme}`);
@@ -952,8 +952,7 @@ exports.ready = function () {
} }
}); });
bindOnce(this.$.btnNew, 'click', () => createNewLevel.call(this)); bindOnce(this.$.btnNew, 'click', () => createNewLevel.call(this));
bindOnce(this.$.btnSave, 'click', () => saveLevel.call(this)); bindOnce(this.$.btnSave, 'click', () => saveMapLevel.call(this));
bindOnce(this.$.btnBake, 'click', () => bakeLevel.call(this));
bindOnce(this.$.btnImportPrefab, 'click', () => importFromPrefab.call(this)); bindOnce(this.$.btnImportPrefab, 'click', () => importFromPrefab.call(this));
bindOnce(this.$.btnOpenPrefab, 'click', () => openLevelPrefab.call(this)); bindOnce(this.$.btnOpenPrefab, 'click', () => openLevelPrefab.call(this));
@@ -991,7 +990,7 @@ exports.ready = function () {
const prefabUuid = await Editor.Message.request( const prefabUuid = await Editor.Message.request(
'asset-db', 'asset-db',
'query-uuid', 'query-uuid',
`db://assets/resources/level-prefabs/Level${state.levelId}.prefab`, prefabSync.prefabDbUrl(state.levelId, projectPath()),
); );
if (prefabUuid) uuids = [prefabUuid]; if (prefabUuid) uuids = [prefabUuid];
} }

View File

@@ -6,7 +6,10 @@ const levelIdUtil = require('./level-id');
const LEVEL_MAP_TYPE = 'd4e5fanuMlNDh8qO0xdbn+K'; const LEVEL_MAP_TYPE = 'd4e5fanuMlNDh8qO0xdbn+K';
const DB_REL = 'assets/level-data/levels-database.json'; const DB_REL = 'assets/level-data/levels-database.json';
const PREFAB_DIR_REL = 'assets/resources/level-prefabs'; const PREFAB_BUNDLE_REL = 'assets/bundle-level-prefabs/level-prefabs';
const PREFAB_LEGACY_REL = 'assets/resources/level-prefabs';
/** @deprecated 使用 resolvePrefabDirRel */
const PREFAB_DIR_REL = PREFAB_LEGACY_REL;
const PREFAB_NAME_RE = /^Level(\d+)\.prefab$/i; const PREFAB_NAME_RE = /^Level(\d+)\.prefab$/i;
const TILE_NODE_RE = /^([gb])_(-?\d+)_(-?\d+)$/; const TILE_NODE_RE = /^([gb])_(-?\d+)_(-?\d+)$/;
@@ -20,8 +23,18 @@ function dbPath(projectPath) {
return path.join(projectRoot(projectPath), DB_REL); return path.join(projectRoot(projectPath), DB_REL);
} }
function resolvePrefabDirRel(projectPath) {
const root = projectRoot(projectPath);
if (fs.existsSync(path.join(root, PREFAB_BUNDLE_REL))) return PREFAB_BUNDLE_REL;
return PREFAB_LEGACY_REL;
}
function prefabDir(projectPath) { function prefabDir(projectPath) {
return path.join(projectRoot(projectPath), PREFAB_DIR_REL); return path.join(projectRoot(projectPath), resolvePrefabDirRel(projectPath));
}
function prefabDbUrl(levelId, projectPath) {
return `db://${resolvePrefabDirRel(projectPath)}/Level${levelId}.prefab`;
} }
function prefabPath(levelId, projectPath) { function prefabPath(levelId, projectPath) {
@@ -258,6 +271,10 @@ function extractLevelIdFromFilename(name) {
module.exports = { module.exports = {
DB_REL, DB_REL,
PREFAB_DIR_REL, PREFAB_DIR_REL,
PREFAB_BUNDLE_REL,
PREFAB_LEGACY_REL,
resolvePrefabDirRel,
prefabDbUrl,
prefabPath, prefabPath,
dbPath, dbPath,
parsePrefabFile, parsePrefabFile,

View File

@@ -1,7 +1,7 @@
{ {
"package_version": 2, "package_version": 2,
"name": "level-map-editor", "name": "level-map-editor",
"version": "1.7.1", "version": "1.7.2",
"description": "关卡地图编辑(对齐 Unity Tile Palette中间关卡、右侧地图模块", "description": "关卡地图编辑(对齐 Unity Tile Palette中间关卡、右侧地图模块",
"author": "tfrh", "author": "tfrh",
"editor": ">=3.8.0", "editor": ">=3.8.0",

View File

@@ -1,4 +1,5 @@
{ {
"132536": "level-prefabs/Level132536",
"91601": "level-prefabs/Level91601", "91601": "level-prefabs/Level91601",
"91602": "level-prefabs/Level91602", "91602": "level-prefabs/Level91602",
"91603": "level-prefabs/Level91603", "91603": "level-prefabs/Level91603",

View File

@@ -0,0 +1,167 @@
#!/usr/bin/env node
/**
* 自测 Unity 绘制排序(与 UnityDrawSort.ts 对齐)
* 运行: node tools/verify-unity-draw-sort.mjs
*/
const UNITY_SORTING_ORDER = { nProp: 1, tilemap: 2, vehicle: 2, prop: 3, player: 4 };
const ORDER_SCALE = 10_000;
const DEPTH_SCALE = 100;
const TILE_ELEVATION_Y_THRESHOLD = 12;
const HALF_W = 50;
const HALF_H = 25;
function cellToWorldCenter(x, y) {
const wx = (x - y) * HALF_W;
const wy = (x + y) * HALF_H + HALF_H;
return { x: wx, y: wy };
}
function unityTileIndividualSortingOrder(cellX, cellY) {
return UNITY_SORTING_ORDER.tilemap - cellX - cellY;
}
function compareTileCellDepth(ax, ay, bx, by) {
if (ay !== by) return by - ay;
return bx - ax;
}
function compareUnityTileToTile(a, b) {
const cellCmp = compareTileCellDepth(a.cellX, a.cellY, b.cellX, b.cellY);
if (cellCmp !== 0) return cellCmp;
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);
}
function tileInFrontOfTile(front, back, depthFront = 0, depthBack = 0) {
const a = { cellX: front.x, cellY: front.y, depthY: depthFront };
const b = { cellX: back.x, cellY: back.y, depthY: depthBack };
return compareUnityTileToTile(a, b) > 0;
}
function compareIsoDrawOrder(ax, ay, bx, by) {
const ka = ax + ay;
const kb = bx + by;
if (ka !== kb) return kb - ka;
return ax - bx;
}
function wallFaceSamples(wallX, wallY) {
return [{ x: wallX, y: wallY - 1 }, { x: wallX - 1, y: wallY }];
}
function compareWallActorIso(actor, wallX, wallY) {
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 compareUnityEntityToWall(entity, tile) {
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;
}
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);
}
function compareUnityEntityToTile(entity, tile) {
if (tile.tileName !== 'WallBlock') return 1;
return compareUnityEntityToWall(entity, tile);
}
function entityInFrontOfTile(entity, tile) {
return compareUnityEntityToTile(entity, tile) > 0;
}
function walkableUnderPlayer(playerCell, tileCell) {
return entityInFrontOfTile(
{ sortingOrder: 4, cellX: playerCell.x, cellY: playerCell.y, centerY: 0, depthY: 0 },
{ cellX: tileCell.x, cellY: tileCell.y, tileName: 'Baseblock', centerY: 0, depthY: 0 },
);
}
function playerBottomY(cellX, cellY) {
return cellToWorldCenter(cellX, cellY).y - 0.35 * 90;
}
function wallFrontY(cellX, cellY) {
const c = cellToWorldCenter(cellX, cellY);
return Math.max(c.y, c.y + (1 - 0.67) * 115);
}
const cases = [
{
name: '砖↔砖:斜向路径 (0,-2)→(0,-1)→(0,0)→(1,0) 下方逐格遮挡',
pass: tileInFrontOfTile({ x: 0, y: -2 }, { x: 0, y: -1 })
&& tileInFrontOfTile({ x: 0, y: -1 }, { x: 0, y: 0 })
&& tileInFrontOfTile({ x: 0, y: 0 }, { x: 1, y: 0 }),
},
{
name: '砖↔砖:对角路径 (0,-3)→(1,-2)→(2,-1)→(3,0) 下方逐格遮挡',
pass: tileInFrontOfTile({ x: 0, y: -3 }, { x: 1, y: -2 })
&& tileInFrontOfTile({ x: 1, y: -2 }, { x: 2, y: -1 })
&& tileInFrontOfTile({ x: 2, y: -1 }, { x: 3, y: 0 }),
},
{
name: '砖↔砖:南侧 (0,-1) 挡住北侧 (0,0)',
pass: tileInFrontOfTile({ x: 0, y: -1 }, { x: 0, y: 0 }),
},
{
name: '砖↔砖:东南 (2,-2) 挡住西北 (0,-1)',
pass: tileInFrontOfTile({ x: 2, y: -2 }, { x: 0, y: -1 }),
},
{
name: '91614 出生 (0,-1) 不被北侧墙 (0,0) 挡住',
pass: entityInFrontOfTile(
{ sortingOrder: 4, cellX: 0, cellY: -1, centerY: 0, depthY: 0 },
{ cellX: 0, cellY: 0, tileName: 'WallBlock', centerY: 0, depthY: 0 },
),
},
{
name: '跨格路径砖:南侧 (0,-2) 不挡北侧角色 (0,-1)',
pass: entityInFrontOfTile(
{ sortingOrder: 4, cellX: 0, cellY: -1, centerY: 0, depthY: 0 },
{ cellX: 0, cellY: -2, tileName: 'Baseblock', centerY: 0, depthY: 100 },
),
},
{
name: '91614 真实Y出生 (0,-1) 在 (0,0) 墙前',
pass: entityInFrontOfTile(
{ sortingOrder: 4, cellX: 0, cellY: -1, centerY: 0, depthY: playerBottomY(0, -1) },
{ cellX: 0, cellY: 0, tileName: 'WallBlock', centerY: 0, depthY: wallFrontY(0, 0) },
),
},
];
let failed = 0;
for (const c of cases) {
const ok = !!c.pass;
console.log(`${ok ? '✓' : '✗'} ${c.name}`);
if (!ok) failed++;
}
if (failed > 0) {
console.error(`\n${failed} case(s) failed`);
process.exit(1);
}
console.log(`\nAll ${cases.length} cases passed.`);