CC 3.8.8 中 UI 渲染管线要求 Graphics 节点自身必须有 UITransform 组件, 否则不会提交渲染。所有 Graphics 节点(方块/棋盘/UI)均已补充。
287 lines
8.0 KiB
TypeScript
287 lines
8.0 KiB
TypeScript
/**
|
|
* 游戏主控 — UI、游戏流程、拖拽输入
|
|
* 所有 UI 用 Graphics 绘制,不依赖 Sprite 纹理
|
|
*/
|
|
import {
|
|
_decorator, Component, Node, Label, Color, Graphics, 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;
|
|
|
|
enum GState { Idle, Playing, Over }
|
|
|
|
/** Canvas 设计尺寸 */
|
|
const CW = 580;
|
|
const CH = 960;
|
|
|
|
@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();
|
|
private dragOff = v3();
|
|
|
|
private scoreLabel!: Label;
|
|
private overlay!: Node;
|
|
private finalScore!: Label;
|
|
|
|
// ============================================================
|
|
// 生命周期
|
|
// ============================================================
|
|
|
|
onLoad() {
|
|
this.drawUI();
|
|
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 — 全部用 Graphics 绘制
|
|
// ============================================================
|
|
|
|
private drawUI() {
|
|
// ---- 全屏背景 ----
|
|
this.gfx('bg', new Color(22, 22, 34, 255), (g) => {
|
|
g.rect(-CW / 2, -CH / 2, CW, CH);
|
|
}, -3);
|
|
|
|
const bh = ROWS * (CELL + PAD); // 棋盘视觉高度
|
|
const barY = Math.floor(bh / 2) + 68; // 顶栏 Y
|
|
|
|
// ---- 顶栏 ----
|
|
this.gfx('bar', new Color(38, 38, 52, 230), (g) => {
|
|
g.roundRect(-240, barY - 26, 480, 52, 10);
|
|
});
|
|
|
|
// ---- 标题 ----
|
|
this.mkLabel('title', '合成大作战', 18, new Color(180, 180, 190, 255), v3(0, barY + 42, 0));
|
|
|
|
// ---- 得分 ----
|
|
const sn = this.mkLabel('score', '得分: 0', 30, Color.WHITE, v3(0, barY, 0));
|
|
this.scoreLabel = sn.getComponent(Label)!;
|
|
|
|
// ---- 游戏结束遮罩 ----
|
|
this.overlay = new Node('overlay');
|
|
this.overlay.addComponent(UITransform).setContentSize(CW, CH);
|
|
const og = this.overlay.addComponent(Graphics);
|
|
og.fillColor = new Color(0, 0, 0, 180);
|
|
og.rect(-CW / 2, -CH / 2, CW, CH);
|
|
og.fill();
|
|
this.overlay.setPosition(0, 0, 10);
|
|
this.overlay.active = false;
|
|
this.node.addChild(this.overlay);
|
|
|
|
this.mkLabel('go', '游戏结束', 48, new Color(255, 200, 50, 255), v3(0, 70, 11), this.overlay);
|
|
|
|
const fn = this.mkLabel('final', '最终得分: 0', 32, Color.WHITE, v3(0, 0, 11), this.overlay);
|
|
this.finalScore = fn.getComponent(Label)!;
|
|
|
|
// ---- 重新开始按钮 ----
|
|
const btn = new Node('restart');
|
|
btn.addComponent(UITransform).setContentSize(200, 56);
|
|
const bg = btn.addComponent(Graphics);
|
|
bg.fillColor = new Color(66, 165, 77, 255);
|
|
bg.roundRect(-100, -28, 200, 56, 12);
|
|
bg.fill();
|
|
btn.setPosition(0, -90, 11);
|
|
this.overlay.addChild(btn);
|
|
|
|
this.mkLabel('bl', '重新开始', 26, Color.WHITE, v3(0, 0, 12), btn);
|
|
|
|
btn.on(Node.EventType.TOUCH_END, () => this.onRestart());
|
|
}
|
|
|
|
/** 快速创建 Graphics 节点 */
|
|
private gfx(
|
|
name: string, color: Color,
|
|
draw: (g: Graphics) => void, z = 0, parent?: Node,
|
|
): Node {
|
|
const n = new Node(name);
|
|
const g = n.addComponent(Graphics);
|
|
n.addComponent(UITransform).setContentSize(CW, CH);
|
|
g.fillColor = color;
|
|
draw(g);
|
|
g.fill();
|
|
n.setPosition(0, 0, z);
|
|
(parent ?? this.node).addChild(n);
|
|
return n;
|
|
}
|
|
|
|
/** 快速创建 Label */
|
|
private mkLabel(
|
|
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;
|
|
n.addComponent(UITransform).setContentSize(text.length * (size * 0.6) + 20, size + 10);
|
|
n.setPosition(pos);
|
|
(parent ?? this.node).addChild(n);
|
|
return n;
|
|
}
|
|
|
|
// ============================================================
|
|
// 棋盘
|
|
// ============================================================
|
|
|
|
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);
|
|
|
|
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();
|
|
}
|
|
|
|
// ============================================================
|
|
// 拖拽
|
|
// ============================================================
|
|
|
|
private toLocal(t: EventTouch): Vec3 {
|
|
const p = t.getUILocation();
|
|
return this.node.getComponent(UITransform)!.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;
|
|
}
|
|
}
|