同步主站 Cocos 当前工程:关卡资源、运行脚本与打包工具。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-12 14:40:34 +08:00
parent ae75fce80b
commit fac515a669
5609 changed files with 27744634 additions and 5653 deletions

View File

@@ -34,7 +34,7 @@ import { VisualAssets } from './visual/VisualAssets';
import { resolveThemeId } from './theme/ThemeDatabase';
import { LevelDisplay } from './level/LevelDisplay';
import { mergeLevelConfigWithMapData } from './level/LevelConfigMerge';
import { ensureEntityUILayer, findLevelChildByName, forEachLevelEntityNode, sortIsoTiles } from './level/TileLayout';
import { ensureEntityUILayer, findLevelChildByName, forEachLevelEntityNode, primeIsoDrawOrder, sortIsoTiles } from './level/TileLayout';
import { Movement } from './gameplay/Movement';
import { tryLinkPlayerVehicle, tryLinkVehicleRider } from './controller/RideLink';
import { GridSnapHelper } from './level/GridSnapHelper';
@@ -73,6 +73,8 @@ export class GameController extends Component {
@property({ group: { name: '关卡切换', id: '1' }, type: CCInteger, displayName: '当前关卡', readonly: true })
curLevelID = 1;
/** 编辑器最近一次要求加载的关;过期回传不再通知,避免慢关握手错切 */
private requestedLevelID = 0;
@property({ group: { name: '关卡切换', id: '1' }, displayName: '关卡范围', readonly: true })
levelRangeHint = '180';
@@ -105,8 +107,22 @@ export class GameController extends Component {
private levelLoadSeq: Promise<void> = Promise.resolve();
/** 倍速 1/2/4作用于移动与角色动画 */
gameSpeed = 1;
//终于完成了********PlayerLTH17/2026/9/12
private creating = false;
/** SwitchLevel 排队未完成:拦截并暂存 Player/Vehicle 指令,避免执行按钮 200ms 抢跑 */
private levelBusy = false;
private pendingLevelOps = 0;
private pendingActorMsgs: Array<{ objectName: string; methodName: string; param?: string | number }> = [];
/** 进关完成后的干净场景树,同关 SwitchLevel 按 Unity销毁当前实例再 instantiate 这份缓存 */
private levelSnapshot: Node | null = null;
private levelSnapshotId = 0;
/** 加载中只保留最后一次 SwitchLevel避免连点排队重建 */
private queuedLevelId: number | null = null;
private instantResetGuard = false;
private lastInstantResetAt = 0;
private lastInstantResetId = 0;
/** 已显示在场上、可以同关原地重置的关卡 IDadopt 之后才写入) */
private playableLevelId = 0;
private curConfig: LevelConfig | null = null;
private gridTypes = new Map<string, GridEntry>();
private gridTypesForProps = new Map<string, GridEntry>();
@@ -137,6 +153,7 @@ export class GameController extends Component {
}
onDestroy() {
this.disposeLevelSnapshot();
if (GameController.instance === this) GameController.instance = null;
}
@@ -284,6 +301,7 @@ export class GameController extends Component {
if (typeof window !== 'undefined') {
(window as unknown as { cocosIns?: typeof api }).cocosIns = api;
(window as unknown as { unityInstance?: typeof api }).unityInstance = api;
this.publishLevelReady(true);
}
}
@@ -302,9 +320,19 @@ export class GameController extends Component {
if (c) this.invoke(c, methodName, param);
return;
}
if (this.shouldDeferActorMessage(objectName)) {
if (methodName === 'CallMove' || methodName === 'callMove') {
console.warn('[tfrh-reset] CallMove 被关卡重置拦住,稍后发出', { creating: this.creating, levelBusy: this.levelBusy });
}
this.pendingActorMsgs.push({ objectName, methodName, param });
return;
}
if (methodName === 'CallMove' || methodName === 'callMove') {
console.warn('[tfrh-reset] CallMove 立刻执行', { param, playableLevelId: this.playableLevelId, busy: this.levelBusy });
}
const node = this.findNodeByName(objectName);
if (!node) {
if (objectName === 'Player' && this.creating) return;
if (objectName === 'Player' && (this.creating || this.levelBusy)) return;
console.warn(`SendMessage: 未找到 ${objectName}`);
return;
}
@@ -577,17 +605,37 @@ export class GameController extends Component {
}
for (const ch of [...this.mainLevelEntrance.children]) {
if (!ch || ch.name === 'LineGrid') continue;
if (ch.isValid) ch.destroy();
if (!ch.isValid) continue;
ch.active = false;
ch.removeFromParent();
ch.destroy();
}
} else if (this.curLevel?.isValid) {
this.curLevel.active = false;
this.curLevel.removeFromParent();
this.curLevel.destroy();
}
this.curLevel = null;
this.playableLevelId = 0;
this.syncCurLevelRef();
this.gridTypes.clear();
this.gridTypesForProps.clear();
this.groundCells.clear();
this.borderCells.clear();
this.disposeLevelSnapshot();
}
/** 新关就绪后再卸旧关,避免执行/取消时闪出另一张地图 */
private adoptLevelRoot(levelRoot: Node) {
this.curLevel = levelRoot;
this.playableLevelId = Number(this.curLevelID);
if (!this.mainLevelEntrance?.isValid) return;
for (const ch of [...this.mainLevelEntrance.children]) {
if (!ch?.isValid || ch === levelRoot || ch.name === 'LineGrid') continue;
ch.active = false;
ch.removeFromParent();
ch.destroy();
}
}
confirmSwitchLevel(levelID?: number) {
@@ -610,34 +658,110 @@ export class GameController extends Component {
);
}
this.inputLevel = String(id);
this.requestedLevelID = id;
this.pendingActorMsgs = [];
this.setLevelBusy(true);
const inPlace = this.tryResetCurrentLevelInPlace(id);
console.warn('[tfrh-reset] SwitchLevel', {
id,
playableLevelId: this.playableLevelId,
curLevelID: this.curLevelID,
inPlace,
creating: this.creating,
pendingLevelOps: this.pendingLevelOps,
hasLevel: !!this.curLevel?.isValid,
});
if (inPlace) {
this.setLevelBusy(false);
return;
}
void this.loadLevel(id);
}
loadLevel(levelID: number) {
if (!this.ready && !this.mainLevelEntrance) {
console.warn('[GameController] 尚未初始化');
if (this.pendingLevelOps <= 0) this.setLevelBusy(false);
return;
}
if (!this.mainLevelEntrance) {
console.error('[GameController] mainLevelEntrance 未绑定');
if (this.pendingLevelOps <= 0) this.setLevelBusy(false);
return;
}
void this.enqueueLoadLevel(levelID, false);
}
private enqueueLoadLevel(levelID: number, forceRestart: boolean) {
this.queuedLevelId = levelID;
if (this.tryResetCurrentLevelInPlace(levelID) && this.pendingLevelOps <= 0 && !this.creating) {
this.queuedLevelId = null;
this.setLevelBusy(false);
return this.levelLoadSeq;
}
this.setLevelBusy(true);
if (this.pendingLevelOps > 0) return this.levelLoadSeq;
this.pendingLevelOps++;
this.levelLoadSeq = this.levelLoadSeq
.then(() => this.loadLevelNow(levelID, forceRestart))
.catch((e) => console.error('[GameController] 关卡加载失败', e));
.then(async () => {
while (this.queuedLevelId != null) {
const id = this.queuedLevelId;
this.queuedLevelId = null;
await this.loadLevelNow(id, forceRestart);
}
})
.catch((e) => console.error('[GameController] 关卡加载失败', e))
.finally(() => {
this.pendingLevelOps = Math.max(0, this.pendingLevelOps - 1);
if (this.queuedLevelId != null && this.pendingLevelOps <= 0) {
void this.enqueueLoadLevel(this.queuedLevelId, forceRestart);
return;
}
if (this.pendingLevelOps <= 0) this.setLevelBusy(false);
});
return this.levelLoadSeq;
}
private async loadLevelNow(levelID: number, forceRestart: boolean) {
if (levelID === this.curLevelID && this.curLevel?.isValid && this.curConfig) {
await this.restartCurrentLevel();
return;
private shouldDeferActorMessage(objectName: string) {
if (!this.levelBusy && !this.creating) return false;
return objectName === 'Player' || objectName === 'Vehicle'
|| /^Player[AB]\d$/.test(objectName) || /^Vehicle[AB]\d$/.test(objectName);
}
private setLevelBusy(busy: boolean) {
this.levelBusy = busy;
this.publishLevelReady(!busy);
if (!busy) this.flushPendingActorMsgs();
}
private publishLevelReady(ready: boolean) {
const apply = (target: { __tfrhLevelReady?: boolean; __tfrhLevelBusy?: boolean } | null) => {
if (!target) return;
target.__tfrhLevelBusy = !ready;
target.__tfrhLevelReady = ready;
};
if (typeof window === 'undefined') return;
apply(window as unknown as { __tfrhLevelReady?: boolean; __tfrhLevelBusy?: boolean });
try {
if (window.parent && window.parent !== window) {
apply(window.parent as unknown as { __tfrhLevelReady?: boolean; __tfrhLevelBusy?: boolean });
}
} catch {
/* 跨域 iframe 无法写父页,编辑器仍靠游戏内排队挡住抢跑 */
}
this.destroyCurLevel();
}
private flushPendingActorMsgs() {
if (!this.pendingActorMsgs.length) return;
const queued = this.pendingActorMsgs;
this.pendingActorMsgs = [];
for (const msg of queued) {
this.sendMessage(msg.objectName, msg.methodName, msg.param);
}
}
private async loadLevelNow(levelID: number, _forceRestart: boolean) {
if (this.tryResetCurrentLevelInPlace(levelID)) return;
await this.createNewLevel(levelID);
}
@@ -657,26 +781,139 @@ export class GameController extends Component {
console.log(`[GameController] 已刷新关卡 ${this.curLevelID} 贴图`);
}
/** 重置玩家/道具,保留关卡砖块与背景,避免闪屏 */
private async restartCurrentLevel() {
if (!this.curLevel?.isValid || !this.curConfig || !this.mainLevelEntrance) {
await this.createNewLevel(this.curLevelID);
/**
* 同关执行:只当场拉回出生点,不拆地图。不同 ID 就是不同地图,绝不以偏移折成同一关。
*/
private tryResetCurrentLevelInPlace(levelID: number): boolean {
if (this.creating) return false;
if (Number(levelID) !== Number(this.playableLevelId)) return false;
if (!this.curLevel?.isValid || !this.curConfig) return false;
this.queuedLevelId = null;
this.resetCurrentLevelInPlace();
return true;
}
private resetCurrentLevelInPlace() {
if (this.instantResetGuard) return;
const id = Number(this.playableLevelId);
const now = Date.now();
if (id === this.lastInstantResetId && now - this.lastInstantResetAt < 50) {
this.pendingActorMsgs = [];
this.initData();
return;
}
const config = this.curConfig;
const levelRoot = this.curLevel;
const tileNames = LevelDisplay.collectTileNames(config);
await VisualAssets.ensureLevelAssetsReady(config, tileNames);
await this.purgeDynamicEntities(levelRoot);
const spawned = await this.spawnAllEntities(levelRoot, config);
await this.finalizeEntityPresentation(levelRoot, config);
this.instantResetGuard = true;
try {
this.restartCurrentLevelSync();
this.lastInstantResetAt = Date.now();
this.lastInstantResetId = id;
} finally {
this.instantResetGuard = false;
}
}
private captureLevelSnapshot(levelRoot: Node) {
this.disposeLevelSnapshot();
if (!levelRoot?.isValid) return;
const snap = instantiate(levelRoot);
snap.active = false;
snap.removeFromParent();
this.levelSnapshot = snap;
this.levelSnapshotId = Number(this.curLevelID);
}
private disposeLevelSnapshot() {
if (this.levelSnapshot?.isValid) this.levelSnapshot.destroy();
this.levelSnapshot = null;
this.levelSnapshotId = 0;
}
/** 对齐 Unity Addressables 二次 Instantiate旧关卸掉缓存树当场克隆出来。 */
private replayFromSnapshot(): boolean {
const snap = this.levelSnapshot;
if (!snap?.isValid || this.levelSnapshotId !== Number(this.curLevelID) || !this.mainLevelEntrance) {
return false;
}
const fresh = instantiate(snap);
if (!fresh?.isValid) return false;
fresh.active = false;
GridSnapHelper.purgeRuntimeGrids(fresh);
fresh.parent = this.mainLevelEntrance;
fresh.setPosition(0, 0, 0);
this.adoptLevelRoot(fresh);
if (this.curConfig) this.applyMapDataFromConfig(this.curConfig);
this.initGridTypes();
this.initData();
EventManager.dispatch(EventType.LevelInit);
this.refreshAllVehicleIcons(fresh);
this.finishLevelDrawOrder(fresh);
this.revealLevelEntities(fresh);
fresh.active = true;
void GameAudio.playBackground(fresh);
GridSnapHelper.purgeRuntimeGrids(fresh);
console.log(`[GameController] 已按快照重建关卡 ${this.curLevelID}`);
return true;
}
private restartCurrentLevelSync() {
const config = this.curConfig;
const levelRoot = this.curLevel;
if (!config || !levelRoot?.isValid) return;
this.resetExistingMoversToSpawn(levelRoot, config);
this.restoreCollectedProps(levelRoot);
this.initData();
EventManager.dispatch(EventType.LevelInit);
this.applyMapDataFromConfig(config);
this.initGridTypes();
this.refreshAllVehicleIcons(levelRoot);
this.externalCallLevelInfo(spawned);
this.syncPlayerAppearanceToLevel(config);
console.log(`[GameController] 已重置关卡 ${this.curLevelID}(无重载)`);
this.finishLevelDrawOrder(levelRoot);
this.revealLevelEntities(levelRoot);
console.log(`[GameController] 已重置关卡 ${this.curLevelID}`);
}
private restoreCollectedProps(levelRoot: Node) {
const walk = (node: Node) => {
if (!node?.isValid) return;
node.getComponent(PropController)?.restoreForRestart();
for (const ch of node.children) walk(ch);
};
walk(levelRoot);
}
private resetExistingMoversToSpawn(levelRoot: Node, config: LevelConfig) {
for (const s of config.spawns ?? []) {
if (s.kind === 'player') {
const pc = findLevelChildByName(levelRoot, 'Player')?.getComponent(PlayerController);
if (!pc) continue;
pc.direction = this.resolveDirection(s.playerDirection) ?? Direction.South;
pc.setSpawnCell(new Vec3(s.x, s.y, 0));
pc.resetToSpawn();
continue;
}
if (s.kind === 'vehicle') {
const vc = findLevelChildByName(levelRoot, 'Vehicle')?.getComponent(VehicleController);
if (!vc) continue;
vc.direction = this.resolveDirection(s.vehicleDirection) ?? Direction.North;
vc.setSpawnCell(new Vec3(s.x, s.y, 0));
vc.resetToSpawn();
}
}
}
private purgePropEntities(levelRoot: Node): Promise<void> {
const toRemove = this.collectDynamicEntities(levelRoot).filter((n) => {
const name = n.name;
return name === 'Prop' || name.startsWith('Prop_')
|| name === 'PropDecor' || name.startsWith('PropDecor_')
|| !!n.getComponent(PropController);
});
for (const ch of toRemove) {
ch.removeFromParent();
ch.destroy();
}
return new Promise((resolve) => {
this.scheduleOnce(() => resolve(), 0);
});
}
/** 主题 entityDisplay 中 Y 偏移/站立补偿变更后,重算实体世界坐标(缩放只改贴图尺寸) */
@@ -796,15 +1033,33 @@ export class GameController extends Component {
const vc = ch.getComponent(VehicleController);
if (vc) tryLinkVehicleRider(vc);
});
// 先排图层再显示,避免载具带着错误 sibling 露出来
sortIsoTiles(levelRoot, true);
// 延迟一帧再强制全量:等 LevelDisplay.scheduleOnce 对齐完砖,开局金币/遮挡一次算准
this.scheduleOnce(() => {
if (levelRoot?.isValid) sortIsoTiles(levelRoot, true);
}, 0);
}
private revealLevelEntities(levelRoot: Node) {
forEachLevelEntityNode(levelRoot, (node) => {
if (node?.isValid && !node.active) node.active = true;
});
}
private finishLevelDrawOrder(levelRoot: Node | null) {
if (!levelRoot?.isValid) return;
sortIsoTiles(levelRoot, true);
primeIsoDrawOrder(levelRoot, 2);
}
async createNewLevel(levelID: number) {
if (this.creating) return;
console.warn('[tfrh-reset] createNewLevel 开始(整关重建)', {
levelID,
playableLevelId: this.playableLevelId,
curLevelID: this.curLevelID,
creating: this.creating,
});
if (this.creating) {
console.warn('[tfrh-reset] createNewLevel 被 creating 挡住,同关执行会走异步队列');
return;
}
this.creating = true;
try {
await ensureRuntimeAssetsForLevel(levelID);
@@ -834,20 +1089,15 @@ export class GameController extends Component {
// 在 instantiate 前无法拦截;先禁用 prefab 内 GridSnapHelper 的 showGrid
GridSnapHelper.stripBeforePlayFromPrefab(prefab);
const levelRoot = instantiate(prefab);
levelRoot.active = false;
GridSnapHelper.purgeRuntimeGrids(levelRoot);
levelRoot.parent = this.mainLevelEntrance;
levelRoot.setPosition(0, 0, 0);
this.curLevel = levelRoot;
// 预制体 onEnable 可能已画格,挂载后再清一次
GridSnapHelper.purgeRuntimeGrids(this.mainLevelEntrance!);
const runtimeConfig = mergeLevelConfigWithMapData(config, levelRoot);
this.curConfig = runtimeConfig;
this.applyMapDataFromConfig(runtimeConfig);
await LevelDisplay.prepare(levelRoot, runtimeConfig);
const mapTheme = runtimeConfig.theme || 'silu';
await ThemeBackground.apply(this.mainLevelEntrance, mapTheme);
await VisualAssets.ensureLevelAssetsReady(
runtimeConfig,
LevelDisplay.collectTileNames(runtimeConfig),
@@ -855,6 +1105,14 @@ export class GameController extends Component {
const spawned = await this.spawnAllEntities(levelRoot, runtimeConfig);
await this.finalizeEntityPresentation(levelRoot, runtimeConfig);
this.adoptLevelRoot(levelRoot);
this.curConfig = runtimeConfig;
this.applyMapDataFromConfig(runtimeConfig);
levelRoot.active = true;
const mapTheme = runtimeConfig.theme || 'silu';
await ThemeBackground.apply(this.mainLevelEntrance, mapTheme);
this.syncPlayerAppearanceToLevel(runtimeConfig);
await GameAudio.playBackground(levelRoot);
@@ -863,6 +1121,8 @@ export class GameController extends Component {
this.initData();
EventManager.dispatch(EventType.LevelInit);
this.refreshAllVehicleIcons(levelRoot);
this.finishLevelDrawOrder(levelRoot);
this.revealLevelEntities(levelRoot);
if (this.mainLevelEntrance?.isValid) {
LineGridRenderer.ensure(this.mainLevelEntrance);
}
@@ -991,6 +1251,7 @@ export class GameController extends Component {
const { node, pos, visual } = built;
ensureEntityUILayer(node);
node.setPosition(pos);
node.active = false;
node.parent = parent;
await VisualAssets.setupEntityVisualAsync(
node,
@@ -1093,6 +1354,12 @@ export class GameController extends Component {
* { LevelID, PlayerName, VehicleName }
*/
externalCallLevelInfo(objects: Node[]) {
if (this.requestedLevelID > 0 && Number(this.curLevelID) !== Number(this.requestedLevelID)) {
console.warn(
`[GameController] 跳过过期 externalLevelInfo cur=${this.curLevelID} want=${this.requestedLevelID}`,
);
return;
}
const info: ExternalLevelInfo = {
LevelID: this.curLevelID,
PlayerName: '',