Files
cocos-hehe/assets/Scripts/GameController.ts
leiyuwei 1342dfb078 fix: 所有 Graphics 节点添加 UITransform,修复 CC 3.8.8 空白问题
CC 3.8.8 中 UI 渲染管线要求 Graphics 节点自身必须有 UITransform 组件,
否则不会提交渲染。所有 Graphics 节点(方块/棋盘/UI)均已补充。
2026-06-16 13:46:05 +08:00

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;
}
}