Files
cocos-hehe/assets/Scripts/GameController.ts
leiyuwei e9e6ddb8a7 重构: 合成大作战改为拖拽合成机制
- 全部代码重写,改为拖拽合成(drag-and-drop merge)
- 拖拽方块到相邻同等级方块上完成合成
- 拖到空格移动,拖到无效位置弹回
- Cocos Creator 3.8.8 全面适配
- 移除 GameManager.ts → 改为 GameController.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 12:12:39 +08:00

284 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 游戏主控 — 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;
}
}