在移动应用开发领域,2048游戏因其简洁的规则和富有挑战性的玩法成为经典案例。本文将基于鸿蒙OS的ArkUI框架,深入解析2048游戏的实现原理,从数据结构设计到动画交互优化,带您全面了解这款益智游戏的开发全过程。

游戏核心架构与数据模型设计

2048游戏的核心是一个4×4的网格系统,每个单元格可以包含数字2的幂次或为空。在鸿蒙实现中,我们采用二维数组作为基础数据结构:

@State grid: number[][] = [
  [0, 0, 0, 0],
  [0, 0, 0, 0],
  [0, 0, 0, 0],
  [0, 0, 0, 0]
];

这种设计简洁高效,便于实现网格的遍历、合并和更新操作。游戏初始化时,通过addRandomTile()方法在网格中随机生成两个初始方块(2或4,10%概率生成4):

addRandomTile() {
  const emptyCells: GeneratedTypeLiteralInterface_1[] = [];
  // 收集所有空单元格坐标
  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
      if (this.grid[i][j] === 0) {
        emptyCells.push({ row: i, col: j });
      }
    }
  }
  // 处理无空位的情况
  if (emptyCells.length === 0 && !this.checkMovesAvailable()) {
    this.gameOver = true;
    return;
  }
  // 随机放置新方块
  if (emptyCells.length > 0) {
    const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
    this.grid[randomCell.row][randomCell.col] = Math.random() < 0.1 ? 4 : 2;
  }
}

数据模型中还包含了分数统计score、游戏状态gameOver和胜利标志gameWon,这些状态通过@State装饰器实现响应式更新,确保UI与数据保持同步。

核心游戏逻辑:移动与合并算法

2048游戏的核心在于四个方向的移动逻辑,其本质是对网格数据的压缩与合并操作。以向左移动为例,核心算法分为两步:压缩非零元素到左侧,然后合并相邻相同元素。

moveLeft() {
  let moved = false;
  const newGrid = this.grid.map(row => {
    const newRow = this.compressAndMerge(row);
    // 检测数据是否变更
    if (JSON.stringify(newRow) !== JSON.stringify(row)) {
      moved = true;
    }
    return newRow;
  });
  // 仅在数据变更时更新状态
  if (moved) {
    this.grid = newGrid;
    this.checkWinCondition();
    this.addRandomTile();
  }
}

compressAndMerge()方法是移动逻辑的核心,它不仅负责将非零元素紧凑排列,还会处理相同元素的合并并计算得分:

compressAndMerge(row: number[]): number[] {
  const filteredRow = row.filter(val => val !== 0);
  const mergedRow: number[] = [];
  let scoreIncrease = 0;

  for (let i = 0; i < filteredRow.length; i++) {
    // 合并相邻相同元素
    if (i < filteredRow.length - 1 && filteredRow[i] === filteredRow[i + 1]) {
      mergedRow.push(filteredRow[i] * 2);
      scoreIncrease += filteredRow[i] * 2;
      // 检查是否达成2048胜利条件
      if (filteredRow[i] * 2 === 2048) {
        this.gameWon = true;
      }
      i++; // 跳过已合并元素
    } else {
      mergedRow.push(filteredRow[i]);
    }
  }

  // 补零操作,保持4格长度
  while (mergedRow.length < 4) {
    mergedRow.push(0);
  }

  // 更新积分
  if (scoreIncrease > 0) {
    this.score += scoreIncrease;
  }
  return mergedRow;
}

向右、向上、向下的移动逻辑基于向左移动算法演变而来,通过行列转换和数组反转实现方向适配。例如向右移动时,先将行数据反转,应用向左移动逻辑后再反转回来:

moveRight() {
  let moved = false;
  const newGrid = this.grid.map(row => {
    const reversedRow = [...row].reverse();
    const mergedRow = this.compressAndMerge(reversedRow);
    const newRow = mergedRow.reverse();
    if (JSON.stringify(newRow) !== JSON.stringify(row)) {
      moved = true;
    }
    return newRow;
  });
  // 状态更新逻辑与向左移动相同
}

界面渲染与交互体验优化

鸿蒙ArkUI的声明式UI特性让2048游戏的界面实现变得简洁直观。我们通过ForEach循环动态渲染4×4网格,每个单元格的样式根据其值动态变化:

ForEach(this.grid, (row: number[], rowIndex: number) => {
  Row({ space: 15 }) {
    ForEach(row, (cell: number, colIndex: number) => {
      Column() {
        CellComponent({ value: cell })
      }
      .width(60)
      .height(60)
      .borderRadius(20)
      .backgroundColor(this.getCellBackgroundColor(cell))
      .justifyContent(FlexAlign.Center)
      .alignItems(HorizontalAlign.Center)
    })
  }
  .width('100%')
  .justifyContent(FlexAlign.SpaceAround)
})

getCellBackgroundColor()方法根据单元格值返回对应的背景色,实现2048游戏经典的视觉层次感:

getCellBackgroundColor(value: number): ResourceColor {
  switch (value) {
    case 0: return '#cdc1b4';
    case 2: return '#eee4da';
    case 4: return '#ede0c8';
    case 8: return '#f2b179';
    // 省略中间值...
    case 2048: return '#edc22e';
    default: return '#3c3a32';
  }
}

交互控制采用四个方向按钮实现,点击事件绑定对应的移动逻辑:

Row({ space: 20 }) {
  Button('←')
    .width(60)
    .height(60)
    .fontColor('#ffffff')
    .onClick(() => this.moveLeft())
    .backgroundColor('#776e65')
    .borderRadius(10);
  // 其他方向按钮类似...
}

游戏状态管理与边界条件处理

2048游戏的难点在于边界条件的处理,包括:

  1. 游戏结束判断:当网格已满且没有可合并的元素时,游戏结束:
checkMovesAvailable(): boolean {
  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
      // 检查右侧和下侧是否有可合并元素
      if (j < 3 && this.grid[i][j] === this.grid[i][j + 1]) return true;
      if (i < 3 && this.grid[i][j] === this.grid[i + 1][j]) return true;
    }
  }
  return false;
}
  1. 胜利条件判断:当网格中出现2048时,游戏胜利:
checkWinCondition() {
  if (!this.gameWon && this.grid.some(row => row.includes(2048))) {
    this.gameWon = true;
  }
}
  1. 积分系统:每次合并操作会根据合并后的数值增加积分,例如合并两个1024得到2048时,积分增加2048。

附:源文件

interface GeneratedTypeLiteralInterface_1 {
  row: number;
  col: number;
}

@Component
export struct play_7 {
  // 游戏数据矩阵
  @State grid: number[][] = [
    [0, 0, 0, 0],
    [0, 0, 0, 0],
    [0, 0, 0, 0],
    [0, 0, 0, 0]
  ];

  // 积分属性
  @State score: number = 0;

  // 游戏状态
  @State gameOver: boolean = false;
  @State gameWon: boolean = false;
  // 初始化游戏
  aboutToAppear() {
    this.addRandomTile();
    this.addRandomTile();
    this.score = 0; // 初始化积分为0
    this.gameOver = false;
    this.gameWon = false;
  }

  // 添加随机方块并更新积分
  addRandomTile() {
    const emptyCells: GeneratedTypeLiteralInterface_1[] = [];

    // 找出所有空位
    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < 4; j++) {
        if (this.grid[i][j] === 0) {
          emptyCells.push({ row: i, col: j });
        }
      }
    }

    // 如果没有空位则检查游戏结束
    if (emptyCells.length === 0 && !this.checkMovesAvailable()) {
      this.gameOver = true;
      return;
    }

    // 随机选择一个位置放置2或4(10%概率)
    if (emptyCells.length > 0) {
      const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
      this.grid[randomCell.row][randomCell.col] = Math.random() < 0.1 ? 4 : 2;
    }
  }

  // 检查是否有可用移动
  checkMovesAvailable(): boolean {
    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < 4; j++) {
        // 检查右侧是否有可合并
        if (j < 3 && this.grid[i][j] === this.grid[i][j + 1]) return true;
        // 检查下方是否有可合并
        if (i < 3 && this.grid[i][j] === this.grid[i + 1][j]) return true;
      }
    }
    return false;
  }

  // 向左移动逻辑并计算积分
  moveLeft() {
    let moved = false;
    const newGrid = this.grid.map(row => {
      const newRow = this.compressAndMerge(row);
      if (JSON.stringify(newRow) !== JSON.stringify(row)) {
        moved = true;
      }
      return newRow;
    });

    if (moved) {
      this.grid = newGrid;
      this.checkWinCondition();
      this.addRandomTile();
    }
  }

  // 向右移动逻辑并计算积分
  moveRight() {
    let moved = false;
    const newGrid = this.grid.map(row => {
      const reversedRow = [...row].reverse();
      const mergedRow = this.compressAndMerge(reversedRow);
      const newRow = mergedRow.reverse();
      if (JSON.stringify(newRow) !== JSON.stringify(row)) {
        moved = true;
      }
      return newRow;
    });

    if (moved) {
      this.grid = newGrid;
      this.checkWinCondition();
      this.addRandomTile();
    }
  }

  // 向上移动逻辑并计算积分
  moveUp() {
    let moved = false;
    const newGrid = [...this.grid];

    for (let col = 0; col < 4; col++) {
      const column = [newGrid[0][col], newGrid[1][col], newGrid[2][col], newGrid[3][col]];
      const newColumn = this.compressAndMerge(column);

      for (let row = 0; row < 4; row++) {
        if (newGrid[row][col] !== newColumn[row]) {
          moved = true;
        }
        newGrid[row][col] = newColumn[row];
      }
    }

    if (moved) {
      this.grid = newGrid;
      this.checkWinCondition();
      this.addRandomTile();
    }
  }

  // 向下移动逻辑并计算积分
  moveDown() {
    let moved = false;
    const newGrid = [...this.grid];

    for (let col = 0; col < 4; col++) {
      const column = [newGrid[3][col], newGrid[2][col], newGrid[1][col], newGrid[0][col]];
      const newColumn = this.compressAndMerge(column);

      for (let row = 0; row < 4; row++) {
        if (newGrid[3 - row][col] !== newColumn[row]) {
          moved = true;
        }
        newGrid[3 - row][col] = newColumn[row];
      }
    }

    if (moved) {
      this.grid = newGrid;
      this.checkWinCondition();
      this.addRandomTile();
    }
  }

  // 压缩并合并单元格,增加积分计算
  compressAndMerge(row: number[]): number[] {
    const filteredRow = row.filter(val => val !== 0);
    const mergedRow: number[] = [];
    let scoreIncrease = 0;

    for (let i = 0; i < filteredRow.length; i++) {
      if (i < filteredRow.length - 1 && filteredRow[i] === filteredRow[i + 1]) {
        mergedRow.push(filteredRow[i] * 2);
        scoreIncrease += filteredRow[i] * 2;
        i++;
        // 检查是否达成2048胜利条件
        if (filteredRow[i] * 2 === 2048) {
          this.gameWon = true;
        }
      } else {
        mergedRow.push(filteredRow[i]);
      }
    }

    // 补零
    while (mergedRow.length < 4) {
      mergedRow.push(0);
    }

    // 更新积分
    if (scoreIncrease > 0) {
      this.score += scoreIncrease;
    }
    return mergedRow;
  }

  // 检查胜利条件
  checkWinCondition() {
    if (!this.gameWon && this.grid.some(row => row.includes(2048))) {
      this.gameWon = true;
    }
  }

  build() {
    Column({ space: 20 }) {
      // 标题区和积分显示
      Row() {
        Text('2048')
          .fontSize(36)
          .fontWeight(FontWeight.Bold)
          .textAlign(TextAlign.Center)
          .width('70%')
          .padding({ top: 10, bottom: 10 })
          .borderRadius(15)
          .backgroundColor('#fdf6ec')
          .height(60)

        // 积分显示
        Text(`分数: ${this.score}`)
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .textAlign(TextAlign.Center)
          .width('30%')
          .padding({ top: 10, bottom: 10 })
          .borderRadius(15)
          .backgroundColor('#f1eeee')
          .height(60)
          .shadow({ color: '#ccc', radius: 6 })
      }
      .width('90%')

      // 显示游戏网格
      Column({ space: 15 }) {
        ForEach(this.grid, (row: number[], rowIndex: number) => {
          Row({ space: 15 }) {
            ForEach(row, (cell: number, colIndex: number) => {
              // 每个单元格的显示
              Column() {
                CellComponent({ value: cell })
              }
              .width(60)
              .height(60)
              .borderRadius(20)
              .backgroundColor(this.getCellBackgroundColor(cell))
              .justifyContent(FlexAlign.Center)
              .alignItems(HorizontalAlign.Center)
              .shadow({ color: '#a394894d', radius: 8, offsetX: 2, offsetY: 2 })
            })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceAround)
        })
      }
      .width('90%')
      .padding(10)
      .borderRadius(20)
      .backgroundColor('#bbada0')
      .shadow({ color: '#888', radius: 10 })

      // 添加控制按钮
      Row({ space: 20 }) {
        Button('←')
          .width(60)
          .height(60)
          .fontColor('#ffffff')
          .onClick(() => {
            this.moveLeft()
          })
          .backgroundColor('#776e65')
          .borderRadius(10)

        Button('↑')
          .width(60)
          .height(60)
          .fontColor('#ffffff')
          .onClick(() => {
            this.moveUp()
          })
          .backgroundColor('#776e65')
          .borderRadius(10)

        Button('↓')
          .width(60)
          .height(60)
          .fontColor('#ffffff')
          .onClick(() => {
            this.moveDown()
          })
          .backgroundColor('#776e65')
          .borderRadius(10)

        Button('→')
          .width(60)
          .height(60)
          .fontColor('#ffffff')
          .onClick(() => {
            this.moveRight()
          })
          .backgroundColor('#776e65')
          .borderRadius(10)
      }
      .width('90%')
      .justifyContent(FlexAlign.Center)

      // 游戏状态提示
      if (this.gameOver) {
        Text('Game Over!')
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .fontColor('#ffffff')
          .backgroundColor('#bbada0')
          .padding({ left: 20, right: 20, top: 10, bottom: 10 })
          .borderRadius(10)
      }

      if (this.gameWon) {
        Text('Congratulations!\nYou reached 2048!')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor('#ffffff')
          .backgroundColor('#edc22e')
          .padding({ left: 20, right: 20, top: 10, bottom: 10 })
          .borderRadius(10)
          .textAlign(TextAlign.Center)
      }
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 20, bottom: 20 })
  }
  // 获取单元格背景颜色
  getCellBackgroundColor(value: number): ResourceColor
  {
    switch (value) {
      case 0:
        return '#cdc1b4'
      case 2:
        return '#eee4da'
      case 4:
        return '#ede0c8'
      case 8:
        return '#f2b179'
      case 16:
        return '#f59563'
      case 32:
        return '#f67c5f'
      case 64:
        return '#f65e3b'
      case 128:
        return '#edcf72'
      case 256:
        return '#edcc61'
      case 512:
        return '#edc850'
      case 1024:
        return '#edc53f'
      case 2048:
        return '#edc22e'
      default:
        return '#3c3a32'
    }
  }
}

@Component
struct CellComponent {
  @Prop value: number

  build() {
    Text(this.value === 0 ? '' : this.value.toString())
      .fontSize(28)
      .fontColor('#776e65')
      .fontWeight(FontWeight.Bold)
      .fontFamily( 'cursive')
  }
}
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐