- 全部代码重写,改为拖拽合成(drag-and-drop merge) - 拖拽方块到相邻同等级方块上完成合成 - 拖到空格移动,拖到无效位置弹回 - Cocos Creator 3.8.8 全面适配 - 移除 GameManager.ts → 改为 GameController.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
284 lines
8.3 KiB
TypeScript
284 lines
8.3 KiB
TypeScript
/**
|
||
* 游戏主控 — UI、游戏流程、拖拽输入
|
||
* 拖拽机制:按下拾取 → 拖拽跟随 → 释放判定合并/移动/弹回
|
||
*/
|
||
import {
|
||
_decorator, Component, Node, Label, Color, Sprite, UITransform, Vec3, v3,
|
||
EventTouch,
|
||
} from 'cc';
|
||
import { GameBoard } from './GameBoard';
|
||
import { Block } from './Block';
|
||
import { ROWS, CELL, PAD, SPAWN_INTERVAL, randLevel } from './BlockConfig';
|
||
|
||
const { ccclass } = _decorator;
|
||
|
||
// 普通枚举(不被 isolatedModules 限制)
|
||
enum GState { Idle, Playing, Over }
|
||
|
||
@ccclass('GameController')
|
||
export class GameController extends Component {
|
||
private board!: GameBoard;
|
||
private boardNode!: Node;
|
||
private state = GState.Idle;
|
||
private score = 0;
|
||
private spawnTimer = 0;
|
||
|
||
// ---- 拖拽 ----
|
||
private dragging: Block | null = null;
|
||
private dragHome = v3(); // 拖拽前的位置(board 空间)
|
||
private dragOff = v3(); // 手指到方块中心偏移
|
||
|
||
// ---- UI ----
|
||
private scoreLabel!: Label;
|
||
private overlay!: Node;
|
||
private finalScore!: Label;
|
||
|
||
// ============================================================
|
||
// 生命周期
|
||
// ============================================================
|
||
|
||
onLoad() {
|
||
this.buildUI();
|
||
this.buildBoard();
|
||
}
|
||
|
||
start() {
|
||
this.startGame();
|
||
}
|
||
|
||
update(dt: number) {
|
||
if (this.state !== GState.Playing) return;
|
||
this.spawnTimer += dt;
|
||
if (this.spawnTimer >= SPAWN_INTERVAL) {
|
||
this.spawnTimer = 0;
|
||
this.trySpawn();
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// UI
|
||
// ============================================================
|
||
|
||
private buildUI() {
|
||
// 背景
|
||
this.addSprite('bg', new Color(22, 22, 34, 255), 600, 1200, v3(0, 0, -3));
|
||
|
||
const bh = ROWS * (CELL + PAD); // 棋盘像素高度
|
||
const barY = Math.floor(bh / 2) + 68; // 顶栏 Y
|
||
|
||
// 顶栏背景
|
||
this.addSprite('bar', new Color(38, 38, 52, 230), 480, 52, v3(0, barY, 0));
|
||
|
||
// 标题
|
||
this.addLabel('title', '合成大作战', 18, new Color(180, 180, 190, 255), v3(0, barY + 42, 0));
|
||
|
||
// 得分
|
||
const sn = this.addLabel('score', '得分: 0', 30, Color.WHITE, v3(0, barY, 0));
|
||
this.scoreLabel = sn.getComponent(Label)!;
|
||
|
||
// ---- 游戏结束 ----
|
||
this.overlay = this.addSprite('overlay', new Color(0, 0, 0, 180), 600, 1200, v3(0, 0, 10));
|
||
this.overlay.active = false;
|
||
|
||
this.addLabel('go', '游戏结束', 48, new Color(255, 200, 50, 255), v3(0, 70, 11), this.overlay);
|
||
|
||
const fn = this.addLabel('final', '最终得分: 0', 32, Color.WHITE, v3(0, 0, 11), this.overlay);
|
||
this.finalScore = fn.getComponent(Label)!;
|
||
|
||
// 重新开始按钮
|
||
const btn = this.addSprite('restart', new Color(66, 165, 77, 255), 200, 56, v3(0, -90, 11), this.overlay);
|
||
this.addLabel('bl', '重新开始', 26, Color.WHITE, v3(0, 0, 12), btn);
|
||
btn.on(Node.EventType.TOUCH_END, () => this.onRestart());
|
||
}
|
||
|
||
// ============================================================
|
||
// 棋盘
|
||
// ============================================================
|
||
|
||
private buildBoard() {
|
||
this.boardNode = new Node('board');
|
||
this.board = this.boardNode.addComponent(GameBoard);
|
||
this.board.onScore = (p) => this.addScore(p);
|
||
this.node.addChild(this.boardNode);
|
||
|
||
// 拖拽事件注册在根节点 (Canvas),确保全屏响应
|
||
this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this);
|
||
this.node.on(Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
|
||
this.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this);
|
||
this.node.on(Node.EventType.TOUCH_CANCEL, this.onTouchCancel, this);
|
||
}
|
||
|
||
// ============================================================
|
||
// 游戏流程
|
||
// ============================================================
|
||
|
||
private startGame() {
|
||
this.score = 0;
|
||
this.spawnTimer = 0;
|
||
this.dragging = null;
|
||
this.state = GState.Playing;
|
||
this.scoreLabel.string = '得分: 0';
|
||
this.overlay.active = false;
|
||
for (let i = 0; i < 3; i++) this.board.spawn(randLevel());
|
||
}
|
||
|
||
private trySpawn() {
|
||
if (!this.board.spawn(randLevel()) && this.board.isFull()) this.gameOver();
|
||
}
|
||
|
||
private addScore(p: number) {
|
||
this.score += p;
|
||
this.scoreLabel.string = `得分: ${this.score}`;
|
||
}
|
||
|
||
private gameOver() {
|
||
this.state = GState.Over;
|
||
this.overlay.active = true;
|
||
this.finalScore.string = `最终得分: ${this.score}`;
|
||
}
|
||
|
||
private onRestart() {
|
||
this.board.clearAll();
|
||
this.startGame();
|
||
}
|
||
|
||
// ============================================================
|
||
// 拖拽事件
|
||
// ============================================================
|
||
|
||
/** UI 坐标 → Canvas 本地坐标 */
|
||
private toLocal(t: EventTouch): Vec3 {
|
||
const p = t.getUILocation();
|
||
const tr = this.node.getComponent(UITransform)!;
|
||
return tr.convertToNodeSpaceAR(v3(p.x, p.y, 0));
|
||
}
|
||
|
||
private onTouchStart(evt: EventTouch) {
|
||
if (this.state !== GState.Playing) return;
|
||
const local = this.toLocal(evt);
|
||
const g = this.board.toGrid(local.x, local.y);
|
||
if (!g) return;
|
||
|
||
const block = this.board.get(g.r, g.c);
|
||
if (!block || block.isMerging) return;
|
||
|
||
// 拾取
|
||
this.dragging = block;
|
||
this.dragHome = block.node.getPosition().clone();
|
||
this.dragOff = v3(local.x - this.dragHome.x, local.y - this.dragHome.y, 0);
|
||
|
||
// 临时挂到 Canvas 根节点,确保渲染在最上层
|
||
block.node.parent = this.node;
|
||
block.node.setPosition(this.dragHome.x, this.dragHome.y, 0);
|
||
block.setHighlight(true);
|
||
}
|
||
|
||
private onTouchMove(evt: EventTouch) {
|
||
if (!this.dragging) return;
|
||
const local = this.toLocal(evt);
|
||
this.dragging.node.setPosition(
|
||
local.x - this.dragOff.x,
|
||
local.y - this.dragOff.y,
|
||
0,
|
||
);
|
||
}
|
||
|
||
private onTouchEnd(_evt: EventTouch) {
|
||
if (!this.dragging) return;
|
||
this.resolveDrop();
|
||
}
|
||
|
||
private onTouchCancel() {
|
||
if (!this.dragging) return;
|
||
this.resolveDrop();
|
||
}
|
||
|
||
// ============================================================
|
||
// 释放判定
|
||
// ============================================================
|
||
|
||
private resolveDrop() {
|
||
const block = this.dragging!;
|
||
const pos = block.node.getPosition();
|
||
|
||
// 挂回棋盘节点(坐标系相同,位置不变)
|
||
block.node.parent = this.boardNode;
|
||
|
||
const g = this.board.toGrid(pos.x, pos.y);
|
||
let ok = false;
|
||
|
||
if (g) {
|
||
const { r, c } = g;
|
||
const target = this.board.get(r, c);
|
||
|
||
if (target && target !== block && !target.isMerging) {
|
||
// ---- 目标是个方块 ----
|
||
if (this.isAdj(block, target) && block.level === target.level) {
|
||
// 相邻同等级 → 合并
|
||
ok = this.board.tryMerge(block, target);
|
||
}
|
||
} else if (!target && (r !== block.row || c !== block.col)) {
|
||
// ---- 目标为空位 → 移动 ----
|
||
ok = this.board.move(block, r, c);
|
||
if (ok) {
|
||
// 移动后检查合并
|
||
this.scheduleOnce(() => {
|
||
if (block.isValid) this.board.checkMerge(block);
|
||
}, 0.1);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!ok) {
|
||
// 弹回
|
||
block.playSnapBack(
|
||
{ x: pos.x, y: pos.y },
|
||
{ x: this.dragHome.x, y: this.dragHome.y },
|
||
);
|
||
}
|
||
|
||
block.setHighlight(false);
|
||
this.dragging = null;
|
||
}
|
||
|
||
/** 两方块是否相邻(四方向之一) */
|
||
private isAdj(a: Block, b: Block): boolean {
|
||
return Math.abs(a.row - b.row) + Math.abs(a.col - b.col) === 1;
|
||
}
|
||
|
||
// ============================================================
|
||
// 工具
|
||
// ============================================================
|
||
|
||
private addSprite(
|
||
name: string, color: Color, w: number, h: number,
|
||
pos: Vec3, parent?: Node,
|
||
): Node {
|
||
const n = new Node(name);
|
||
const s = n.addComponent(Sprite);
|
||
s.type = Sprite.Type.SIMPLE;
|
||
s.color = color;
|
||
n.addComponent(UITransform).setContentSize(w, h);
|
||
n.setPosition(pos);
|
||
(parent ?? this.node).addChild(n);
|
||
return n;
|
||
}
|
||
|
||
private addLabel(
|
||
name: string, text: string, size: number, color: Color,
|
||
pos: Vec3, parent?: Node,
|
||
): Node {
|
||
const n = new Node(name);
|
||
const l = n.addComponent(Label);
|
||
l.string = text;
|
||
l.fontSize = size;
|
||
l.color = color;
|
||
const ut = n.addComponent(UITransform);
|
||
// 估算宽高让点击区域适用
|
||
ut.setContentSize(text.length * (size * 0.6) + 20, size + 10);
|
||
n.setPosition(pos);
|
||
(parent ?? this.node).addChild(n);
|
||
return n;
|
||
}
|
||
}
|