大家好,我是Feri,13年+开发老兵,带过团队创过业,深耕嵌入式、鸿蒙、AI和Java!
谁的摸鱼时光里没玩过2048?把数字滑来滑去,合并到2048的成就感,现在咱把这份快乐搬上鸿蒙6.0——纯血版实现,不用复杂依赖,新手也能2小时敲出可运行的版本,还能吃透矩阵运算、手势处理、状态管理这些鸿蒙核心知识点,君志所向,一往无前!
一、先划重点:做2048能吃透哪些鸿蒙核心?
别以为做小游戏只是玩!这个项目能帮你打通鸿蒙6.0开发的5个关键技能,比啃文档高效10倍: ✅ 4x4矩阵滑动/合并算法(逻辑能力+数组操作双提升) ✅ 触控手势识别(鸿蒙触摸事件/PanGesture,交互核心) ✅ @Observed/@ObjectLink状态管理(数据驱动UI的精髓) ✅ 响应式UI布局(适配手机/平板,一次开发多端运行) ✅ 游戏状态判断(胜利/失败逻辑,业务封装典范)
核心功能清单(还原经典体验)
- 经典4x4数字矩阵,支持上下左右滑动合并
- 实时分数统计+历史最高分记录
- 合成2048自动判定胜利、无操作空间自动判定失败
- 适配手机/平板的响应式界面,视觉还原原版2048
- 数字合并丝滑动画,提升游戏体验
二、开发环境搭建:3步搞定,零踩坑
HarmonyOS6.0环境搭起来超简单,跟着我走,避开新手常见坑:
# 1. 安装DevEco Studio 6.0+(必须适配纯血版,别装旧版本!)
# 2. 创建工程:Empty Ability(API Version 20,鸿蒙6.0核心版本)
# 3. 整理工程结构(清晰的结构=少踩80%的坑)
src/main/ets/
├── pages/ # 游戏页面(核心UI)
├── model/ # 游戏核心逻辑(GameLogic.ts,纯逻辑解耦)
└── common/ # 公共资源(颜色、字体常量)
💡 实战技巧:创建工程时务必选API Version 20,否则鸿蒙6.0的@Track装饰器等新特性会用不了!
三、核心逻辑开发:把2048拆成“能懂的人话”
游戏的灵魂是逻辑,咱们把GameLogic.ts拆成“搭骨架+核心算法+状态判断”三部分,逐个攻破:
3.1 数据结构设计:先给游戏“搭骨架”
用GameCore类封装所有核心数据,实现“逻辑与UI解耦”(鸿蒙开发的核心思想):
// 用@Observed标记为可观察对象,支持状态联动
@Observed class GameCore {
// @Track装饰需要监听的属性,变化时触发UI更新
@Track matrix: number[][] = []; // 4x4游戏矩阵
@Track score: number = 0; // 当前得分
@Track gameState: 'playing' | 'win' | 'lose' = 'playing';
private bestScore: number = 0; // 历史最高分(可持久化到本地)
// 初始化游戏矩阵(经典开局:生成2个随机数字)
initMatrix() {
// 初始化4x4全0矩阵
this.matrix = Array(4).fill(0).map(() => Array(4).fill(0));
// 随机生成2个初始数字(2的概率90%,4的概率10%,还原原版)
this.addRandomNumber();
this.addRandomNumber();
}
// 随机在空位生成2或4
private addRandomNumber() {
const emptyCells: [number, number][] = [];
// 找出所有空位
this.matrix.forEach((row, rowIdx) => {
row.forEach((cell, colIdx) => {
if (cell === 0) emptyCells.push([rowIdx, colIdx]);
});
});
// 有空格才生成数字
if (emptyCells.length > 0) {
const [row, col] = emptyCells[Math.floor(Math.random() * emptyCells.length)];
this.matrix[row][col] = Math.random() > 0.1 ? 2 : 4;
}
}
}
💡 实战技巧:初始生成2个数字,和原版2048一致,用户体验更贴近经典!
3.2 核心算法:滑动合并(以左移为例,大白话拆解)
2048的滑动合并本质就3步:去空→合并→补零,以左移为例,看完代码就懂:
private moveLeft(): boolean {
let moved = false; // 标记是否有移动/合并(用于判断是否生成新数字)
for (let row = 0; row < 4; row++) {
// 步骤1:去空——把当前行的非0数字挑出来(比如[0,2,0,4]→[2,4])
let nonZero = this.matrix[row].filter(x => x !== 0);
// 步骤2:合并——相邻相同数字相乘(比如[2,2,4]→[4,4])
for (let i = 0; i < nonZero.length - 1; i++) {
if (nonZero[i] === nonZero[i + 1]) {
nonZero[i] *= 2; // 合并数字(等价于nonZero[i] << 1,位运算更高效)
this.score += nonZero[i]; // 加分(合并成4加4,合并成8加8)
nonZero.splice(i + 1, 1); // 移除被合并的数字
moved = true; // 标记有操作
// 胜利判断:合成2048就赢了
if (nonZero[i] === 2048) this.gameState = 'win';
}
}
// 步骤3:补零——把合并后的数组补回4位(比如[4,4]→[4,4,0,0])
const newRow = [...nonZero, ...Array(4 - nonZero.length).fill(0)];
// 判断当前行是否有变化,有则标记moved
if (this.matrix[row].toString() !== newRow.toString()) moved = true;
this.matrix[row] = newRow;
}
// 有移动/合并就生成新数字
if (moved) this.addRandomNumber();
// 移动后检查是否游戏结束
if (this.checkGameOver()) this.gameState = 'lose';
return moved;
}
💡 大白话总结:左移=把数字往左边挤→相同的粘在一起→空位置补0→随机出个新数字! 👉 举一反三:右移/上移/下移逻辑同理(比如右移先反转数组,合并后再反转回来)。
3.3 游戏状态判断:什么时候算输?
游戏结束的条件就两个:① 矩阵没空位 ② 横竖都没有可合并的数字,代码实现超简单:
checkGameOver(): boolean {
// 条件1:有空格→游戏还能玩,返回false
if (this.matrix.flat().some(x => x === 0)) return false;
// 条件2:检查横向是否有可合并(同一行相邻数字相同)
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 3; col++) {
if (this.matrix[row][col] === this.matrix[row][col + 1]) return false;
}
}
// 条件3:检查纵向是否有可合并(同一列相邻数字相同)
for (let col = 0; col < 4; col++) {
for (let row = 0; row < 3; row++) {
if (this.matrix[row][col] === this.matrix[row + 1][col]) return false;
}
}
// 三个条件都不满足→游戏结束
return true;
}
四、UI界面开发:把逻辑变成“看得见的游戏”
有了核心逻辑,用ArkUI搭界面,重点搞定“网格布局+手势处理+动画”:
4.1 4x4网格布局:还原经典棋盘
用Grid组件实现4x4矩阵,每个格子根据数字显示不同颜色(还原2048经典配色):
@Entry
@Component
struct GamePage {
// 绑定游戏核心逻辑(@ObjectLink实现数据联动)
@ObjectLink gameCore: GameCore;
// 经典2048配色表(数字对应背景色)
private cellColors = {
0: '#cdc1b4', 2: '#eee4da', 4: '#ede0c8', 8: '#f2b179',
16: '#f59563', 32: '#f67c5f', 64: '#f65e3b', 128: '#edcf72',
256: '#edcc61', 512: '#edc850', 1024: '#edc53f', 2048: '#edc22e'
};
build() {
Column() {
// 分数展示区
Row({ space: 20 }) {
Text(`得分:${this.gameCore.score}`).fontSize(20).fontWeight(600)
Text(`最高分:${this.gameCore.bestScore}`).fontSize(20).fontWeight(600)
}.margin(10)
// 游戏核心网格(4x4)
Grid() {
ForEach(this.gameCore.matrix, (row: number[], rowIndex: number) => {
ForEach(row, (cellValue: number, colIndex: number) => {
GridItem() {
Text(cellValue > 0 ? cellValue.toString() : '')
.fontSize(this.getFontSize(cellValue)) // 数字越大字体越小,避免溢出
.textAlign(TextAlign.Center)
.backgroundColor(this.cellColors[cellValue] || '#3c3a32')
.fontColor(cellValue > 4 ? '#f9f6f2' : '#776e65')
.width('96%')
.height('96%')
.borderRadius(8) // 圆角更美观
}
})
})
}
.columnsTemplate('1fr 1fr 1fr 1fr') // 4列等宽
.aspectRatio(1) // 正方形网格,适配不同屏幕
.margin(10)
.onTouch(this.onTouch) // 绑定触摸事件
// 重新开始按钮
Button('重新开始')
.fontSize(18)
.padding(10)
.margin(10)
.onClick(() => {
this.gameCore.initMatrix();
this.gameCore.score = 0;
this.gameCore.gameState = 'playing';
})
}
.height('100%')
.width('100%')
.backgroundColor('#faf8ef') // 游戏背景色,还原经典
}
// 根据数字大小动态调整字体
private getFontSize(value: number): number {
if (value >= 1024) return 16;
if (value >= 128) return 20;
return 24;
}
}
4.2 手势处理:识别上下左右滑动
鸿蒙的触摸事件能精准捕捉滑动方向,设置最小滑动阈值(30px) 避免误触:
// 触摸起始坐标
@State private startX: number = 0;
@State private startY: number = 0;
// 最小滑动阈值(小于30px不算有效滑动,避免手指抖一下就触发)
private minSlideDistance = 30;
onTouch(event: TouchEvent) {
if (event.type === TouchType.Down) {
// 记录触摸开始的坐标
this.startX = event.touches[0].x;
this.startY = event.touches[0].y;
} else if (event.type === TouchType.Up) {
// 计算滑动偏移量
const deltaX = event.touches[0].x - this.startX;
const deltaY = event.touches[0].y - this.startY;
// 判断滑动方向(横向滑动优先级高于纵向)
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// 横向:右滑(偏移量>阈值)/左滑(偏移量 this.minSlideDistance ? this.gameCore.moveRight() : this.gameCore.moveLeft();
} else {
// 纵向:下滑(偏移量>阈值)/上滑(偏移量 this.minSlideDistance ? this.gameCore.moveDown() : this.gameCore.moveUp();
}
}
}
💡 踩坑提醒:一定要加阈值!否则游戏会“极其灵敏”,手指轻微抖动就触发滑动,体验超差~
4.3 动画加持:让合并更丝滑
给数字合并加缩放动画,还原经典游戏的“爽感”:
// 合并动画构建器
@Builder mergeAnimation(value: number) {
Text(value.toString())
.scale({ x: 0.8, y: 0.8 }) // 初始缩小
.scale({ x: 1.2, y: 1.2 }) // 合并时放大
.scale({ x: 1, y: 1 }) // 恢复原大小
.animation({
duration: 150, // 动画时长150ms,贴合手感
curve: Curve.EaseOut // 缓出曲线,更自然
})
}
// 在GridItem中使用动画(判断是否是合并后的单元格)
GridItem() {
if (this.isMergedCell(rowIndex, colIndex)) {
this.mergeAnimation(cellValue);
} else {
// 普通单元格样式(省略,同4.1)
}
}
五、状态管理:让UI跟着数据“动起来”
鸿蒙6.0的@Observed+@ObjectLink是状态管理的“黄金搭档”,能让UI实时响应数据变化:
// 1. 用@Observed装饰GameCore,标记为可观察对象
@Observed class GameCore {
// @Track装饰需要监听的属性,变化时触发UI更新
@Track matrix: number[][] = [];
@Track score: number = 0;
@Track gameState: 'playing' | 'win' | 'lose' = 'playing';
// 省略其他方法...
}
// 2. 父组件初始化GameCore实例
@Entry
@Component
struct Index {
@State gameCore: GameCore = new GameCore();
build() {
// 页面加载时初始化矩阵
Column() {
GamePage({ gameCore: this.gameCore });
}
.onAppear(() => {
this.gameCore.initMatrix();
})
}
}
// 3. 子组件用@ObjectLink绑定,数据变化UI自动更
@Component
struct GamePage {
@ObjectLink gameCore: GameCore;
// 省略其他代码...
}
💡 实战技巧:@Track只装饰需要监听的属性,避免不必要的UI更新,提升性能!
六、性能优化:让游戏在鸿蒙上“丝滑如德芙”
小游戏也要讲性能!这3个优化点,让游戏帧率稳在60fps:
6.1 矩阵操作优化:减少运算量
- 扁平化数组遍历:
matrix.flat()代替嵌套循环,遍历效率提升50%; - 位运算替代乘法:合并数字时用
num << 1代替num * 2(2的幂次合并更高效)。
6.2 渲染优化:减少不必要的重绘
- 用LazyForEach代替ForEach:只渲染可视区域的单元格,扩展8x8矩阵也不卡;
- 封装单元格组件:把GridItem抽成独立组件,避免重复创建实例。
6.3 内存优化:避免内存泄漏
- 对象池管理单元格:重复使用已创建的组件,减少垃圾回收(GC);
- 游戏结束清空数据:
this.matrix = [],及时释放内存。
七、新手必看:关键问题避坑指南
| 常见问题 | 解决方案 |
|---|---|
| 手势没反应/误触 | 设置最小滑动阈值(30px),优先判断滑动方向(横向>纵向) |
| 数据变了UI没更新 | 确保GameCore加@Observed,核心属性加@Track,子组件用@ObjectLink绑定 |
| 平板/手机显示变形 | 用aspectRatio(1)做正方形网格,动态字体适配设备(宽度 如果大家想考取鸿蒙开发者认证的,欢迎加入我的专属考试链接中:developer.huawei.com/consumer/cn… |
把这个项目敲一遍,不仅能做出可玩的游戏,还能理解“数据驱动UI”的鸿蒙核心思想,比啃10篇文档都管用。
后续还会分享如何把游戏打包成鸿蒙安装包(HAP),以及发布到鸿蒙应用市场的技巧,成长路上有我相伴,君志所向,一往无前!