手把手教,HarmonyOS6.0纯血版2048小游戏,2小时搞定

1 阅读10分钟

大家好,我是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),以及发布到鸿蒙应用市场的技巧,成长路上有我相伴,君志所向,一往无前!