Complete Cocos Creator port with level bundles, themes, and tooling.
Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,19 +1,48 @@
|
||||
import {
|
||||
_decorator, Component, Node, Canvas, Camera, UITransform, view, Color,
|
||||
director, Label, find,
|
||||
_decorator, Component, Node, Camera, Canvas, UITransform, view, Color,
|
||||
director, find, Layers,
|
||||
} from 'cc';
|
||||
import { GameController } from './GameController';
|
||||
import { GameManager } from './manager/GameManager';
|
||||
import { VisualAssets } from './visual/VisualAssets';
|
||||
import { ViewController } from './controller/ViewController';
|
||||
import { LineGridRenderer } from './gameplay/LineGridRenderer';
|
||||
import { UIMain } from './ui/UIMain';
|
||||
import { loadLevelDatabase, refreshLevelIdBounds, LEVEL_ID_BASE } from './level/LevelRegistry';
|
||||
import { loadThemeDatabase } from './theme/ThemeRegistry';
|
||||
import { ensureResourcesBundle } from './core/ResourcesBundle';
|
||||
import { loadTileDisplayMeta } from './visual/TileDisplayMeta';
|
||||
import { ThemeBackground } from './theme/ThemeBackground';
|
||||
import { GridSnapHelper } from './level/GridSnapHelper';
|
||||
import { GameAudio } from './audio/GameAudio';
|
||||
import { GameplayDebugBar } from './ui/GameplayDebugBar';
|
||||
import {
|
||||
CELL_PIXEL, CAMERA_ORTHO_HALF, DESIGN_WIDTH, DESIGN_HEIGHT,
|
||||
} from './core/GridConstants';
|
||||
import { applyEmbeddedDesignResolution } from './core/EmbeddedView';
|
||||
|
||||
export {
|
||||
CELL_PIXEL, CAMERA_ORTHO_HALF, DESIGN_WIDTH, DESIGN_HEIGHT,
|
||||
} from './core/GridConstants';
|
||||
|
||||
const { ccclass, executionOrder } = _decorator;
|
||||
|
||||
/** 每格像素尺寸(UI 坐标) */
|
||||
export const CELL_PIXEL = 56;
|
||||
/** HUD 专用层:与关卡 UI_2D 分离,不受主相机缩放/拖拽影响 */
|
||||
const HUD_LAYER = Layers.Enum.UI_3D;
|
||||
/** 背景专用层:BgCamera 固定正交,主相机缩放/平移时不跟随 */
|
||||
const BG_LAYER = Layers.Enum.DEFAULT;
|
||||
|
||||
@ccclass('AppBootstrap')
|
||||
@executionOrder(-100)
|
||||
export class AppBootstrap extends Component {
|
||||
private readonly onCanvasResize = () => {
|
||||
const sync = (globalThis as { __tfrhSyncEmbeddedCanvas?: () => void }).__tfrhSyncEmbeddedCanvas;
|
||||
if (typeof sync === 'function') sync();
|
||||
else applyEmbeddedDesignResolution();
|
||||
};
|
||||
|
||||
onDestroy() {
|
||||
view.off('canvas-resize', this.onCanvasResize, this);
|
||||
}
|
||||
|
||||
async onLoad() {
|
||||
try {
|
||||
await this.bootstrap();
|
||||
@@ -24,94 +53,200 @@ export class AppBootstrap extends Component {
|
||||
|
||||
private async bootstrap() {
|
||||
console.log('[AppBootstrap] 开始初始化…');
|
||||
await VisualAssets.preload();
|
||||
applyEmbeddedDesignResolution();
|
||||
view.on('canvas-resize', this.onCanvasResize, this);
|
||||
await ensureResourcesBundle();
|
||||
await loadThemeDatabase();
|
||||
await loadTileDisplayMeta();
|
||||
await loadLevelDatabase();
|
||||
refreshLevelIdBounds();
|
||||
await GameAudio.preload();
|
||||
|
||||
const scene = director.getScene()!;
|
||||
find('UICanvas', scene)?.destroy();
|
||||
|
||||
let mainCam = find('Main Camera', scene)?.getComponent(Camera) ?? null;
|
||||
if (!mainCam) {
|
||||
const camNode = new Node('Main Camera');
|
||||
camNode.parent = scene;
|
||||
mainCam = camNode.addComponent(Camera);
|
||||
} else if (mainCam.node.parent !== scene) {
|
||||
mainCam.node.parent = scene;
|
||||
}
|
||||
this.setupCamera(mainCam);
|
||||
mainCam.node.getComponent(ViewController) ?? mainCam.node.addComponent(ViewController);
|
||||
|
||||
const light = find('Main Light', scene);
|
||||
if (light) light.active = false;
|
||||
|
||||
let canvasNode = find('Canvas', scene);
|
||||
if (!canvasNode) {
|
||||
canvasNode = new Node('Canvas');
|
||||
canvasNode.parent = scene;
|
||||
canvasNode.addComponent(Canvas);
|
||||
}
|
||||
const canvas = canvasNode.getComponent(Canvas)!;
|
||||
canvas.cameraComponent = mainCam;
|
||||
let canvasUi = canvasNode.getComponent(UITransform);
|
||||
if (!canvasUi) canvasUi = canvasNode.addComponent(UITransform);
|
||||
const size = view.getVisibleSize();
|
||||
canvasUi.setContentSize(size.width, size.height);
|
||||
const gameRoot = this.ensureGameRoot(scene, size, mainCam);
|
||||
this.ensureBgOverlay(scene, size, mainCam);
|
||||
const uiOverlay = this.ensureUIOverlay(scene, size, mainCam);
|
||||
this.bindAllCanvases(scene, mainCam);
|
||||
|
||||
let gameRoot = canvasNode.getChildByName('GameRoot');
|
||||
if (!gameRoot) {
|
||||
gameRoot = new Node('GameRoot');
|
||||
gameRoot.parent = canvasNode;
|
||||
const grUi = gameRoot.addComponent(UITransform);
|
||||
grUi.setContentSize(size.width, size.height);
|
||||
}
|
||||
const host = this.node;
|
||||
const ctl = host.getComponent(GameController) ?? host.addComponent(GameController);
|
||||
|
||||
let gcNode = scene.getChildByName('GameController');
|
||||
if (!gcNode) {
|
||||
gcNode = new Node('GameController');
|
||||
gcNode.parent = scene;
|
||||
gcNode.addComponent(GameManager);
|
||||
gcNode.addComponent(GameController);
|
||||
}
|
||||
|
||||
const gctl = gcNode.getComponent(GameController);
|
||||
const gm = gcNode.getComponent(GameManager)!;
|
||||
if (gctl) {
|
||||
if (gctl.mainLevelEntrance) gm.mainLevelEntrance = gctl.mainLevelEntrance;
|
||||
gm.initialLevelID = gctl.initialLevelID;
|
||||
gm.playerSkin = gctl.playerSkin;
|
||||
}
|
||||
let entrance = gameRoot.getChildByName('MainLevelEntrance');
|
||||
if (!entrance) {
|
||||
entrance = new Node('MainLevelEntrance');
|
||||
entrance.parent = gameRoot;
|
||||
const eUi = entrance.addComponent(UITransform);
|
||||
eUi.setContentSize(size.width, size.height);
|
||||
entrance.addComponent(UITransform).setContentSize(size.width, size.height);
|
||||
}
|
||||
gm.mainLevelEntrance = entrance;
|
||||
gm.initialLevelID = 1;
|
||||
entrance.layer = Layers.Enum.UI_2D;
|
||||
LineGridRenderer.ensure(entrance);
|
||||
GridSnapHelper.purgeRuntimeGrids(entrance);
|
||||
|
||||
this.ensureHint(canvasNode);
|
||||
ctl.mainLevelEntrance = entrance;
|
||||
if (ctl.initialLevelID <= 0) ctl.initialLevelID = LEVEL_ID_BASE;
|
||||
if (!ctl.inputLevel?.trim()) ctl.inputLevel = String(ctl.initialLevelID);
|
||||
|
||||
await gm.createNewLevel(gm.initialLevelID);
|
||||
console.log('[AppBootstrap] 关卡已加载');
|
||||
this.removeRuntimeOverlayUI(gameRoot);
|
||||
gameRoot.getChildByName('UIMain')?.destroy();
|
||||
UIMain.ensure(uiOverlay);
|
||||
GameplayDebugBar.ensure(uiOverlay);
|
||||
ThemeBackground.purgeStaleNodes(entrance);
|
||||
|
||||
// 关卡预制体由 SwitchLevel → createNewLevel 按需加载(loader 进关再下 levels_all)
|
||||
ctl.markReady();
|
||||
ctl.onBootstrapReady();
|
||||
console.log('[AppBootstrap] 引擎已就绪;SwitchLevel 进关时再加载关卡预制体');
|
||||
}
|
||||
|
||||
private ensureGameRoot(scene: Node, size: { width: number; height: number }, mainCam: Camera): Node {
|
||||
let gameRoot = find('GameRoot', scene) ?? find('Canvas', scene);
|
||||
if (!gameRoot) {
|
||||
gameRoot = new Node('GameRoot');
|
||||
gameRoot.parent = scene;
|
||||
}
|
||||
gameRoot.name = 'GameRoot';
|
||||
gameRoot.layer = Layers.Enum.UI_2D;
|
||||
gameRoot.active = true;
|
||||
const ui = gameRoot.getComponent(UITransform) ?? gameRoot.addComponent(UITransform);
|
||||
ui.setContentSize(size.width, size.height);
|
||||
|
||||
const canvas = gameRoot.getComponent(Canvas) ?? gameRoot.addComponent(Canvas);
|
||||
canvas.cameraComponent = mainCam;
|
||||
canvas.alignCanvasWithScreen = true;
|
||||
return gameRoot;
|
||||
}
|
||||
|
||||
/** 固定背景相机:先于主相机绘制,缩放/拖拽关卡时背景不动 */
|
||||
private ensureBgOverlay(scene: Node, size: { width: number; height: number }, mainCam: Camera): Node {
|
||||
let bgCamNode = find('BgCamera', scene);
|
||||
if (!bgCamNode) {
|
||||
bgCamNode = new Node('BgCamera');
|
||||
bgCamNode.parent = scene;
|
||||
}
|
||||
bgCamNode.setPosition(0, 0, 1000);
|
||||
bgCamNode.setRotationFromEuler(0, 0, 0);
|
||||
const bgCam = bgCamNode.getComponent(Camera) ?? bgCamNode.addComponent(Camera);
|
||||
bgCam.projection = Camera.ProjectionType.ORTHO;
|
||||
bgCam.orthoHeight = CAMERA_ORTHO_HALF;
|
||||
bgCam.near = 1;
|
||||
bgCam.far = 2000;
|
||||
bgCam.priority = mainCam.priority - 10;
|
||||
bgCam.clearFlags = Camera.ClearFlag.SOLID_COLOR;
|
||||
bgCam.clearColor = new Color(1, 1, 1, 255);
|
||||
bgCam.visibility = BG_LAYER;
|
||||
|
||||
let overlay = find('BgOverlay', scene);
|
||||
if (!overlay) {
|
||||
overlay = new Node('BgOverlay');
|
||||
overlay.parent = scene;
|
||||
}
|
||||
overlay.layer = BG_LAYER;
|
||||
overlay.active = true;
|
||||
const ui = overlay.getComponent(UITransform) ?? overlay.addComponent(UITransform);
|
||||
ui.setContentSize(size.width, size.height);
|
||||
const canvas = overlay.getComponent(Canvas) ?? overlay.addComponent(Canvas);
|
||||
canvas.cameraComponent = bgCam;
|
||||
canvas.alignCanvasWithScreen = true;
|
||||
return overlay;
|
||||
}
|
||||
|
||||
/** 固定 HUD 相机:缩放/平移主相机时按钮位置不变(对齐 Unity Screen Space Overlay) */
|
||||
private ensureUIOverlay(scene: Node, size: { width: number; height: number }, mainCam: Camera): Node {
|
||||
let uiCamNode = find('UICamera', scene);
|
||||
if (!uiCamNode) {
|
||||
uiCamNode = new Node('UICamera');
|
||||
uiCamNode.parent = scene;
|
||||
}
|
||||
uiCamNode.setPosition(0, 0, 1000);
|
||||
uiCamNode.setRotationFromEuler(0, 0, 0);
|
||||
const uiCam = uiCamNode.getComponent(Camera) ?? uiCamNode.addComponent(Camera);
|
||||
uiCam.projection = Camera.ProjectionType.ORTHO;
|
||||
uiCam.orthoHeight = CAMERA_ORTHO_HALF;
|
||||
uiCam.near = 1;
|
||||
uiCam.far = 2000;
|
||||
uiCam.priority = mainCam.priority + 10;
|
||||
uiCam.clearFlags = Camera.ClearFlag.DEPTH_STENCIL;
|
||||
uiCam.visibility = HUD_LAYER;
|
||||
|
||||
let overlay = find('UIOverlay', scene);
|
||||
if (!overlay) {
|
||||
overlay = new Node('UIOverlay');
|
||||
overlay.parent = scene;
|
||||
}
|
||||
overlay.layer = HUD_LAYER;
|
||||
overlay.active = true;
|
||||
const ui = overlay.getComponent(UITransform) ?? overlay.addComponent(UITransform);
|
||||
ui.setContentSize(size.width, size.height);
|
||||
const canvas = overlay.getComponent(Canvas) ?? overlay.addComponent(Canvas);
|
||||
canvas.cameraComponent = uiCam;
|
||||
canvas.alignCanvasWithScreen = true;
|
||||
return overlay;
|
||||
}
|
||||
|
||||
private bindAllCanvases(scene: Node, mainCam: Camera) {
|
||||
const uiCam = find('UICamera', scene)?.getComponent(Camera) ?? null;
|
||||
const bgCam = find('BgCamera', scene)?.getComponent(Camera) ?? null;
|
||||
for (const canvas of scene.getComponentsInChildren(Canvas)) {
|
||||
if (canvas.node.name === 'BgOverlay') {
|
||||
if (bgCam) canvas.cameraComponent = bgCam;
|
||||
canvas.alignCanvasWithScreen = true;
|
||||
continue;
|
||||
}
|
||||
if (canvas.node.name === 'UIOverlay') {
|
||||
if (uiCam) canvas.cameraComponent = uiCam;
|
||||
canvas.alignCanvasWithScreen = true;
|
||||
continue;
|
||||
}
|
||||
if (!canvas.cameraComponent?.isValid) {
|
||||
canvas.cameraComponent = mainCam;
|
||||
}
|
||||
canvas.alignCanvasWithScreen = true;
|
||||
}
|
||||
// 清理无相机 Canvas,避免 PointerEventDispatcher 读 null.cameraPriority
|
||||
for (const canvas of scene.getComponentsInChildren(Canvas)) {
|
||||
if (!canvas.cameraComponent) {
|
||||
canvas.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private removeRuntimeOverlayUI(gameRoot: Node) {
|
||||
gameRoot.getChildByName('Hint')?.destroy();
|
||||
gameRoot.getChildByName('LevelSwitchBar')?.destroy();
|
||||
}
|
||||
|
||||
private setupCamera(cam: Camera) {
|
||||
const camNode = cam.node;
|
||||
camNode.setPosition(0, 0, 1000);
|
||||
camNode.setRotationFromEuler(0, 0, 0);
|
||||
camNode.layer = Layers.Enum.DEFAULT;
|
||||
cam.projection = Camera.ProjectionType.ORTHO;
|
||||
cam.orthoHeight = 360;
|
||||
cam.orthoHeight = CAMERA_ORTHO_HALF;
|
||||
cam.near = 1;
|
||||
cam.far = 2000;
|
||||
cam.clearFlags = Camera.ClearFlag.SOLID_COLOR;
|
||||
cam.clearColor = new Color(30, 40, 60, 255);
|
||||
cam.clearFlags = Camera.ClearFlag.DEPTH_STENCIL;
|
||||
cam.visibility = Layers.Enum.UI_2D;
|
||||
}
|
||||
|
||||
private ensureHint(canvasNode: Node) {
|
||||
if (canvasNode.getChildByName('Hint')) return;
|
||||
const hint = new Node('Hint');
|
||||
hint.parent = canvasNode;
|
||||
const ui = hint.addComponent(UITransform);
|
||||
ui.setContentSize(400, 40);
|
||||
hint.setPosition(0, 280, 0);
|
||||
const label = hint.addComponent(Label);
|
||||
label.string = '主站 Cocos · 关卡运行中';
|
||||
label.fontSize = 22;
|
||||
label.color = new Color(200, 220, 255);
|
||||
switchLevelFromBootstrap() {
|
||||
const gc = this.getComponent(GameController);
|
||||
if (!gc) {
|
||||
console.warn('[AppBootstrap] 未找到 GameController,请先点预览 ▶');
|
||||
return;
|
||||
}
|
||||
gc.clickSwitchLevel();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user