纯血Harmony NETX 5小游戏实践:趣味三消游戏(附源文件)

在移动游戏领域,三消类游戏凭借其简单易上手的规则和充满策略性的玩法,一直占据着重要地位。本文将以鸿蒙系统的ArkTS语言为基础,深入解析一款三消游戏的完整实现过程,从核心消除算法到动画交互效果,带您领略鸿蒙应用开发的魅力。

游戏架构与核心数据结构设计

三消游戏的核心在于网格数据的管理与状态变化,在该实现中,我们通过精心设计的数据结构来支撑游戏逻辑:

typescript 复制代码
// 核心游戏状态定义
@Component
export struct play_5 {
  // 5x5的游戏网格数据,数值代表不同类型的方块
  @State private gridData: number[][] = [
    [1, 2, 3, 2, 1],
    [3, 1, 2, 3, 2],
    [2, 3, 1, 2, 3],
    [1, 2, 3, 2, 1],
    [3, 1, 2, 3, 2]
  ];
  // 方块视觉尺寸,支持屏幕适配
  private blockSize: number = 60;
  // 游戏状态标记
  @State private showRules: boolean = true;
  // 选中方块位置记录
  @State private selectedBlock: GeneratedTypeLiteralInterface_1 | null = null;
  // 动画状态矩阵
  @State private eliminationAnimation: boolean[][] = Array(5).fill(false).map(() => Array(5).fill(false));
  @State private dropAnimation: boolean[][] = Array(5).fill(false).map(() => Array(5).fill(false));
  // 自动检查开关
  @State private autoCheckEnabled: boolean = true;
}

这里采用了@State装饰器来管理响应式数据,当gridData等状态发生变化时,UI会自动更新。特别值得注意的是两个动画状态矩阵eliminationAnimationdropAnimation,它们通过布尔值标记每个方块是否需要播放消除或下落动画,实现了视觉效果与逻辑数据的解耦。

核心消除算法的实现与优化

三消游戏的灵魂在于消除逻辑的实现,该算法需要高效地检测可消除组合并处理连锁反应:

ini 复制代码
private async eliminateMatches(): Promise<void> {
  // 创建标记矩阵
  let toEliminate: boolean[][] = this.gridData.map(row => Array(row.length).fill(false));
  
  // 横向检测
  for (let row = 0; row < this.gridData.length; row++) {
    for (let col = 0; col < this.gridData[row].length - 2; col++) {
      if (this.gridData[row][col] === this.gridData[row][col + 1] && 
          this.gridData[row][col] === this.gridData[row][col + 2]) {
        toEliminate[row][col] = toEliminate[row][col + 1] = toEliminate[row][col + 2] = true;
      }
    }
  }
  
  // 纵向检测
  for (let row = 0; row < this.gridData.length - 2; row++) {
    for (let col = 0; col < this.gridData[row].length; col++) {
      if (this.gridData[row][col] === this.gridData[row + 1][col] && 
          this.gridData[row][col] === this.gridData[row + 2][col]) {
        toEliminate[row][col] = toEliminate[row + 1][col] = toEliminate[row + 2][col] = true;
      }
    }
  }
  
  // 无匹配且自动检查开启时直接返回
  if (!toEliminate.some(row => row.some(val => val)) && this.autoCheckEnabled) {
    return;
  }
  
  // 动画与数据处理逻辑(省略部分代码)
}

算法采用两次遍历的方式:首次横向扫描检测水平方向的三个连续相同方块,第二次纵向扫描检测垂直方向的匹配。这种双重检测机制确保了所有可能的消除组合都能被识别。值得注意的是,代码中使用了some方法来高效判断是否存在可消除方块,避免了不必要的后续处理。

连锁消除与下落逻辑的实现

消除后的方块下落与新方块生成是三消游戏的关键环节,该实现采用了高效的列优先处理策略:

ini 复制代码
// 下落逻辑核心部分
for (let col = 0; col < newGridData[0].length; col++) {
  let writeIndex = newGridData.length - 1;
  for (let row = newGridData.length - 1; row >= 0; row--) {
    if (newGridData[row][col] !== 0) {
      newGridData[writeIndex][col] = newGridData[row][col];
      if (writeIndex !== row) {
        // 记录下落位置用于动画
        dropPositions.push({ row, col, value: newGridData[row][col] });
      }
      writeIndex--;
    }
  }
  
  // 顶部生成新方块
  for (let row = 0; row <= writeIndex; row++) {
    const newValue = Math.floor(Math.random() * 3) + 1;
    newGridData[row][col] = newValue;
    dropPositions.push({ row, col, value: newValue });
  }
}

这段代码以列为单位处理方块下落,从底部向上遍历每个单元格,将非零元素移动到列的底部,顶部则生成随机新方块。这种"列优先"的处理方式比逐行处理更高效,尤其在处理多列同时下落时优势明显。同时,dropPositions数组记录了所有需要动画的方块位置,实现了逻辑与视觉的同步。

交互体验与动画效果设计

在鸿蒙ArkTS中,通过声明式UI结合动画API可以轻松实现流畅的交互效果:

kotlin 复制代码
// 方块UI组件与交互逻辑
Column() {
  Image(this.getBlockImage(item))
    .width(this.blockSize)
    .height(this.blockSize)
    // 消除动画:缩放效果
    .scale({
      x: this.eliminationAnimation[rowIndex][colIndex] ? 1.2 : 1,
      y: this.eliminationAnimation[rowIndex][colIndex] ? 1.2 : 1
    })
    .animation({ duration: 300, curve: Curve.EaseOut })
    // 下落动画:位移效果
    .translate({ y: this.dropAnimation[rowIndex][colIndex] ? -10 : 0 })
    .animation({ duration: 500, curve: Curve.Linear })
}
.onClick(() => {
  // 交互逻辑:选择与交换方块
  if (this.selectedBlock === null) {
    this.selectedBlock = { row: rowIndex, col: colIndex };
  } else {
    this.swapBlocks(this.selectedBlock, { row: rowIndex, col: colIndex });
    this.selectedBlock = null;
  }
})
.border({
  width: this.selectedBlock && this.selectedBlock.row === rowIndex && 
        this.selectedBlock.col === colIndex ? 3 : 1,
  color: this.selectedBlock ? '#FF0000' : '#AAAAAA'
})

这里通过scaletranslate修饰符结合animationAPI,实现了消除时的缩放动画和下落时的位移动画。Curve.EaseOut曲线让消除动画有"弹性"效果,而Curve.Linear则让下落动画更加匀速。选中方块时的红色边框高亮效果,通过border修饰符动态切换,提升了交互反馈的清晰度。

鸿蒙ArkTS开发的特色与优势

该三消游戏的实现充分体现了鸿蒙ArkTS开发的诸多优势:

  • 响应式状态管理 :通过@State装饰器实现数据与UI的自动同步,减少了手动更新UI的繁琐工作
  • 声明式UI编程:UI布局以组件化方式声明,代码结构清晰且易于维护
  • 丰富的动画API:内置的动画修饰符和曲线函数,无需额外库即可实现专业级动画效果
  • 高效的组件化设计 :通过ForEach循环动态生成网格组件,实现了数据与UI的解耦

附:源文件

ini 复制代码
interface GeneratedTypeLiteralInterface_1 {
  row: number;
  col: number;
}

interface GeneratedTypeLiteralInterface_2 {
  row: number;
  col: number;
  value: number;
}

@Component
export struct play_5 {
  // 游戏网格数据
  @State private gridData: number[][] = [
    [1, 2, 3, 2, 1],
    [3, 1, 2, 3, 2],
    [2, 3, 1, 2, 3],
    [1, 2, 3, 2, 1],
    [3, 1, 2, 3, 2]
  ];
  // 当前方块大小(适配屏幕)
  private blockSize: number = 60;
  // 游戏说明可见性
  @State private showRules: boolean = true;
  // 存储选中的方块位置
  @State private selectedBlock: GeneratedTypeLiteralInterface_1 | null = null;
  // 消除动画状态
  @State private eliminationAnimation: boolean[][] = Array(5).fill(false).map(() => Array(5).fill(false));
  // 新增下落动画状态
  @State private dropAnimation: boolean[][] = Array(5).fill(false).map(() => Array(5).fill(false));
  // 自动检查开关
  @State private autoCheckEnabled: boolean = true;

  // 消除匹配的方块
  private async eliminateMatches(): Promise<void> {
    // 创建标记矩阵用于标记消除位置
    let toEliminate: boolean[][] = this.gridData.map(row => Array(row.length).fill(false));

    // 标记需要消除的方块(横向)
    for (let row = 0; row < this.gridData.length; row++) {
      for (let col = 0; col < this.gridData[row].length - 2; col++) {
        if (
          this.gridData[row][col] === this.gridData[row][col + 1] &&
          this.gridData[row][col] === this.gridData[row][col + 2]
        ) {
          toEliminate[row][col] = true;
          toEliminate[row][col + 1] = true;
          toEliminate[row][col + 2] = true;
        }
      }
    }

    // 标记需要消除的方块(纵向)
    for (let row = 0; row < this.gridData.length - 2; row++) {
      for (let col = 0; col < this.gridData[row].length; col++) {
        if (
          this.gridData[row][col] === this.gridData[row + 1][col] &&
          this.gridData[row][col] === this.gridData[row + 2][col]
        ) {
          toEliminate[row][col] = true;
          toEliminate[row + 1][col] = true;
          toEliminate[row + 2][col] = true;
        }
      }
    }

    // 如果没有可消除的方块且自动检查开启,直接返回
    if (!toEliminate.some(row => row.some(val => val)) && this.autoCheckEnabled) {
      return;
    }

    // 设置消除动画状态
    this.eliminationAnimation = JSON.parse(JSON.stringify(toEliminate));

    try {
      // 触发动画
      this.eliminationAnimation = toEliminate.map(row => row.map(val => val));

      // 等待动画完成
      setTimeout(() => {
        // 动画完成后的处理逻辑
      }, 300);

      // 实际消除方块
      for (let row = 0; row < this.gridData.length; row++) {
        for (let col = 0; col < this.gridData[row].length; col++) {
          if (toEliminate[row][col]) {
            this.gridData[row][col] = 0; // 设置为 0 表示消除
          }
        }
      }

      // 重置消除动画状态
      this.eliminationAnimation = this.eliminationAnimation.map(row => row.map(() => false));

      // 下落逻辑:将非零元素下落到底部
      const newGridData = [...this.gridData.map(row => [...row])]; // 创建副本避免直接修改状态
      const dropPositions: GeneratedTypeLiteralInterface_2[] = [];

      for (let col = 0; col < newGridData[0].length; col++) {
        let writeIndex = newGridData.length - 1;
        for (let row = newGridData.length - 1; row >= 0; row--) {
          if (newGridData[row][col] !== 0) {
            newGridData[writeIndex][col] = newGridData[row][col];
            if (writeIndex !== row) {
              // 记录下落的位置用于动画
              dropPositions.push({ row, col, value: newGridData[row][col] });
            }
            writeIndex--;
          }
        }

        // 在顶部补充新的随机方块
        for (let row = 0; row <= writeIndex; row++) {
          const newValue = Math.floor(Math.random() * 3) + 1;
          newGridData[row][col] = newValue;
          // 记录新增方块的位置用于动画
          dropPositions.push({ row, col, value: newValue });
        }
      }

      // 触发下落动画
      this.dropAnimation = Array(this.gridData.length).fill(0).map(() => Array(this.gridData[0].length).fill(false));

      // 更新网格数据并触发重新渲染
      this.gridData = newGridData;

      // 为所有下落的方块触发动画
      dropPositions.forEach(pos => {
        this.dropAnimation[pos.row][pos.col] = true;
      });
      // 再次检查是否有新的可消除组合
      if (this.checkForMatches(this.gridData)) {
        // 添加小延迟让UI有时间更新
        setTimeout(() => {
          // 模拟微任务延迟,鸿蒙ArkTS中实现异步延迟更新
        }, 50);
        this.eliminateMatches(); // 递归调用以处理连续消除
      }

      // 重置下落动画状态
      setTimeout(() => {
        this.dropAnimation = this.dropAnimation.map(row => row.map(() => false));
      }, 300);
    } catch (error) {
      console.error('Error during elimination:', error);
    }
  }

  // 交换两个相邻方块
  private swapBlocks(first: GeneratedTypeLiteralInterface_1, second: GeneratedTypeLiteralInterface_1): void {
    // 检查是否是相邻方块
    const rowDiff = Math.abs(first.row - second.row);
    const colDiff = Math.abs(first.col - second.col);

    if ((rowDiff === 1 && colDiff === 0) || (rowDiff === 0 && colDiff === 1)) {
      // 创建新数组进行交换
      const newGridData = this.gridData.map(row => [...row]);

      // 直接交换方块值
      const temp = newGridData[first.row][first.col];
      newGridData[first.row][first.col] = newGridData[second.row][second.col];
      newGridData[second.row][second.col] = temp;

      // 检查是否有可消除的方块
      const hasMatches = this.checkForMatches(newGridData);

      if (hasMatches) {
        // 如果有可消除的方块,更新数据
        this.gridData = newGridData;
        this.eliminateMatches();
      } else {
        // 如果没有可消除的方块,撤销交换
        console.log('No matches after swap, reverting');
      }
    }
  }

  // 检查是否有可消除的方块
  private checkForMatches(grid: number[][]): boolean {
    // 检查横向匹配
    for (let row = 0; row < grid.length; row++) {
      for (let col = 0; col < grid[row].length - 2; col++) {
        if (grid[row][col] === grid[row][col + 1] && grid[row][col] === grid[row][col + 2]) {
          return true;
        }
      }
    }

    // 检查纵向匹配
    for (let row = 0; row < grid.length - 2; row++) {
      for (let col = 0; col < grid[row].length; col++) {
        if (grid[row][col] === grid[row + 1][col] && grid[row][col] === grid[row + 2][col]) {
          return true;
        }
      }
    }

    return false;
  }

  build() {
    Column() {
      // 控制面板
      // 游戏规则说明
      if (this.showRules) {
        Column() {
          Image($r('app.media.ic_game_icon'))
            .width(80)
            .height(80)
            .margin({ bottom: 15 })
          Text('游戏规则')
            .fontSize(32)
            .fontWeight(FontWeight.Bold)
            .fontFamily('Comic Sans MS')
            .margin({ bottom: 20 })
          Text('1. 点击任意两个相邻方块交换位置\n2. 三个或以上相同方块连成一线即可消除\n3. 消除更多方块获得更高分数\n4. 继续游戏直到无法再消除')
            .fontSize(20)
            .fontFamily('Comic Sans MS')
            .textAlign(TextAlign.Center)
            .margin({ bottom: 30 })
          Button('开始游戏')
            .fontFamily('Comic Sans MS')
            .fontSize(24)
            .width(180)
            .height(50)
            .backgroundColor('#FFD700')
            .onClick(() => {
              this.showRules = false;
            })
        }
        .width('90%')
        .padding({ top: 30, bottom: 35, left: 25, right: 25 })
        .backgroundColor('rgba(255, 255, 255, 0.9)')
        .borderRadius(15)
        .shadow({ color: '#E0E0E0', radius: 10 })  // 添加柔和阴影效果
      } else {
        Row() {
          Text('无尽模式')
            .fontSize(16)
            .fontFamily('Comic Sans MS')
            .fontWeight(FontWeight.Bold)
            .margin({ left: 10 })
        }
        .width('90%')
        .justifyContent(FlexAlign.Start)
        .margin({ bottom: 10 })
        // 构建游戏界面
        ForEach(this.gridData, (row: number[], rowIndex: number) => {
          Row() {
            ForEach(row, (item: number, colIndex: number) => {
              // 方块组件
              Column() {
                Image(this.getBlockImage(item))
                  .width(this.blockSize)
                  .height(this.blockSize)
                  .scale({
                    x: this.eliminationAnimation[rowIndex][colIndex] ? 1.2 : 1,
                    y: this.eliminationAnimation[rowIndex][colIndex] ? 1.2 : 1
                  })
                  .animation({
                    duration: 300,
                    curve: Curve.EaseOut
                  })
                  // 添加下落动画效果
                  .translate({
                    y: this.dropAnimation[rowIndex][colIndex] ? -10 : 0
                  })
                  .animation({
                    duration: 500,
                    curve: Curve.Linear
                  })
              }
              .onClick(() => {
                if (this.selectedBlock === null) {
                  // 第一次选择方块
                  this.selectedBlock = { row: rowIndex, col: colIndex };
                  console.log(`Selected block: ${rowIndex}, ${colIndex}`)
                } else {
                  // 第二次选择,尝试交换
                  this.swapBlocks(this.selectedBlock, { row: rowIndex, col: colIndex });
                  // 重置选中方块状态
                  this.selectedBlock = null;
                  console.log('Cleared selected block state');
                }
              })
              .border({
                width: this.selectedBlock && this.selectedBlock.row === rowIndex &&
                  this.selectedBlock.col === colIndex ? 3 : 1,
                color: this.selectedBlock && this.selectedBlock.row === rowIndex &&
                  this.selectedBlock.col === colIndex ? '#FF0000' : '#AAAAAA'
              }) // 高亮显示当前选中的方块
            })
          }
          .margin({ bottom: 15 })
          .width('100%')
          .justifyContent(FlexAlign.SpaceEvenly)
        })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FFF8DC')  // 卡通风格背景色
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }

  // 根据方块数值获取对应的图片资源
  private getBlockImage(value: number): Resource {
    switch (value) {
      case 1:
        return $r('app.media.block_1')
      case 2:
        return $r('app.media.block_2')
      case 3:
        return $r('app.media.block_3')
      default:
        return $r('app.media.background')
    }
  }
}
相关推荐
Aisanyi2 小时前
【鸿蒙开发】PC实现开局沉浸式全屏
前端·华为·harmonyos
我睡醒再说5 小时前
以下是对华为 HarmonyOS NETX 5属性动画(ArkTS)文档的结构化整理,通过层级标题、表格和代码块提升可读性:
harmonyos
我睡醒再说5 小时前
ArkUI-X跨平台开发能力解析:优势与限制场景
harmonyos
我睡醒再说5 小时前
HarmonyOS NETX 5ArkUI-X打造数字猜谜游戏:(附源文件)
harmonyos
我睡醒再说5 小时前
纯血Harmony NETX 5小游戏实践:电子木鱼(附源文件)
harmonyos
shenshizhong6 小时前
鸿蒙列表新的实现方式
harmonyos
程序员小刘6 小时前
鸿蒙跨平台开发:打通安卓、iOS生态
android·ios·harmonyos
王二蛋与他的张大花6 小时前
鸿蒙运动项目开发:封装超级好用的 RCP 网络库(中)—— 错误处理,会话管理与网络状态检测篇
harmonyos
王二蛋与他的张大花7 小时前
鸿蒙运动项目开发:封装超级好用的 RCP 网络库(上)—— 请求参数封装,类型转化器与日志记录篇
harmonyos