- 移除手动创建的场景文件(3.8.8 需在编辑器中创建) - 移除旧的 .meta 文件(3.8.8 会自动重新生成) - 重写所有脚本,优化代码结构和 3.8.8 兼容性 - 新增完整使用教程 TUTORIAL.md - 更新 .gitignore 排除 .claude/ 目录 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
291 lines
9.7 KiB
TypeScript
291 lines
9.7 KiB
TypeScript
/**
|
|
* 游戏主控制器 - UI、计分、输入、游戏流程
|
|
* Cocos Creator 3.8.8
|
|
*/
|
|
import { _decorator, Component, Node, Label, Color, Sprite, UITransform, Vec3, v3, EventTouch } from 'cc';
|
|
import { GameBoard } from './GameBoard';
|
|
import { Block } from './Block';
|
|
import { ROWS, COLS, CELL_SIZE, BOARD_PADDING, SPAWN_INTERVAL, getRandomSpawnLevel } from './BlockConfig';
|
|
|
|
const { ccclass, property } = _decorator;
|
|
|
|
const enum GameState { IDLE, PLAYING, GAME_OVER }
|
|
|
|
@ccclass('GameManager')
|
|
export class GameManager extends Component {
|
|
// Cocos Creator 属性面板可见(可在此处拖拽引用)
|
|
@property({ displayName: '生成间隔(秒)', min: 1, max: 10 })
|
|
private spawnInterval: number = SPAWN_INTERVAL;
|
|
|
|
// ---- 内部状态 ----
|
|
private board: GameBoard | null = null;
|
|
private boardNode: Node | null = null;
|
|
private score = 0;
|
|
private spawnTimer = 0;
|
|
private selected: Block | null = null;
|
|
private state: GameState = GameState.IDLE;
|
|
|
|
// ---- UI 节点 ----
|
|
private scoreLabel: Label | null = null;
|
|
private gameOverNode: Node | null = null;
|
|
private finalScoreLabel: Label | null = null;
|
|
|
|
// ============================================================
|
|
// 生命周期
|
|
// ============================================================
|
|
|
|
onLoad() {
|
|
this.buildUI();
|
|
this.buildBoard();
|
|
}
|
|
|
|
start() {
|
|
this.startGame();
|
|
}
|
|
|
|
update(dt: number) {
|
|
if (this.state !== GameState.PLAYING) return;
|
|
this.spawnTimer += dt;
|
|
if (this.spawnTimer >= this.spawnInterval) {
|
|
this.spawnTimer = 0;
|
|
this.spawnBlock();
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// UI 构建
|
|
// ============================================================
|
|
|
|
private buildUI() {
|
|
const root = this.node;
|
|
const rh = 960; // Canvas 设计高度
|
|
|
|
// ---- 背景 ----
|
|
{
|
|
const bg = new Node('Bg');
|
|
const s = bg.addComponent(Sprite);
|
|
s.type = Sprite.Type.SIMPLE;
|
|
s.color = new Color(25, 25, 35, 255);
|
|
bg.addComponent(UITransform).setContentSize(600, 1200);
|
|
bg.setPosition(0, 0, -2);
|
|
root.addChild(bg);
|
|
}
|
|
|
|
const boardH = ROWS * (CELL_SIZE + BOARD_PADDING);
|
|
const topY = boardH / 2 + 60; // 棋盘中点之上的偏移
|
|
|
|
// ---- 记分板底 ----
|
|
{
|
|
const sb = new Node('ScoreBg');
|
|
const s = sb.addComponent(Sprite);
|
|
s.type = Sprite.Type.SIMPLE;
|
|
s.color = new Color(40, 40, 50, 220);
|
|
sb.addComponent(UITransform).setContentSize(480, 60);
|
|
sb.setPosition(0, topY, 0);
|
|
root.addChild(sb);
|
|
}
|
|
|
|
// ---- 标题 ----
|
|
{
|
|
const t = new Node('Title');
|
|
const l = t.addComponent(Label);
|
|
l.string = '合成大作战';
|
|
l.fontSize = 20;
|
|
l.color = new Color(200, 200, 200, 255);
|
|
t.addComponent(UITransform).setContentSize(160, 30);
|
|
t.setPosition(0, topY + 45, 0);
|
|
root.addChild(t);
|
|
}
|
|
|
|
// ---- 分数 ----
|
|
{
|
|
const sn = new Node('ScoreLabel');
|
|
this.scoreLabel = sn.addComponent(Label);
|
|
this.scoreLabel.string = '得分: 0';
|
|
this.scoreLabel.fontSize = 28;
|
|
this.scoreLabel.color = Color.WHITE.clone();
|
|
sn.addComponent(UITransform).setContentSize(300, 40);
|
|
sn.setPosition(0, topY, 0);
|
|
root.addChild(sn);
|
|
}
|
|
|
|
// ---- 游戏结束遮罩 ----
|
|
{
|
|
const go = new Node('GameOver');
|
|
const s = go.addComponent(Sprite);
|
|
s.type = Sprite.Type.SIMPLE;
|
|
s.color = new Color(0, 0, 0, 180);
|
|
go.addComponent(UITransform).setContentSize(600, 1200);
|
|
go.setPosition(0, 0, 10);
|
|
go.active = false;
|
|
root.addChild(go);
|
|
this.gameOverNode = go;
|
|
|
|
// "游戏结束"
|
|
{
|
|
const n = new Node('GoLabel');
|
|
const l = n.addComponent(Label);
|
|
l.string = '游戏结束';
|
|
l.fontSize = 48;
|
|
l.color = new Color(255, 200, 50, 255);
|
|
n.addComponent(UITransform).setContentSize(300, 80);
|
|
n.setPosition(0, 80, 11);
|
|
go.addChild(n);
|
|
}
|
|
|
|
// 最终得分
|
|
{
|
|
const n = new Node('FinalScore');
|
|
this.finalScoreLabel = n.addComponent(Label);
|
|
this.finalScoreLabel.string = '最终得分: 0';
|
|
this.finalScoreLabel.fontSize = 32;
|
|
this.finalScoreLabel.color = Color.WHITE.clone();
|
|
n.addComponent(UITransform).setContentSize(300, 50);
|
|
n.setPosition(0, 10, 11);
|
|
go.addChild(n);
|
|
}
|
|
|
|
// 重新开始按钮
|
|
{
|
|
const btn = new Node('RestartBtn');
|
|
const s = btn.addComponent(Sprite);
|
|
s.type = Sprite.Type.SIMPLE;
|
|
s.color = new Color(76, 175, 80, 255);
|
|
btn.addComponent(UITransform).setContentSize(200, 60);
|
|
btn.setPosition(0, -80, 11);
|
|
const ln = new Node('BtnLabel');
|
|
const l = ln.addComponent(Label);
|
|
l.string = '重新开始';
|
|
l.fontSize = 28;
|
|
l.color = Color.WHITE.clone();
|
|
ln.addComponent(UITransform).setContentSize(200, 40);
|
|
ln.setPosition(0, 0, 12);
|
|
btn.addChild(ln);
|
|
btn.on(Node.EventType.TOUCH_END, () => this.restartGame());
|
|
go.addChild(btn);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// 棋盘
|
|
// ============================================================
|
|
|
|
private buildBoard() {
|
|
this.boardNode = new Node('Board');
|
|
this.board = this.boardNode.addComponent(GameBoard);
|
|
this.board.onScoreUpdate = (pts) => this.addScore(pts);
|
|
this.node.addChild(this.boardNode);
|
|
}
|
|
|
|
// ============================================================
|
|
// 游戏流程
|
|
// ============================================================
|
|
|
|
private startGame() {
|
|
this.score = 0;
|
|
this.spawnTimer = 0;
|
|
this.selected = null;
|
|
this.state = GameState.PLAYING;
|
|
|
|
if (this.scoreLabel) this.scoreLabel.string = '得分: 0';
|
|
if (this.gameOverNode) this.gameOverNode.active = false;
|
|
|
|
// 初始生成 3 个
|
|
if (this.board) {
|
|
for (let i = 0; i < 3; i++) {
|
|
this.board.spawnRandom(getRandomSpawnLevel());
|
|
}
|
|
}
|
|
|
|
// 注册触摸
|
|
this.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this);
|
|
}
|
|
|
|
private spawnBlock() {
|
|
if (!this.board) return;
|
|
if (!this.board.spawnRandom(getRandomSpawnLevel()) && this.board.isBoardFull()) {
|
|
this.gameOver();
|
|
}
|
|
}
|
|
|
|
private addScore(pts: number) {
|
|
this.score += pts;
|
|
if (this.scoreLabel) this.scoreLabel.string = `得分: ${this.score}`;
|
|
}
|
|
|
|
private gameOver() {
|
|
this.state = GameState.GAME_OVER;
|
|
if (this.gameOverNode) this.gameOverNode.active = true;
|
|
if (this.finalScoreLabel) this.finalScoreLabel.string = `最终得分: ${this.score}`;
|
|
}
|
|
|
|
private restartGame() {
|
|
if (this.board) this.board.clearAll();
|
|
this.startGame();
|
|
}
|
|
|
|
// ============================================================
|
|
// 输入处理
|
|
// ============================================================
|
|
|
|
private onTouchEnd(evt: EventTouch) {
|
|
if (this.state !== GameState.PLAYING || !this.board) return;
|
|
|
|
// 屏幕坐标 → Canvas 节点空间坐标
|
|
const uiPos = evt.getUILocation();
|
|
const uiTransform = this.node.getComponent(UITransform);
|
|
if (!uiTransform) return;
|
|
const local = uiTransform.convertToNodeSpaceAR(v3(uiPos.x, uiPos.y, 0));
|
|
|
|
const g = this.board.worldToGrid(local.x, local.y);
|
|
if (!g) { this.clearSelected(); return; }
|
|
|
|
const { row, col } = g;
|
|
const tapped = this.board.getBlock(row, col);
|
|
|
|
if (tapped) {
|
|
// 点击了方块
|
|
if (this.selected) {
|
|
const sel = this.selected;
|
|
if (this.isAdj(sel, tapped) && sel.level === tapped.level) {
|
|
this.board.tryMerge(sel, tapped);
|
|
this.clearSelected();
|
|
} else {
|
|
this.clearSelected();
|
|
tapped.setSelected(true);
|
|
this.selected = tapped;
|
|
}
|
|
} else {
|
|
tapped.setSelected(true);
|
|
this.selected = tapped;
|
|
}
|
|
} else if (this.selected && this.board.isEmpty(row, col)) {
|
|
// 点击空格 → 移动选中方块
|
|
if (this.board.moveBlock(this.selected, row, col)) {
|
|
const block = this.selected;
|
|
this.clearSelected();
|
|
this.scheduleOnce(() => {
|
|
if (block.isValid) this.board!.triggerMergeCheck(block);
|
|
}, 0.15);
|
|
} else {
|
|
this.clearSelected();
|
|
}
|
|
} else {
|
|
this.clearSelected();
|
|
}
|
|
}
|
|
|
|
private isAdj(a: Block, b: Block): boolean {
|
|
return (Math.abs(a.row - b.row) === 1 && a.col === b.col) ||
|
|
(Math.abs(a.col - b.col) === 1 && a.row === b.row);
|
|
}
|
|
|
|
private clearSelected() {
|
|
if (this.selected) {
|
|
this.selected.setSelected(false);
|
|
this.selected = null;
|
|
}
|
|
}
|
|
}
|