no message
This commit is contained in:
222
assets/bundle-level-prefabs/level-prefabs/Level132536.prefab
Normal file
222
assets/bundle-level-prefabs/level-prefabs/Level132536.prefab
Normal 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"
|
||||
}
|
||||
]
|
||||
@@ -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
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"132536": "da966895-698a-4c63-b46f-159ea2d26660",
|
||||
"91601": "18396660-9a51-4503-8d42-c52d4bfb51e6",
|
||||
"91602": "6c0fe2aa-0b6f-4582-b3bf-f021d2ad32b2",
|
||||
"91603": "9157606c-1248-4878-b61b-d7d74030fdc6",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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)",
|
||||
"levelIdBase": 91601,
|
||||
"stats": {
|
||||
@@ -598,30 +598,30 @@
|
||||
"-3,-2": "WallBlock",
|
||||
"-3,-3": "WallBlock",
|
||||
"-3,-4": "WallBlock",
|
||||
"0,-2": "WallBlock",
|
||||
"0,-3": "WallBlock",
|
||||
"0,-4": "WallBlock",
|
||||
"0,0": "WallBlock",
|
||||
"0,1": "WallBlock",
|
||||
"0,2": "WallBlock",
|
||||
"1,-2": "WallBlock",
|
||||
"1,-3": "WallBlock",
|
||||
"1,-4": "WallBlock",
|
||||
"1,0": "WallBlock",
|
||||
"1,1": "WallBlock",
|
||||
"1,2": "WallBlock",
|
||||
"2,-2": "WallBlock",
|
||||
"2,-3": "WallBlock",
|
||||
"2,-4": "WallBlock",
|
||||
"2,0": "WallBlock",
|
||||
"2,1": "WallBlock",
|
||||
"2,2": "WallBlock",
|
||||
"3,-2": "WallBlock",
|
||||
"3,-3": "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,2": "WallBlock"
|
||||
"3,0": "WallBlock",
|
||||
"0,-2": "WallBlock"
|
||||
},
|
||||
"theme": "silu"
|
||||
},
|
||||
@@ -422356,7 +422356,10 @@
|
||||
}
|
||||
],
|
||||
"cocosPrefab": "level-prefabs/Level132536",
|
||||
"unityPrefab": "Assets/Prefabs/Level/Level40936.prefab"
|
||||
"unityPrefab": "Assets/Prefabs/Level/Level132536.prefab",
|
||||
"ground": {},
|
||||
"border": {},
|
||||
"theme": "silu"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
assets/resources/level-prefabs.meta
Normal file
9
assets/resources/level-prefabs.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "582480fc-2689-4db8-ad14-eb68b592174b",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
7539
assets/resources/level-prefabs/Level91614.prefab
Normal file
7539
assets/resources/level-prefabs/Level91614.prefab
Normal file
File diff suppressed because it is too large
Load Diff
13
assets/resources/level-prefabs/Level91614.prefab.meta
Normal file
13
assets/resources/level-prefabs/Level91614.prefab.meta
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
moverLogicalGridPositionForRole,
|
||||
worldToMoverCellForRole,
|
||||
} from '../level/EntitySpawnPlacement';
|
||||
import { forEachLevelEntityNode } from '../level/TileLayout';
|
||||
import { forEachLevelEntityNode, resolveLevelDrawRoot, sortIsoTiles } from '../level/TileLayout';
|
||||
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
@@ -583,6 +583,9 @@ export class PlayerController extends Movement {
|
||||
|
||||
externalCallResult(isWin: boolean) {
|
||||
const gm = GameManager.instance!;
|
||||
if (gm.gameState === GameState.Run) {
|
||||
gm.setGameState(isWin ? GameState.ResultWin : GameState.ResultFail);
|
||||
}
|
||||
if (this.node.name === 'Player') {
|
||||
if (isWin) {
|
||||
GameAudio.playSfx('success', this.node);
|
||||
@@ -601,7 +604,7 @@ export class PlayerController extends Movement {
|
||||
} satisfies ExternalResult));
|
||||
this.sendFinally = true;
|
||||
}
|
||||
if (gm.gameState !== GameState.Run) return;
|
||||
gm.setGameState(isWin ? GameState.ResultWin : GameState.ResultFail);
|
||||
const root = resolveLevelDrawRoot(this.node);
|
||||
if (root) sortIsoTiles(root);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,8 @@ export const CommonDefine = {
|
||||
Vehicle: 'Vehicle',
|
||||
BlockBase: 'Baseblock',
|
||||
BlockJump: 'JumpBlock',
|
||||
/** 墙砖贴图 key(Border 默认瓦片;仅此类参与墙遮挡) */
|
||||
BlockWall: 'WallBlock',
|
||||
};
|
||||
|
||||
export function addDirection(dir: Direction, delta: number): Direction {
|
||||
|
||||
@@ -80,7 +80,7 @@ export function getHalfCellSize(): { halfW: number; halfH: number } {
|
||||
return { halfW: HALF_W, halfH: HALF_H };
|
||||
}
|
||||
|
||||
/** 等距绘制深度(连续值,移动插值用;与 compareIsoDrawOrder 配套) */
|
||||
/** 等距绘制深度(连续值;关卡 sibling 排序已改用底边 Y + pivot) */
|
||||
export function isoDrawDepthFromWorld(world: Vec3): { x: number; y: number } {
|
||||
const cx = (world.y / HALF_H + world.x / HALF_W) * 0.5;
|
||||
const cy = (world.y / HALF_H - world.x / HALF_W) * 0.5;
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Direction, GameState, GridType, MoveState, MoverRole, addDirection,
|
||||
} from '../core/Define';
|
||||
import { GameManager } from '../manager/GameManager';
|
||||
import { markIsoDrawOrderDirty, resolveLevelDrawRoot } from '../level/TileLayout';
|
||||
import { scaledMoveSpeed, UNITY_VEHICLE_MOVE_SPEED } from '../core/GridConstants';
|
||||
import { cellToWorldCenter } from '../core/GridCoords';
|
||||
import {
|
||||
@@ -199,6 +200,7 @@ export class Movement extends Component {
|
||||
/** 主题 entityDisplay 变更后重算站立 Y(缩放刷新不会自动更新节点坐标) */
|
||||
reapplyCellStandPosition() {
|
||||
this.snapMoverToCellStand();
|
||||
this.markDrawOrderDirty();
|
||||
}
|
||||
|
||||
resetMoveRuntime() {
|
||||
@@ -216,15 +218,22 @@ export class Movement extends Component {
|
||||
Vec3.moveTowards(next, pos, this.targetPosition, this.moveSpeed * dt * speedMul);
|
||||
this.node.setPosition(next);
|
||||
this.onMoving();
|
||||
this.markDrawOrderDirty();
|
||||
this.syncRideAfterMoveStep();
|
||||
if (Vec3.distance(next, this.targetPosition) < 0.005) {
|
||||
this.node.setPosition(this.targetPosition);
|
||||
this.moveState = MoveState.Idle;
|
||||
this.moveWait = false;
|
||||
this.onMoveToTarget();
|
||||
this.markDrawOrderDirty();
|
||||
}
|
||||
}
|
||||
|
||||
private markDrawOrderDirty() {
|
||||
const root = resolveLevelDrawRoot(this.node);
|
||||
if (root) markIsoDrawOrderDirty(root);
|
||||
}
|
||||
|
||||
protected syncRideAfterMoveStep() {}
|
||||
|
||||
setDirection(dir: Direction) {
|
||||
|
||||
@@ -16,16 +16,29 @@ function ensureLayer(root: Node, name: 'Ground' | 'Border'): Node {
|
||||
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);
|
||||
let node = layer.getChildByName(nodeName);
|
||||
if (!node) {
|
||||
node = new Node(nodeName);
|
||||
node.parent = layer;
|
||||
node.layer = UI_LAYER;
|
||||
node.addComponent(UITransform);
|
||||
node.addComponent(Sprite);
|
||||
for (const parentName of ['Tiles', 'Ground', 'Border'] as const) {
|
||||
const parent = levelRoot.getChildByName(parentName);
|
||||
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.layer = UI_LAYER;
|
||||
node.addComponent(UITransform);
|
||||
node.addComponent(Sprite);
|
||||
node.active = true;
|
||||
return node;
|
||||
}
|
||||
@@ -41,13 +54,13 @@ export function syncTileNodesFromConfig(levelRoot: Node, config: LevelConfig): n
|
||||
for (const key of wantGround) {
|
||||
const p = parseCellKey(key);
|
||||
if (!p) continue;
|
||||
ensureTile(ground, 'ground', p.x, p.y);
|
||||
ensureTile(levelRoot, ground, 'ground', p.x, p.y);
|
||||
n++;
|
||||
}
|
||||
for (const key of wantBorder) {
|
||||
const p = parseCellKey(key);
|
||||
if (!p) continue;
|
||||
ensureTile(border, 'border', p.x, p.y);
|
||||
ensureTile(levelRoot, border, 'border', p.x, p.y);
|
||||
n++;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 { CommonDefine } from '../core/Define';
|
||||
import { CommonDefine, MoverRole, GameState } from '../core/Define';
|
||||
import { GameManager } from '../manager/GameManager';
|
||||
import { getTilePivot } from '../visual/TilePivots';
|
||||
import { getTileDrawSize, resolveTilePixelSize } from '../visual/TileSizes';
|
||||
import { LevelConfig } from './LevelTypes';
|
||||
import { getLevelRuntimeContext } from './LevelRuntimeContext';
|
||||
import { 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;
|
||||
|
||||
/**
|
||||
* 遮挡分类(仅在关卡加载时按格子坐标排序一次)
|
||||
* - walkable : Baseblock / JumpBlock,永不在 actor 之上
|
||||
* - wall : Border / WallBlock,与 actor 按 iso 深度比较
|
||||
* - actor : 角色 / 载具
|
||||
* - pickable : 可拾取物(Prop),永不在墙砖/路径砖之下;玩家仍在其上
|
||||
* - scenery : 其它
|
||||
* Cocos 用 siblingIndex 模拟 Unity SortingOrder + CustomAxis 深度。
|
||||
* 逻辑见 UnityDrawSort.ts(对齐 GraphicsSettings + TilemapRenderer Individual)。
|
||||
*/
|
||||
export const DRAW_SORT_Y_SCALE = 100;
|
||||
type DrawKind = 'walkable' | 'wall' | 'actor' | 'pickable' | 'scenery';
|
||||
|
||||
const DRAW_RANK = {
|
||||
groundProp: 10,
|
||||
groundTile: 20,
|
||||
borderTile: 22,
|
||||
vehicle: 28,
|
||||
coin: 30,
|
||||
propDecor: 30,
|
||||
player: 40,
|
||||
} as const;
|
||||
|
||||
interface DrawEntry {
|
||||
interface TileDrawEntry {
|
||||
node: Node;
|
||||
x: number;
|
||||
y: number;
|
||||
kind: DrawKind;
|
||||
tileName: string;
|
||||
}
|
||||
|
||||
interface EntityDrawEntry {
|
||||
node: Node;
|
||||
x: number;
|
||||
y: number;
|
||||
rank: number;
|
||||
kind: DrawKind;
|
||||
}
|
||||
|
||||
type DrawEntry = TileDrawEntry | EntityDrawEntry;
|
||||
|
||||
function safeNodeName(node: Node | null | undefined): string {
|
||||
return (node?.isValid ? node.name : '') || '';
|
||||
}
|
||||
@@ -80,21 +90,30 @@ function isWalkablePathTile(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 {
|
||||
return config?.ground?.[`${cellX},${cellY}`] ?? CommonDefine.BlockBase;
|
||||
function resolveTileName(
|
||||
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(
|
||||
parsed: { layer: 'ground' | 'border'; x: number; y: number },
|
||||
config?: LevelConfig,
|
||||
): DrawKind {
|
||||
if (parsed.layer === 'border') return 'wall';
|
||||
const name = resolveGroundTileName(parsed.x, parsed.y, config);
|
||||
if (isWalkablePathTile(name)) return 'walkable';
|
||||
const name = resolveTileName(parsed, config);
|
||||
if (isWallTileName(name)) return 'wall';
|
||||
if (isWalkablePathTile(name)) return 'walkable';
|
||||
return 'scenery';
|
||||
}
|
||||
|
||||
@@ -109,69 +128,254 @@ export interface EntityDrawOrderOptions {
|
||||
preferVehicleOverPlayer?: boolean;
|
||||
}
|
||||
|
||||
function entityRank(node: Node): number {
|
||||
function unityEntitySortingOrder(node: Node): number {
|
||||
const n = safeNodeName(node);
|
||||
if (isPlayerEntityName(n)) return DRAW_RANK.player;
|
||||
if (isVehicleEntityName(n)) return DRAW_RANK.vehicle;
|
||||
if (isCoinEntityName(n)) return DRAW_RANK.coin;
|
||||
if (n === 'PropDecor' || n.startsWith('PropDecor_')) return DRAW_RANK.propDecor;
|
||||
return DRAW_RANK.groundProp;
|
||||
if (isPlayerEntityName(n)) return UNITY_SORTING_ORDER.player;
|
||||
if (isVehicleEntityName(n)) return UNITY_SORTING_ORDER.vehicle;
|
||||
if (isPickableProp(node)) {
|
||||
const prop = node.getComponent('PropController') as { getSpawnCell?: () => Vec3 | null } | null;
|
||||
const cell = prop?.getSpawnCell?.();
|
||||
const config = resolveLevelConfig();
|
||||
if (cell && !cellHasTile(cell.x, cell.y, config)) {
|
||||
return UNITY_SORTING_ORDER.nProp;
|
||||
}
|
||||
return UNITY_SORTING_ORDER.prop;
|
||||
}
|
||||
if (n === 'PropDecor' || n.startsWith('PropDecor_')) return UNITY_SORTING_ORDER.prop;
|
||||
return UNITY_SORTING_ORDER.nProp;
|
||||
}
|
||||
|
||||
function tileRank(kind: DrawKind, layer: 'ground' | 'border'): number {
|
||||
if (kind === 'walkable') return DRAW_RANK.groundTile;
|
||||
if (kind === 'wall') return DRAW_RANK.borderTile;
|
||||
return layer === 'border' ? DRAW_RANK.borderTile : DRAW_RANK.groundTile;
|
||||
function toUnitySortableTile(tile: TileDrawEntry): UnitySortableTile {
|
||||
const centerY = tile.node.position.y;
|
||||
let depthY = getNodeBottomY(tile.node);
|
||||
if (isWallTileName(tile.tileName)) {
|
||||
depthY = getWallSortFrontY(tile);
|
||||
} else if (isWalkablePathTile(tile.tileName)) {
|
||||
depthY = Math.max(depthY, centerY);
|
||||
} else {
|
||||
depthY = centerY;
|
||||
}
|
||||
return {
|
||||
cellX: tile.x,
|
||||
cellY: tile.y,
|
||||
tileName: tile.tileName,
|
||||
centerY,
|
||||
depthY,
|
||||
};
|
||||
}
|
||||
|
||||
function toUnitySortableEntity(entity: EntityDrawEntry): UnitySortableEntity {
|
||||
const node = entity.node;
|
||||
const cell = entity.kind === 'actor'
|
||||
? getActorWallSampleCell(entity, node)
|
||||
: { x: entity.x, y: entity.y };
|
||||
const depthY = entity.kind === 'actor'
|
||||
? getEntitySortBottomY(node)
|
||||
: getNodeBottomY(node);
|
||||
return {
|
||||
sortingOrder: unityEntitySortingOrder(node),
|
||||
cellX: cell.x,
|
||||
cellY: cell.y,
|
||||
centerY: node.position.y,
|
||||
depthY,
|
||||
isPlayer: isPlayerEntityName(safeNodeName(node)),
|
||||
isPickable: entity.kind === 'pickable',
|
||||
};
|
||||
}
|
||||
|
||||
/** 等距深度:返回值 > 0 表示 a 应排在 b 之后(更靠前) */
|
||||
export function compareIsoDrawOrder(cellX: number, cellY: number, otherX: number, otherY: number): number {
|
||||
const ka = cellX + cellY;
|
||||
const kb = otherX + otherY;
|
||||
if (ka !== kb) return kb - ka;
|
||||
return otherX - cellX;
|
||||
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 wallFaceSamples(wallX: number, wallY: number): { x: number; y: number }[] {
|
||||
return [{ x: wallX, y: wallY - 1 }, { x: wallX - 1, y: wallY }];
|
||||
/** 精灵顶边 Y(Tiles 本地) */
|
||||
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 {
|
||||
let actorAhead = 0;
|
||||
for (const face of wallFaceSamples(wall.x, wall.y)) {
|
||||
const iso = compareIsoDrawOrder(actor.x, actor.y, face.x, face.y);
|
||||
if (iso < 0) return iso;
|
||||
if (iso > actorAhead) actorAhead = iso;
|
||||
}
|
||||
return actorAhead;
|
||||
/** 墙砖立面顶边 Y(WallBlock pivot 偏低,取格心与顶边较大值) */
|
||||
function getWallSortFrontY(tile: TileDrawEntry): number {
|
||||
const centerY = cellToWorldCenter(new Vec3(tile.x, tile.y, 0)).y;
|
||||
return Math.max(centerY, getNodeTopY(tile.node));
|
||||
}
|
||||
|
||||
function compareDrawEntries(a: DrawEntry, b: DrawEntry): number {
|
||||
if (a.kind === 'walkable' && b.kind === 'actor') return -1;
|
||||
if (b.kind === 'walkable' && a.kind === 'actor') return 1;
|
||||
function resolveActorSortAnchorY(node: Node): number {
|
||||
return isVehicleEntityName(safeNodeName(node)) ? DEFAULT_VEHICLE_ANCHOR_Y : DEFAULT_PLAYER_ANCHOR_Y;
|
||||
}
|
||||
|
||||
// 可拾取物:永远在墙砖/路径砖/载具之上;玩家仍在其上
|
||||
if (a.kind === 'pickable' && b.kind === 'actor') {
|
||||
return isPlayerEntityName(safeNodeName(b.node)) ? -1 : 1;
|
||||
function resolveActorSortDisplayHeight(node: Node, role: MoverRole, theme?: string): number {
|
||||
const ui = resolveSortTransform(node);
|
||||
if (ui && ui.contentSize.height > 0) {
|
||||
return ui.contentSize.height * Math.abs(node.scale.y);
|
||||
}
|
||||
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;
|
||||
const sizes = getEntityDisplaySizes(theme);
|
||||
return role === 'vehicle' ? sizes.vehicle.height : sizes.player.height;
|
||||
}
|
||||
|
||||
if (a.kind === 'actor' && b.kind === 'wall') {
|
||||
const wallIso = compareWallActorIso(a, b);
|
||||
if (wallIso !== 0) return wallIso;
|
||||
/** 逻辑格站立点 + 固定参考锚点 → 排序底边 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);
|
||||
}
|
||||
if (a.kind === 'wall' && b.kind === 'actor') {
|
||||
const wallIso = compareWallActorIso(b, a);
|
||||
if (wallIso !== 0) return -wallIso;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/** 足底中心 Y(Tiles 本地);排序依据底边而非锚点/中心 */
|
||||
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;
|
||||
}
|
||||
|
||||
/** 画家算法 Order:round(底边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 iso = compareIsoDrawOrder(a.x, a.y, b.x, b.y);
|
||||
if (iso !== 0) return iso;
|
||||
return a.rank - b.rank;
|
||||
const cell = mov?.getCommittedCell() ?? mov?.getSpawnCell();
|
||||
if (!cell) return getNodeBottomY(node);
|
||||
return getActorStandBottomY(cell, node, role, config, theme);
|
||||
}
|
||||
|
||||
/** 实体所在关卡根(Ground/Border/Tiles 的父节点,不是 Tiles 容器本身) */
|
||||
@@ -187,7 +391,7 @@ export function resolveLevelDrawRoot(from: Node): Node | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 实体逻辑格(仅 spawn / committed,移动中不重算) */
|
||||
/** 实体逻辑格(spawn / committed;实体互排序用,与砖块比较用底边 Y) */
|
||||
function getEntityLogicCell(node: Node): { x: number; y: number } {
|
||||
const propCtrl = node.getComponent('PropController') as { getSpawnCell?: () => Vec3 | null } | null;
|
||||
const propCell = propCtrl?.getSpawnCell?.();
|
||||
@@ -205,10 +409,7 @@ function getEntityLogicCell(node: Node): { x: number; y: number } {
|
||||
if (riderCell) return { x: riderCell.x, y: riderCell.y };
|
||||
}
|
||||
|
||||
const mov = node.getComponent('Movement') as {
|
||||
getCommittedCell?: () => Vec3 | null;
|
||||
getSpawnCell?: () => Vec3 | null;
|
||||
} | null;
|
||||
const mov = node.getComponent(Movement);
|
||||
if (mov) {
|
||||
const cell = mov.getCommittedCell?.() ?? mov.getSpawnCell?.();
|
||||
if (cell) return { x: cell.x, y: cell.y };
|
||||
@@ -248,19 +449,20 @@ function resolveLevelConfig(): LevelConfig | 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 entries: DrawEntry[] = [];
|
||||
const entries: TileDrawEntry[] = [];
|
||||
const pushTile = (node: Node, parsed: { layer: 'ground' | 'border'; x: number; y: number }) => {
|
||||
if (seen.has(node)) return;
|
||||
seen.add(node);
|
||||
const tileName = resolveTileName(parsed, config);
|
||||
const kind = classifyTileKind(parsed, config);
|
||||
entries.push({
|
||||
node,
|
||||
x: parsed.x,
|
||||
y: parsed.y,
|
||||
rank: tileRank(kind, parsed.layer),
|
||||
kind,
|
||||
tileName,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -273,8 +475,8 @@ function collectTileEntries(levelRoot: Node, tilesRoot: Node, seen: Set<Node>):
|
||||
return entries;
|
||||
}
|
||||
|
||||
function collectEntityEntries(levelRoot: Node, tilesRoot: Node, seen: Set<Node>): DrawEntry[] {
|
||||
const entries: DrawEntry[] = [];
|
||||
function collectEntityEntries(levelRoot: Node, tilesRoot: Node, seen: Set<Node>): EntityDrawEntry[] {
|
||||
const entries: EntityDrawEntry[] = [];
|
||||
const pushEntity = (node: Node | null | undefined) => {
|
||||
if (!node?.isValid || seen.has(node) || !isEntityDrawNode(node)) return;
|
||||
seen.add(node);
|
||||
@@ -283,64 +485,86 @@ function collectEntityEntries(levelRoot: Node, tilesRoot: Node, seen: Set<Node>)
|
||||
node,
|
||||
x: cell.x,
|
||||
y: cell.y,
|
||||
rank: entityRank(node),
|
||||
kind: classifyEntityKind(node),
|
||||
});
|
||||
};
|
||||
for (const ch of levelRoot.children) pushEntity(ch);
|
||||
for (const ch of tilesRoot.children) pushEntity(ch);
|
||||
const walkEntities = (parent: Node) => {
|
||||
for (const ch of parent.children) {
|
||||
if (!ch?.isValid) continue;
|
||||
if (isEntityDrawNode(ch)) {
|
||||
pushEntity(ch);
|
||||
continue;
|
||||
}
|
||||
if (ch.name === 'Ground' || ch.name === 'Border' || ch.name === 'Tiles') {
|
||||
walkEntities(ch);
|
||||
}
|
||||
}
|
||||
};
|
||||
walkEntities(levelRoot);
|
||||
return entries;
|
||||
}
|
||||
|
||||
function applyDrawOrder(tilesRoot: Node, entries: DrawEntry[]) {
|
||||
const sorted = entries.slice().sort(compareDrawEntries);
|
||||
const inSort = new Set<Node>();
|
||||
const worldPos = new Vec3();
|
||||
|
||||
for (const { node } of sorted) {
|
||||
for (const { node } of entries) {
|
||||
if (!node.isValid) continue;
|
||||
inSort.add(node);
|
||||
if (isEntityDrawNode(node)) ensureEntityUILayer(node);
|
||||
if (node.parent !== tilesRoot) {
|
||||
const lp = node.position.clone();
|
||||
node.getWorldPosition(worldPos);
|
||||
node.parent = tilesRoot;
|
||||
node.setPosition(lp);
|
||||
node.setWorldPosition(worldPos);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
const node = sorted[i].node;
|
||||
if (!node.isValid) continue;
|
||||
if (node.getSiblingIndex() !== i) node.setSiblingIndex(i);
|
||||
}
|
||||
|
||||
let tail = sorted.length;
|
||||
// 未参与排序的节点保持在最底层,避免盖住角色
|
||||
let head = 0;
|
||||
for (const ch of [...tilesRoot.children]) {
|
||||
if (!ch.isValid || inSort.has(ch)) continue;
|
||||
if (ch.getSiblingIndex() !== tail) ch.setSiblingIndex(tail);
|
||||
tail++;
|
||||
if (ch.getSiblingIndex() !== head) ch.setSiblingIndex(head);
|
||||
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) {
|
||||
for (const name of ['Ground', 'Border'] as const) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
tilesRoot.setSiblingIndex(levelRoot.children.length - 1);
|
||||
tilesRoot.active = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关卡加载 / 重置时按格子坐标排序一次;移动过程中不再改 sibling。
|
||||
* 砖块按格子深度排序,人物/载具/金币按底边 Y 插入砖块序列;移动中可重排。
|
||||
*/
|
||||
export function sortIsoTiles(levelRoot: Node) {
|
||||
if (!levelRoot?.isValid) return;
|
||||
ensureDrawOrderUpdater(levelRoot);
|
||||
const tilesRoot = ensureTilesRoot(levelRoot);
|
||||
pullDrawablesIntoTiles(levelRoot, tilesRoot);
|
||||
dedupeLayerTileDuplicates(levelRoot, tilesRoot);
|
||||
const seen = new Set<Node>();
|
||||
const tileEntries = collectTileEntries(levelRoot, tilesRoot, seen);
|
||||
const entityEntries = collectEntityEntries(levelRoot, tilesRoot, seen);
|
||||
applyDrawOrder(tilesRoot, [...tileEntries, ...entityEntries]);
|
||||
applyDrawOrder(tilesRoot, buildSortedDrawOrder(tileEntries, entityEntries));
|
||||
finalizeLevelRootOrder(levelRoot, tilesRoot);
|
||||
}
|
||||
|
||||
@@ -383,7 +607,7 @@ function ensureTilesRoot(levelRoot: Node): Node {
|
||||
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) {
|
||||
for (const layerName of TILE_LAYER_NAMES) {
|
||||
@@ -429,9 +653,9 @@ function tileNameFromConfig(
|
||||
return config.ground?.[key] ?? CommonDefine.BlockBase;
|
||||
}
|
||||
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;
|
||||
return 'WallBlock';
|
||||
return CommonDefine.BlockWall;
|
||||
}
|
||||
|
||||
export function layoutLevelTiles(levelRoot: Node, config: LevelConfig) {
|
||||
@@ -470,3 +694,26 @@ export function layoutLevelTiles(levelRoot: Node, config: LevelConfig) {
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
182
assets/scripts/level/UnityDrawSort.ts
Normal file
182
assets/scripts/level/UnityDrawSort.ts
Normal 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 + TopRight:baseOrder - (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;
|
||||
}
|
||||
9
assets/scripts/level/UnityDrawSort.ts.meta
Normal file
9
assets/scripts/level/UnityDrawSort.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "150bb469-a447-44ff-b4c8-3c02cd2fb949",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { VisualAssets } from './VisualAssets';
|
||||
import { GameManager } from '../manager/GameManager';
|
||||
import { Movement } from '../gameplay/Movement';
|
||||
import { resolveLevelDrawRoot, sortIsoTiles } from '../level/TileLayout';
|
||||
import { resolveThemeId } from '../theme/ThemeDatabase';
|
||||
import {
|
||||
PlayerAction, PlayerAnimPaths, actionFolder, resolvePlayerAnimPaths,
|
||||
@@ -205,6 +206,7 @@ export class PlayerActionAnimator extends Component {
|
||||
|
||||
setAction(action: PlayerAction, force = false) {
|
||||
if (!force && this.action === action && this.useSequence && this.frames.length > 0) return;
|
||||
const actionChanged = this.action !== action;
|
||||
this.action = action;
|
||||
this.frameIdx = 0;
|
||||
this.frameTimer = 0;
|
||||
@@ -212,9 +214,12 @@ export class PlayerActionAnimator extends Component {
|
||||
VisualAssets.applyPlayerSprite(
|
||||
this.spriteNode, this.direction, { theme: this.theme }, this.scaleMul,
|
||||
);
|
||||
if (actionChanged && (action === PlayerAction.Win || action === PlayerAction.Fail)) {
|
||||
this.refreshResultDrawOrder();
|
||||
}
|
||||
return;
|
||||
}
|
||||
void this.applyActionFrames(action);
|
||||
void this.applyActionFrames(action, actionChanged);
|
||||
}
|
||||
|
||||
getAction(): PlayerAction {
|
||||
@@ -279,7 +284,12 @@ export class PlayerActionAnimator extends Component {
|
||||
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;
|
||||
const gen = ++this.loadingGen;
|
||||
const isFront = this.direction === Direction.South || this.direction === Direction.East;
|
||||
@@ -305,6 +315,9 @@ export class PlayerActionAnimator extends Component {
|
||||
this.frameIdx = 0;
|
||||
this.frameTimer = 0;
|
||||
this.showFrame(frames[0]!);
|
||||
if (sortOnLoad && (action === PlayerAction.Win || action === PlayerAction.Fail)) {
|
||||
this.refreshResultDrawOrder();
|
||||
}
|
||||
}
|
||||
|
||||
private showFrame(sf: SpriteFrame) {
|
||||
|
||||
@@ -70,8 +70,7 @@ exports.template = `
|
||||
<ui-button id="btn-next">下一关</ui-button>
|
||||
<ui-button id="btn-load">加载</ui-button>
|
||||
<ui-button id="btn-new">新建</ui-button>
|
||||
<ui-button id="btn-save" type="primary">保存 JSON</ui-button>
|
||||
<ui-button id="btn-bake">烘焙预制体</ui-button>
|
||||
<ui-button id="btn-save" type="primary">保存地图</ui-button>
|
||||
<ui-button id="btn-import-prefab">从预制体同步</ui-button>
|
||||
<ui-button id="btn-open-prefab">打开 Level 预制体</ui-button>
|
||||
<ui-button id="btn-scene-snap">场景选中吸附</ui-button>
|
||||
@@ -118,7 +117,7 @@ exports.template = `
|
||||
<div class="lme-canvas-wrap">
|
||||
<canvas id="lme-canvas" width="720" height="520"></canvas>
|
||||
</div>
|
||||
<div class="lme-status" id="status">地图/出生点编辑 · 自动保存 JSON 并烘焙 · 60×60 网格 · 滚轮缩放 · 长按右键拖动</div>
|
||||
<div class="lme-status" id="status">地图/出生点编辑 · 绘制后自动暂存 · 点「保存地图」写入 JSON 并烘焙预制体 · 滚轮缩放 · 长按右键拖动</div>
|
||||
</div>
|
||||
<div class="lme-palette">
|
||||
<h4 id="palette-title">平铺调色板</h4>
|
||||
@@ -135,7 +134,6 @@ exports.$ = {
|
||||
btnLoad: '#btn-load',
|
||||
btnNew: '#btn-new',
|
||||
btnSave: '#btn-save',
|
||||
btnBake: '#btn-bake',
|
||||
btnImportPrefab: '#btn-import-prefab',
|
||||
btnOpenPrefab: '#btn-open-prefab',
|
||||
btnSceneSnap: '#btn-scene-snap',
|
||||
@@ -561,11 +559,12 @@ exports.ready = function () {
|
||||
persistMapChanges({ quiet: true, skipBake: true });
|
||||
try {
|
||||
bakeIgnore.markBakePending(state.levelId);
|
||||
const outDir = prefabSync.resolvePrefabDirRel(projectPath());
|
||||
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' },
|
||||
);
|
||||
const prefabUrl = `db://assets/resources/level-prefabs/Level${state.levelId}.prefab`;
|
||||
const prefabUrl = prefabSync.prefabDbUrl(state.levelId, projectPath());
|
||||
try {
|
||||
Editor.Message.request('asset-db', 'refresh-asset', prefabUrl);
|
||||
} catch (e) {
|
||||
@@ -574,7 +573,8 @@ exports.ready = function () {
|
||||
if (options.auto) {
|
||||
setStatus(`关卡 ${state.levelId} 已自动保存并烘焙 → Level${state.levelId}.prefab`);
|
||||
} 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`);
|
||||
} catch (e) {
|
||||
@@ -587,25 +587,25 @@ exports.ready = function () {
|
||||
}
|
||||
};
|
||||
|
||||
const saveLevel = () => {
|
||||
/** 保存 JSON + 烘焙预制体(运行时分包 bundle-level-prefabs) */
|
||||
const saveMapLevel = () => {
|
||||
cancelAutoBake();
|
||||
persistMapChanges({ quiet: true, skipBake: true });
|
||||
setStatus(`已保存关卡 ${state.levelId}(主题 ${state.theme})`);
|
||||
if (!state.config || !state.db) {
|
||||
setStatus('请先加载或新建关卡');
|
||||
return;
|
||||
}
|
||||
if (!persistMapChanges({ quiet: true, skipBake: true })) return;
|
||||
runBake.call(this, { auto: false });
|
||||
};
|
||||
|
||||
/** 每次改地图立即写 JSON */
|
||||
/** 绘制时暂存 JSON,延迟自动烘焙 */
|
||||
const scheduleMapPersist = () => {
|
||||
persistMapChanges();
|
||||
};
|
||||
|
||||
const bakeLevel = () => {
|
||||
cancelAutoBake();
|
||||
runBake.call(this, { auto: false });
|
||||
};
|
||||
|
||||
const openLevelPrefab = async () => {
|
||||
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) {
|
||||
await Editor.Message.request('asset-db', 'open-asset', uuid);
|
||||
setStatus(`已打开 Level${id}.prefab(Ground / Border 子节点,贴图主题 ${state.theme})`);
|
||||
@@ -952,8 +952,7 @@ exports.ready = function () {
|
||||
}
|
||||
});
|
||||
bindOnce(this.$.btnNew, 'click', () => createNewLevel.call(this));
|
||||
bindOnce(this.$.btnSave, 'click', () => saveLevel.call(this));
|
||||
bindOnce(this.$.btnBake, 'click', () => bakeLevel.call(this));
|
||||
bindOnce(this.$.btnSave, 'click', () => saveMapLevel.call(this));
|
||||
bindOnce(this.$.btnImportPrefab, 'click', () => importFromPrefab.call(this));
|
||||
bindOnce(this.$.btnOpenPrefab, 'click', () => openLevelPrefab.call(this));
|
||||
|
||||
@@ -991,7 +990,7 @@ exports.ready = function () {
|
||||
const prefabUuid = await Editor.Message.request(
|
||||
'asset-db',
|
||||
'query-uuid',
|
||||
`db://assets/resources/level-prefabs/Level${state.levelId}.prefab`,
|
||||
prefabSync.prefabDbUrl(state.levelId, projectPath()),
|
||||
);
|
||||
if (prefabUuid) uuids = [prefabUuid];
|
||||
}
|
||||
|
||||
21
extensions/level-map-editor/dist/prefab-sync.js
vendored
21
extensions/level-map-editor/dist/prefab-sync.js
vendored
@@ -6,7 +6,10 @@ const levelIdUtil = require('./level-id');
|
||||
|
||||
const LEVEL_MAP_TYPE = 'd4e5fanuMlNDh8qO0xdbn+K';
|
||||
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 TILE_NODE_RE = /^([gb])_(-?\d+)_(-?\d+)$/;
|
||||
|
||||
@@ -20,8 +23,18 @@ function dbPath(projectPath) {
|
||||
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) {
|
||||
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) {
|
||||
@@ -258,6 +271,10 @@ function extractLevelIdFromFilename(name) {
|
||||
module.exports = {
|
||||
DB_REL,
|
||||
PREFAB_DIR_REL,
|
||||
PREFAB_BUNDLE_REL,
|
||||
PREFAB_LEGACY_REL,
|
||||
resolvePrefabDirRel,
|
||||
prefabDbUrl,
|
||||
prefabPath,
|
||||
dbPath,
|
||||
parsePrefabFile,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"package_version": 2,
|
||||
"name": "level-map-editor",
|
||||
"version": "1.7.1",
|
||||
"version": "1.7.2",
|
||||
"description": "关卡地图编辑(对齐 Unity Tile Palette:中间关卡、右侧地图模块)",
|
||||
"author": "tfrh",
|
||||
"editor": ">=3.8.0",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"132536": "level-prefabs/Level132536",
|
||||
"91601": "level-prefabs/Level91601",
|
||||
"91602": "level-prefabs/Level91602",
|
||||
"91603": "level-prefabs/Level91603",
|
||||
|
||||
167
tools/verify-unity-draw-sort.mjs
Normal file
167
tools/verify-unity-draw-sort.mjs
Normal 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.`);
|
||||
Reference in New Issue
Block a user