当前位置: 首页 > article >正文

鸿蒙 ArkUI 实现 2048 小游戏

2048 是一款经典的益智游戏,玩家通过滑动屏幕合并相同数字的方块,最终目标是合成数字 2048。本文基于鸿蒙 ArkUI 框架,详细解析其实现过程,帮助开发者理解如何利用声明式 UI 和状态管理构建此类游戏。

一、核心数据结构与状态管理

1. 游戏网格与得分

游戏的核心是一个 4x4 的二维数组,用于存储每个格子的数字。通过 @State 装饰器管理网格状态,确保数据变化时 UI 自动刷新:

@State grid: number[][] = Array(4).fill(0).map(() => Array(4).fill(0));
@State score: number = 0; // 当前得分
@State bestScore: number = 0; // 历史最高分
2. 初始化游戏

initGame 方法负责重置网格、添加初始方块并重置得分。通过 addNewTile 在随机空位生成新方块(90% 概率生成 2,10% 概率生成 4):

initGame() {
  this.grid = this.grid.map(() => Array(4).fill(0));
  this.addNewTile();
  this.addNewTile();
  this.score = 0;
}

二、滑动逻辑与合并算法

1. 方向处理与矩阵旋转

游戏支持 上下左右四个方向的滑动。为简化代码逻辑,通过矩阵旋转将不同方向的移动统一转换为 左移操作

  • 左移:直接处理每一行。
  • 右移:将行反转后左移,再反转回来。
  • 上移/下移:旋转矩阵为行,处理后恢复为列。
// 矩阵旋转辅助方法
const rotate = (matrix: number[][]) => {
  return matrix[0].map((_, i) => matrix.map(row => row[i]).reverse());
};
2. 单行合并逻辑

每行处理分为三步:

  1. 移除空格:过滤出非零数字。
  2. 合并相同数字:相邻相同数字合并,并累加得分。
  3. 补齐长度:填充零至长度为 4。
const moveRow = (row: number[]) => {
  let newRow = row.filter(cell => cell !== 0);
  for (let i = 0; i < newRow.length - 1; i++) {
    if (newRow[i] === newRow[i + 1]) {
      newRow[i] *= 2;
      this.score += newRow[i]; // 得分累加
      newRow.splice(i + 1, 1);
    }
  }
  return [...newRow, ...Array(4 - newRow.length).fill(0)];
};

三、游戏结束判断

游戏结束的条件是 网格填满且无相邻可合并的方块。通过以下步骤检测:

  1. 检查是否有空格:存在空格则游戏未结束。
  2. 横向检测:遍历每一行,检查是否有相邻相同数字。
  3. 纵向检测:遍历每一列,检查是否有相邻相同数字。
isGameOver(): boolean {
  if (this.grid.some(row => row.includes(0))) return false;
  // 横向和纵向检测逻辑
  // ...
  return true;
}

四、UI 实现与交互设计

1. 网格渲染

使用 Grid 组件动态生成 4x4 网格,每个 GridItem 根据数字值显示不同背景色和文字颜色:

Grid() {
  ForEach(this.grid, (row: number[], i) => {
    ForEach(row, (value: number, j) => {
      GridItem() {
        Text(value ? `${value}` : '')
          .backgroundColor(this.getTileColor(value))
          .fontColor(this.getTextColor(value));
      }
    })
  })
}

2. 触摸事件处理
通过 onTouch 监听滑动事件,计算起始和结束坐标的差值,判断滑动方向:

onTouch((event) => {
  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;
    // 判断方向并调用 move 方法
  }
});

五、本地存储与动画效果

1. 最高分持久化

使用 PreferencesUtil 存储和读取最高分,确保数据在应用重启后保留:

aboutToAppear() {
  this.bestScore = PreferencesUtil.getNumberSync("bestScore");
}

// 更新最高分
if (this.score > this.bestScore) {
  PreferencesUtil.putSync('bestScore', this.score);
}
2. 动画与视觉效果

每个方块的文字变化添加了 150ms 的渐变动画,提升用户体验:

Text(value ? `${value}` : '')
  .animation({ duration: 150, curve: Curve.EaseOut });

六、完整代码

import { HashMap } from '@kit.ArkTS'
import { AppUtil, PreferencesUtil, ToastUtil } from '@pura/harmony-utils'

// index.ets
@Entry
@Component
struct Game2048 {
  @State grid: number[][] = Array(4).fill(0).map(() => Array(4).fill(0)) // 4x4游戏网格
  @State score: number = 0 // 当前得分
  @State bestScore: number = 0 // 历史最高分
  private startX: number = 0 // 触摸起始X坐标
  private startY: number = 0 // 触摸起始Y坐标

  // 生命周期方法:页面即将显示时触发
  aboutToAppear() {
    this.initGame()
    this.bestScore = PreferencesUtil.getNumberSync("bestScore") // 读取本地存储的最高分
  }

  // 初始化游戏
  initGame() {
    this.grid = this.grid.map(() => Array(4).fill(0)) // 重置网格
    this.addNewTile() // 添加两个新方块
    this.addNewTile() // 重置当前得分
    this.score = 0
  }

  addNewTile() {
    const emptyCells: [number, number][] = [] // 收集空单元格坐标
    this.grid.forEach((row, i) => {
      row.forEach((cell, j) => {
        if (cell === 0) {
          emptyCells.push([i, j])
        }
      })
    })

    if (emptyCells.length > 0) {
      let n = Math.floor(Math.random() * emptyCells.length) // 随机选择空单元格
      const i = emptyCells[n][0]
      const j = emptyCells[n][1]
      this.grid[i][j] = Math.random() < 0.9 ? 2 : 4 // 90%概率生成2,10%概率生成4
    }
  }

  // 处理移动逻辑
  move(direction: 'left' | 'right' | 'up' | 'down') {
    let newGrid = this.grid.map(row => [...row]) // 创建网格副本
    let moved = false // 移动标志位

    // 矩阵旋转辅助方法
    const rotate = (matrix: number[][]) => {
      return matrix[0].map((_, i) => matrix.map(row => row[i]).reverse())
    }
    const rotateReverse = (matrix: number[][]) => {
      return matrix[0].map((_, i) => matrix.map(row => row[row.length - 1 - i]))
    }

    // 处理单行移动和合并
    const moveRow = (row: number[]) => {
      let newRow = row.filter(cell => cell !== 0) // 移除空格
      for (let i = 0; i < newRow.length - 1; i++) {
        if (newRow[i] === newRow[i + 1]) { // 合并相同数字
          newRow[i] *= 2
          this.score += newRow[i] // 更新得分
          newRow.splice(i + 1, 1) // 移除合并后的元素
        }
      }

      // 补齐长度
      while (newRow.length < 4) {
        newRow.push(0)
      }
      return newRow
    }

    // 根据方向处理移动
    switch (direction) {
      case 'left':
        newGrid.forEach((row, i) => newGrid[i] = moveRow(row))
        break
      case 'right':
        newGrid.forEach((row, i) => newGrid[i] = moveRow(row.reverse()).reverse())
        break
      case 'up':
        let rotatedDown = rotate(newGrid)
        rotatedDown.forEach((row, i) => rotatedDown[i] = moveRow(row.reverse()).reverse())
        newGrid = rotateReverse(rotatedDown)
        break
      case 'down':
        let rotatedUp = rotate(newGrid)
        rotatedUp.forEach((row, i) => rotatedUp[i] = moveRow(row))
        newGrid = rotateReverse(rotatedUp)
        break
    }

    moved = JSON.stringify(newGrid) !== JSON.stringify(this.grid) // 判断是否发生移动
    this.grid = newGrid

    if (moved) {
      this.addNewTile() // 移动后添加新方块
      if (this.score > this.bestScore) { // 更新最高分
        this.bestScore = this.score
        PreferencesUtil.putSync('bestScore', this.bestScore) //保存最高分
      }
    }

    if (this.isGameOver()) { // 游戏结束检测
      ToastUtil.showToast('游戏结束!')
    }
  }

  // 游戏结束判断
  isGameOver(): boolean {
    // 检查空格子
    if (this.grid.some(row => row.includes(0))) {
      return false
    }

    // 检查横向可合并
    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < 3; j++) {
        if (this.grid[i][j] === this.grid[i][j + 1]) {
          return false
        }
      }
    }

    // 检查纵向可合并
    for (let j = 0; j < 4; j++) {
      for (let i = 0; i < 3; i++) {
        if (this.grid[i][j] === this.grid[i + 1][j]) {
          return false
        }
      }
    }

    return true
  }

  build() {
    Column() {
      // 分数显示行
      Row() {
        Text(`得分: ${this.score}`)
          .fontSize(20)
          .margin(10)
        Text(`最高分: ${this.bestScore}`)
          .fontSize(20)
          .margin(10)
        Button('新游戏')
          .onClick(() => this.initGame())
          .margin(10)
      }.margin({top:px2vp(AppUtil.getStatusBarHeight()) })

      // 游戏网格
      Grid() {
        ForEach(this.grid, (row: number[], i) => {
          ForEach(row, (value: number, j) => {
            GridItem() {
              Text(value ? `${value}` : '')
                .textAlign(TextAlign.Center)
                .fontSize(24)
                .fontColor(this.getTextColor(value))
                .width('100%')
                .height('100%')
                .backgroundColor(this.getTileColor(value))
                .animation({
                  duration: 150,
                  curve: Curve.EaseOut
                })
            }.key(`${i}-${j}`)
          })
        })
      }
      .columnsTemplate('1fr 1fr 1fr 1fr')    // 4等分列
      .rowsTemplate('1fr 1fr 1fr 1fr')       // 4等分行
      .width('90%')
      .aspectRatio(1)                        // 保持正方形
      .margin(10)
      .onTouch((event) => {                  // 触摸事件处理
        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)) {
            deltaX > 0 ? this.move('right') : this.move('left')
          } else {
            deltaY > 0 ? this.move('down') : this.move('up')
          }
        }
      })
    }
    .width('100%')
  }

  // 获取方块背景色
  getTileColor(value: number): string {
    const colors = new HashMap<number, string>()
    colors.set(0, '#CDC1B4')
    colors.set(2, '#EEE4DA')
    colors.set(4, '#EDE0C8')
    colors.set(8, '#F2B179')
    colors.set(16, '#F59563')
    colors.set(32, '#F67C5F')
    colors.set(64, '#F65E3B')
    colors.set(128, '#EDCF72')
    colors.set(256, '#EDCF72')
    colors.set(512, '#EDCC61')
    colors.set(1024, '#EDC850')
    colors.set(2048, '#EDC22E')
    return colors.get(value) || '#CDC1B4'
  }

  // 获取文字颜色
  getTextColor(value: number): Color {
    return value > 4 ? Color.White : Color.Black
  }
}


http://www.kler.cn/a/566860.html

相关文章:

  • Spring系列学习之Spring CredHub
  • 1160 拼写单词
  • TP-LINK路由器如何设置网段、网关和DHCP服务
  • 网络层(IP)
  • c#实现485协议
  • TCP基本入门-简单认识一下什么是TCP
  • 【deepseek】本地部署+webui访问
  • Redis使用手册
  • Spring Boot 启动与 Service 注入的 JVM 运行细节
  • DeepSeek接入问题-Xshell5连接Ubuntu22失败解决方案
  • 【欢迎来到Git世界】Github入门
  • 【FL0086】基于SSM和微信小程序的垃圾分类小程序
  • 火语言RPA--Word写入文本段
  • MySQL数据库基本概念
  • DeepSeek开源周Day5: 3FS存储系统与AI数据处理新标杆
  • Github 2025-02-28 Java开源项目日报 Top9
  • 13.重新设计oj_model|综合测试|顶层makefile(C++)
  • SAP-ABAP:SAP数据库视图(Database View)详解-创建
  • 学习dify第二天-web前篇
  • 典型相关分析:原理、检验与Matlab实战