CC 3.8.8 中 UI 渲染管线要求 Graphics 节点自身必须有 UITransform 组件, 否则不会提交渲染。所有 Graphics 节点(方块/棋盘/UI)均已补充。
198 lines
5.6 KiB
TypeScript
198 lines
5.6 KiB
TypeScript
/**
|
||
* 棋盘 — 8×12 网格管理、生成、移动、合并
|
||
*/
|
||
import { _decorator, Component, Node, Graphics, Color, UITransform, Vec3, v3 } from 'cc';
|
||
import { ROWS, COLS, CELL, PAD, MAX_LEVEL, MERGE_BASE_SCORE } from './BlockConfig';
|
||
import { Block } from './Block';
|
||
|
||
const { ccclass } = _decorator;
|
||
|
||
@ccclass('GameBoard')
|
||
export class GameBoard extends Component {
|
||
private grid: (Block | null)[][] = [];
|
||
private originX = 0;
|
||
private originY = 0;
|
||
|
||
onScore: ((pts: number) => void) | null = null;
|
||
|
||
onLoad() {
|
||
const w = COLS * (CELL + PAD) + PAD;
|
||
const h = ROWS * (CELL + PAD) + PAD;
|
||
this.originX = -w / 2 + PAD;
|
||
this.originY = -h / 2 + PAD;
|
||
|
||
this.drawBg(w, h);
|
||
this.drawCells();
|
||
this.initGrid();
|
||
}
|
||
|
||
private initGrid() {
|
||
this.grid = [];
|
||
for (let r = 0; r < ROWS; r++) {
|
||
this.grid[r] = [];
|
||
for (let c = 0; c < COLS; c++) {
|
||
this.grid[r][c] = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 用 Graphics 画棋盘背景 */
|
||
private drawBg(w: number, h: number) {
|
||
const g = new Node('bg');
|
||
const gfx = g.addComponent(Graphics);
|
||
g.addComponent(UITransform).setContentSize(w + 16, h + 16);
|
||
gfx.fillColor = new Color(28, 28, 40, 220);
|
||
gfx.roundRect(-w / 2 - 8, -h / 2 - 8, w + 16, h + 16, 8);
|
||
gfx.fill();
|
||
this.node.addChild(g);
|
||
}
|
||
|
||
/** 用 Graphics 画所有格子底纹 */
|
||
private drawCells() {
|
||
const g = new Node('cells');
|
||
const gfx = g.addComponent(Graphics);
|
||
g.addComponent(UITransform).setContentSize(COLS * (CELL + PAD), ROWS * (CELL + PAD));
|
||
for (let r = 0; r < ROWS; r++) {
|
||
for (let c = 0; c < COLS; c++) {
|
||
const p = this.toWorld(r, c);
|
||
const s = CELL - 4;
|
||
const h = s / 2;
|
||
gfx.fillColor = new Color(60, 60, 60, 80);
|
||
gfx.roundRect(p.x - h, p.y - h, s, s, 4);
|
||
gfx.fill();
|
||
}
|
||
}
|
||
this.node.addChild(g);
|
||
}
|
||
|
||
// ============================================================
|
||
// 坐标
|
||
// ============================================================
|
||
|
||
toWorld(r: number, c: number): Vec3 {
|
||
return v3(
|
||
this.originX + c * (CELL + PAD) + CELL / 2,
|
||
this.originY + r * (CELL + PAD) + CELL / 2,
|
||
0,
|
||
);
|
||
}
|
||
|
||
toGrid(x: number, y: number): { r: number; c: number } | null {
|
||
const c = Math.round((x - this.originX) / (CELL + PAD));
|
||
const r = Math.round((y - this.originY) / (CELL + PAD));
|
||
if (r < 0 || r >= ROWS || c < 0 || c >= COLS) return null;
|
||
return { r, c };
|
||
}
|
||
|
||
// ============================================================
|
||
// 查询
|
||
// ============================================================
|
||
|
||
isEmpty(r: number, c: number) {
|
||
return r >= 0 && r < ROWS && c >= 0 && c < COLS && !this.grid[r][c];
|
||
}
|
||
|
||
get(r: number, c: number): Block | null {
|
||
if (r < 0 || r >= ROWS || c < 0 || c >= COLS) return null;
|
||
return this.grid[r][c];
|
||
}
|
||
|
||
neighbors(r: number, c: number): Block[] {
|
||
const out: Block[] = [];
|
||
for (const [dr, dc] of [[-1, 0], [1, 0], [0, -1], [0, 1]]) {
|
||
const b = this.get(r + dr, c + dc);
|
||
if (b) out.push(b);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
// ============================================================
|
||
// 方块操作
|
||
// ============================================================
|
||
|
||
spawn(level: number): Block | null {
|
||
const empty: { r: number; c: number }[] = [];
|
||
for (let r = 0; r < ROWS; r++)
|
||
for (let c = 0; c < COLS; c++)
|
||
if (this.isEmpty(r, c)) empty.push({ r, c });
|
||
if (!empty.length) return null;
|
||
const g = empty[Math.floor(Math.random() * empty.length)];
|
||
return this._create(g.r, g.c, level);
|
||
}
|
||
|
||
_create(r: number, c: number, level: number): Block {
|
||
const n = new Node('block');
|
||
const b = n.addComponent(Block);
|
||
b.init(r, c, level);
|
||
n.setPosition(this.toWorld(r, c));
|
||
this.node.addChild(n);
|
||
this.grid[r][c] = b;
|
||
return b;
|
||
}
|
||
|
||
move(block: Block, nr: number, nc: number): boolean {
|
||
if (!this.isEmpty(nr, nc)) return false;
|
||
this.grid[block.row][block.col] = null;
|
||
block.row = nr;
|
||
block.col = nc;
|
||
this.grid[nr][nc] = block;
|
||
block.node.setPosition(this.toWorld(nr, nc));
|
||
return true;
|
||
}
|
||
|
||
remove(block: Block) {
|
||
this.grid[block.row][block.col] = null;
|
||
block.node.destroy();
|
||
}
|
||
|
||
// ============================================================
|
||
// 合并
|
||
// ============================================================
|
||
|
||
tryMerge(a: Block, b: Block): boolean {
|
||
if (a.level !== b.level || a.isMerging || b.isMerging) return false;
|
||
if (Math.abs(a.row - b.row) + Math.abs(a.col - b.col) !== 1) return false;
|
||
this._doMerge(a, b);
|
||
return true;
|
||
}
|
||
|
||
checkMerge(block: Block) {
|
||
if (block.isMerging) return;
|
||
for (const nb of this.neighbors(block.row, block.col)) {
|
||
if (nb.level === block.level && !nb.isMerging) {
|
||
this._doMerge(block, nb);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
private _doMerge(a: Block, b: Block) {
|
||
a.isMerging = true;
|
||
b.isMerging = true;
|
||
const lv = Math.min(a.level + 1, MAX_LEVEL);
|
||
this.remove(b);
|
||
a.upgradeTo(lv);
|
||
if (this.onScore) this.onScore(lv * MERGE_BASE_SCORE);
|
||
this.scheduleOnce(() => this.checkMerge(a), 0.25);
|
||
}
|
||
|
||
// ============================================================
|
||
// 全局
|
||
// ============================================================
|
||
|
||
isFull(): boolean {
|
||
for (let r = 0; r < ROWS; r++)
|
||
for (let c = 0; c < COLS; c++)
|
||
if (!this.grid[r][c]) return false;
|
||
return true;
|
||
}
|
||
|
||
clearAll() {
|
||
for (let r = 0; r < ROWS; r++)
|
||
for (let c = 0; c < COLS; c++) {
|
||
const b = this.grid[r][c];
|
||
if (b) { b.node.destroy(); this.grid[r][c] = null; }
|
||
}
|
||
}
|
||
}
|