

实例:2048 小游戏(Game 2048)|技术:正方形画布、方块渲染、触摸滑动判定、得分与状态提示
一、页面架构总览
Column
├── 标题栏 // 标题 + 得分 + 新游戏按钮
├── 状态提示 // 游戏进行/胜利/失败文案
├── 棋盘画布 // Canvas:正方形,4×4 方块
└── 规则说明 // 滑动合并规则
页面结构与 34 贪吃蛇类似(标题 + 画布 + 提示),核心在正方形棋盘渲染 与滑动交互。
二、正方形棋盘画布
typescript
Canvas(this.ctx)
.width('94%')
.aspectRatio(1) // 正方形棋盘
.borderRadius(12)
.border({ width: 1, color: '#E2E8F0' })
.onReady(() => {
this.canvasW = this.ctx.width;
this.canvasH = this.ctx.height;
this.draw();
})
aspectRatio(1):宽高比 1:1------棋盘天然正方形,任何屏幕都不变形(34 贪吃蛇用过 aspectRatio,此处强调"正方形"语义)。
三、棋盘渲染 ★核心绘制
typescript
private draw(): void {
if (this.canvasW === 0) {
return;
}
this.ctx.clearRect(0, 0, this.canvasW, this.canvasH);
// 棋盘背景(米色)
this.ctx.fillStyle = '#BBADA0';
this.ctx.fillRect(0, 0, this.canvasW, this.canvasH);
const gap = 8;
const cellSize = (this.canvasW - gap * 5) / SIZE; // 4 格 + 5 条间距
for (let r = 0; r < SIZE; r++) {
for (let c = 0; c < SIZE; c++) {
const x = gap + c * (cellSize + gap);
const y = gap + r * (cellSize + gap);
const v = this.board[r][c];
// 方块底(按数值着色)
this.ctx.fillStyle = v === 0 ? '#CDC1B4' : (TILE_COLORS[v] ?? '#3C3A32');
this.ctx.fillRect(x, y, cellSize, cellSize);
// 数字(字号随位数变小)
if (v !== 0) {
this.ctx.font = v >= 100 ? 'bold 26px sans-serif' : 'bold 32px sans-serif';
this.ctx.fillStyle = v <= 4 ? '#776E65' : '#F9F6F2'; // 浅底深字/深底浅字
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
this.ctx.fillText(`${v}`, x + cellSize / 2, y + cellSize / 2);
this.ctx.textAlign = 'start'; // 复位
this.ctx.textBaseline = 'alphabetic'; // 复位
}
}
}
}
棋盘几何 :cellSize = (canvasW - gap*5) / SIZE------画布宽减去 5 条间距(4 格之间有 3 条 + 两侧 2 条 = 5 条 gap),除以 4 得每格尺寸。方块位置 :gap + idx × (cellSize + gap)。
方块着色的三级语义:
| 值 | 背景 | 文字色 |
|---|---|---|
| 0(空) | 灰 #CDC1B4 | --- |
| 2/4 | 浅色(#EEE4DA/#EDE0C8) | 深字 #776E65 |
| 8+ | 深色(TILE_COLORS) | 白字 #F9F6F2 |
v <= 4 ? 深字 : 白字------浅底深字、深底浅字 的对比度规则。TILE_COLORS[v] ?? '#3C3A32' 兜底超 2048 的深色。
字号分级 :v >= 100 ? 26px : 32px------三位数以上缩小字号防溢出(128/256/512)。长数字的布局适配。
文本居中 + 复位 :textAlign='center' + textBaseline='middle' 让数字精确居中,画完复位(全系列一致的状态泄漏防护)。
四、触摸滑动判定
typescript
private onSwipe(e: TouchEvent): void {
const t = e.touches[0];
if (!t) {
return;
}
if (e.type === TouchType.Down) {
this.swipeStart = { x: t.x, y: t.y };
} else if (e.type === TouchType.Up && this.swipeStart !== null) {
const dx = t.x - this.swipeStart.x;
const dy = t.y - this.swipeStart.y;
if (Math.abs(dx) > 30 || Math.abs(dy) > 30) { // 阈值防误触
if (Math.abs(dx) > Math.abs(dy)) {
this.move(dx > 0 ? 1 : 0); // 右/左
} else {
this.move(dy > 0 ? 3 : 2); // 下/上
}
}
this.swipeStart = null;
}
}
滑动判定(Up 时结算) :与 34 贪吃蛇的"Move 实时"不同,2048 用 Down 记录起点 + Up 结算方向 ------滑动完成后执行一次 move(棋盘游戏是"一次滑动一步",不是连续转向)。游戏类型决定交互粒度:贪吃蛇连续控制用 Move,2048 离散移动用 Up。
阈值 30px :位移超 30 才算有效滑动(防误触)。主导轴判定 :|dx| > |dy| 取水平方向------与 34 同款算法。
五、得分与状态提示
typescript
@State score: number = 0;
@State statusText: string = '滑动棋盘开始游戏';
Text(`得分 ${this.score}`)... // 标题栏橙色
Text(this.statusText)
.fontSize(14)
.fontColor(this.statusText.startsWith('💀') || this.statusText.startsWith('🎉') ? '#EF4444' : '#999999')
状态文案三态:
| 状态 | 文案 | 颜色 |
|---|---|---|
| 进行中 | (初始提示/无) | 灰 |
| 胜利 | 🎉 2048!你赢了! | 红(警示强调) |
| 失败 | 💀 游戏结束,得分 X | 红 |
startsWith('💀') || startsWith('🎉') 判断终局态------emoji 前缀作为状态标志的轻量技巧。
六、新游戏
typescript
Button('新游戏').fontSize(13).height(34).backgroundColor('#8B5CF6')
.onClick(() => this.newGame())
newGame 完整重置 :棋盘清零 + 分数清零 + 状态复位 + 生成两个初始方块 + 绘制------一个方法完成全部初始化(34 的 startGame 同款)。
七、交互流程图
启动 ──► aboutToAppear ──► newGame(棋盘 + 2 方块 + 绘制)
│
Canvas onReady ──► draw
│
触摸滑动 ──► onSwipe
│ ├─ Down 记录起点
│ ├─ Up 结算方向(阈值 + 主导轴)
│ └─ move(方向)
│ ├─ slideLine × 4 行
│ ├─ 有效性判定
│ ├─ 有效 → 更新棋盘 + 计分 + spawnTile + checkEnd
│ └─ draw
│
点"新游戏" ──► newGame
八、UI 技术要点对照表
| 技术点 | 实现方式 | 生产价值 |
|---|---|---|
| 正方形画布 | aspectRatio(1) | 棋盘不变形 |
| 棋盘几何 | cellSize = (W - 5gap)/4 | 自适应尺寸 |
| 三色层级 | 空/浅/深 + 字色对比 | 视觉分级 |
| 字号分级 | >=100 缩小 | 长数字适配 |
| Up 结算滑动 | Down 起点 + Up 方向 | 离散移动 |
| 阈值防误触 | 30px | 交互稳健 |
| emoji 状态标志 | startsWith 判断 | 轻量状态机 |
| 完整重置 | newGame 收口 | 初始化一致 |
九、本篇小结
本篇完成了 2048 的界面与交互:aspectRatio 正方形棋盘 、三色层级方块渲染(背景色 + 字色对比 + 字号分级) 、Up 结算的滑动判定 、得分与终局提示 。UI 层的核心是"棋盘几何自适应"------cellSize 由画布宽推导,任何屏幕正方形棋盘都铺满。
下一篇《完整代码与运行效果》将给出 Game2048Page 全量源码解读、操作剧本演示,以及"网格棋盘游戏"通用模板抽象------与 34 贪吃蛇的模板合并,覆盖全部棋盘类游戏。
十、UI 深度扩展
1. 滑动动画
move 后不立即绘制新棋盘,而是记录每格"从哪来 → 到哪去",帧循环插值移动 + 合并缩放。插值动画是 2048 质感的来源,实现 = 动画帧 + 位移数组。
2. 最高分
preferences 存最高分(34 已演示),标题栏显示"得分 X · 最高 Y"。跨局激励。
3. 撤销
历史棋盘栈(每次 move 前 push 深拷贝),撤销按钮 pop 恢复。悔棋是策略游戏标配。
4. 主题切换
深色主题 / 经典米色切换:TILE_COLORS 换一套映射。主题 = 颜色配置。
5. FAQ
Q1:为什么 2048 用 Up 结算滑动而贪吃蛇用 Move?
A:贪吃蛇是"持续控制"(手指方向即蛇方向,实时响应);2048 是"离散操作"(一次滑动 = 一步)。交互粒度由游戏规则决定------连续用 Move,离散用 Up 结算。
Q2:字号分级为什么以 100 为界?
A:三位数(128/256/512)在 32px 下会溢出格子。100 是"是否三位数"的阈值。四位数(2048)用 26px 仍偏大------可再加 >=1000 ? 22px 档。字号随位数阶梯降。
Q3:方块颜色表没有 4096 会怎样?
A:?? '#3C3A32' 兜底深色------超过 2048 的方块显示深灰底白字,不报错。映射表 + 兜底是配置型代码的防御。
Q4:滑动阈值 30 比贪吃蛇的 20 大,为什么?
A:2048 的滑动是"整步操作",需要更明确的意图(误触代价高------一次误滑可能毁局);贪吃蛇滑动是"方向微调",阈值小响应快。阈值随操作重要度调整。
Q5:棋盘居中吗?
A:画布 94% 宽 + aspectRatio(1),在 Column 中默认水平居中(Column 子元素居中)。布局默认行为即可。
十一、下篇预告
最后一篇《完整代码与运行效果》将:给出 Game2048Page.ets 完整源码逐段解读;演示滑动合并、生成新方块、计分、胜负判定的完整操作链;抽象"网格棋盘游戏"通用模板,并与 34 贪吃蛇模板合并成"棋盘游戏全家桶"。