鸿蒙HarmonyOS 5小游戏实践:动物连连看(附:源代码)

2025-06-29 09:57:38
109次阅读
0个评论

连连看是一款深受大众喜爱的经典消除类游戏,它不仅能锻炼玩家的观察力和反应能力,还能带来轻松愉快的游戏体验。本文将详细介绍如何使用鸿蒙(OpenHarmony)的ArkUI框架开发一款功能完整的动物连连看游戏,涵盖游戏设计、核心算法实现和界面构建的全过程。

screenshots (11).gif

游戏设计概述

我们的动物连连看游戏具有以下特点:

  • 8×10的游戏棋盘:提供适中的游戏难度
  • 15种可爱动物图标:采用emoji表情符号,直观易懂
  • 多种连接方式:支持直线、一个拐角和两个拐角连接
  • 智能提示系统:帮助玩家找到可消除的方块对
  • 完整游戏流程:包含计时、计分和游戏结束判断

核心游戏逻辑实现

1. 游戏状态管理

我们使用@State装饰器来管理游戏的各种状态:

@State gameBoard: number[][] = []; // 游戏棋盘状态
@State selectedTile: SelectedTile | null = null; // 选中的方块
@State remainingPairs: number = 0; // 剩余方块对数
@State timer: number = 0; // 游戏时间
@State gameOver: boolean = false; // 游戏结束标志
@State showHint: boolean = false; // 提示显示状态
@State hintPath: SelectedTile[] = []; // 提示路径

这种响应式状态管理确保当这些值变化时,UI能够自动更新。

2. 棋盘初始化与洗牌算法

游戏开始时需要初始化并随机排列动物方块:

private initializeBoard() {
  const totalTiles = this.ROWS * this.COLS;
  const pairsNeeded = Math.floor(totalTiles / 2);
  this.remainingPairs = pairsNeeded;

  // 创建动物对数组
  let animals: number[] = [];
  for (let i = 0; i < this.ANIMAL_TYPES && i < pairsNeeded; i++) {
    animals.push(i, i); // 每种动物至少一对
  }
  
  // 随机打乱
  this.shuffleArray(animals);
  
  // 填充到游戏棋盘
  // ...
}

private shuffleArray(array: number[]) {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]]; // ES6解构赋值交换元素
  }
}

3. 连接检测算法

这是游戏最核心的部分,我们实现了三种连接方式检测:

直线连接检测

private canConnectStraight(pos1: SelectedTile, pos2: SelectedTile): boolean {
  // 同一行检测
  if (pos1.row === pos2.row) {
    const [minCol, maxCol] = [Math.min(pos1.col, pos2.col), Math.max(pos1.col, pos2.col)];
    for (let col = minCol + 1; col < maxCol; col++) {
      if (this.gameBoard[pos1.row][col] !== -1) return false;
    }
    return true;
  }
  
  // 同一列检测
  if (pos1.col === pos2.col) {
    const [minRow, maxRow] = [Math.min(pos1.row, pos2.row), Math.max(pos1.row, pos2.row)];
    for (let row = minRow + 1; row < maxRow; row++) {
      if (this.gameBoard[row][pos1.col] !== -1) return false;
    }
    return true;
  }
  
  return false;
}

一个拐角连接检测

private canConnectWithOneCorner(pos1: SelectedTile, pos2: SelectedTile): SelectedTile | null {
  // 检查两个可能的拐点
  const corner1 = {row: pos1.row, col: pos2.col};
  if (this.isPositionEmpty(corner1) && 
      this.canConnectStraight(pos1, corner1) && 
      this.canConnectStraight(corner1, pos2)) {
    return corner1;
  }

  const corner2 = {row: pos2.row, col: pos1.col};
  if (this.isPositionEmpty(corner2) && 
      this.canConnectStraight(pos1, corner2) && 
      this.canConnectStraight(corner2, pos2)) {
    return corner2;
  }

  return null;
}

两个拐角连接检测

private canConnectWithTwoCorners(pos1: SelectedTile, pos2: SelectedTile): SelectedTile[] {
  // 行方向检测
  for (let row = 0; row < this.ROWS; row++) {
    if (row === pos1.row || row === pos2.row) continue;
    
    const corner1 = {row, col: pos1.col};
    const corner2 = {row, col: pos2.col};
    
    if (this.isPositionEmpty(corner1) && this.isPositionEmpty(corner2) && 
        this.canConnectStraight(pos1, corner1) && 
        this.canConnectStraight(corner1, corner2) && 
        this.canConnectStraight(corner2, pos2)) {
      return [corner1, corner2];
    }
  }
  
  // 列方向检测
  // ...
}

4. 智能提示系统

提示功能能帮助玩家找到可消除的方块对:

showHint() {
  if (this.gameOver || this.showHint) return;

  // 遍历所有方块对
  for (let row1 = 0; row1 < this.ROWS; row1++) {
    for (let col1 = 0; col1 < this.COLS; col1++) {
      if (this.gameBoard[row1][col1] === -1) continue;

      for (let row2 = 0; row2 < this.ROWS; row2++) {
        for (let col2 = 0; col2 < this.COLS; col2++) {
          if ((row1 === row2 && col1 === col2) || this.gameBoard[row2][col2] === -1) continue;

          if (this.gameBoard[row1][col1] === this.gameBoard[row2][col2]) {
            const path = this.findConnectionPath({row: row1, col: col1}, {row: row2, col: col2});
            if (path.length > 0) {
              this.hintPath = path;
              this.showHint = true;
              
              // 1.5秒后自动隐藏提示
              this.hintTimeout = setTimeout(() => {
                this.showHint = false;
                this.hintPath = [];
              }, this.HINT_DURATION);
              
              return;
            }
          }
        }
      }
    }
  }
  
  // 无解提示
  this.message = "没有可消除的对,请重新开始游戏";
  setTimeout(() => this.message = `剩余: ${this.remainingPairs}对`, 2000);
}

界面构建与交互设计

1. 游戏主界面结构

build() {
  Column() {
    // 游戏标题
    Text('动物连连看')
      .fontSize(26)
      .fontWeight(FontWeight.Bold)
    
    // 游戏信息显示
    Row() {
      Column() {
        Text('剩余')
        Text(`${this.remainingPairs}对`)
      }
      Column() {
        Text('时间')
        Text(this.formatTime(this.timer))
      }
    }
    
    // 提示信息
    if (this.message !== `剩余: ${this.remainingPairs}对`) {
      Text(this.message)
    }
    
    // 游戏棋盘
    Grid() {
      ForEach(this.gameBoard, (row, rowIndex) => {
        ForEach(row, (tile, colIndex) => {
          GridItem() {
            this.AnimalTile(tile, isSelected, isEmpty, rowIndex, colIndex, isHint)
          }
        })
      })
    }
    
    // 控制按钮
    Row() {
      Button('提示')
      Button(this.gameOver ? '重新开始' : '新游戏')
    }
    
    // 游戏结束提示
    if (this.gameOver) {
      Text('恭喜通关!')
    }
  }
}

2. 方块组件设计

@Builder
AnimalTile(
  animalIndex: number,
  isSelected: boolean,
  isEmpty: boolean,
  rowIndex: number,
  colIndex: number,
  isHint: boolean
) {
  Stack() {
    if (!isEmpty) {
      Column() {
        Text(this.ANIMAL_ICONS[animalIndex % this.ANIMAL_ICONS.length])
          .fontSize(24)
      }
      .backgroundColor(isSelected ? '#FFEB3B' : (isHint ? '#FF9800' : '#FFFFFF'))
      .border({
        width: isHint ? 3 : 1,
        color: isHint ? '#FF9800' : Color.Black
      })
    } else {
      Column()
        .backgroundColor('#EEEEEE')
    }

    // 提示高亮效果
    if (isHint && !isEmpty) {
      Column()
        .backgroundColor('#ffec2727')
        .opacity(0.3)
    }
  }
  .onClick(() => this.handleTileClick(rowIndex, colIndex))
}

关键技术与优化点

  1. 性能优化
    • 使用ForEach高效渲染网格
    • 合理使用状态管理,减少不必要的重绘
    • 实现高效的连接检测算法
  2. 用户体验优化
    • 添加连接路径可视化
    • 实现平滑的过渡动画
    • 智能提示系统帮助玩家
  3. 代码质量保证
    • 清晰的类型定义(如SelectedTile接口)
    • 合理的函数拆分和封装
    • 完善的注释说明
  4. 资源管理
    • 组件生命周期中清理计时器
    • 合理使用内存,避免泄漏

附:代码

// AnimalLinkGame.ets
interface SelectedTile {
  row: number;
  col: number;
}


@Entry
@Component
struct AnimalLinkGame {
  @State message: string = ''
  // 游戏配置
  private readonly ROWS = 8;
  private readonly COLS = 10;
  private readonly ANIMAL_TYPES = 15;
  private readonly ANIMAL_ICONS = ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼',
    '🦁', '🐮', '🐷', '🐸', '🐵', '🐔', '🐧'];
  private readonly HINT_DURATION = 1500; // 提示显示时间(毫秒)

  // 游戏状态
  @State gameBoard: number[][] = [];
  @State selectedTile: SelectedTile | null = null;
  @State remainingPairs: number = 0;
  @State timer: number = 0;
  @State gameOver: boolean = false;
  @State showHint1: boolean = false;
  @State hintPath: SelectedTile[] = []; // 提示路径
  private timerInterval: number | null = null;
  private hintTimeout: number | null = null;

  // 生命周期
  aboutToAppear() {
    this.startNewGame();
  }

  aboutToDisappear() {
    this.clearTimers();
  }

  // 清理计时器
  private clearTimers() {
    if (this.timerInterval) clearInterval(this.timerInterval);
    if (this.hintTimeout) clearTimeout(this.hintTimeout);
  }

  // 开始新游戏
  startNewGame() {
    this.clearTimers();
    this.timer = 0;
    this.gameOver = false;
    this.selectedTile = null;
    this.showHint1 = false;
    this.hintPath = [];
    this.initializeBoard();
    this.timerInterval = setInterval(() => this.timer++, 1000);
  }

  // 初始化游戏棋盘
  initializeBoard() {
    const totalTiles = this.ROWS * this.COLS;
    const pairsNeeded = Math.floor(totalTiles / 2);
    this.remainingPairs = pairsNeeded;

    let animals: number[] = [];
    for (let i = 0; i < this.ANIMAL_TYPES && i < pairsNeeded; i++) {
      animals.push(i, i);
    }

    while (animals.length < totalTiles) {
      animals.push(...animals.slice(0, totalTiles - animals.length));
    }

    this.shuffleArray(animals);

    this.gameBoard = [];
    for (let row = 0; row < this.ROWS; row++) {
      let boardRow: number[] = [];
      for (let col = 0; col < this.COLS; col++) {
        boardRow.push(animals[row * this.COLS + col]);
      }
      this.gameBoard.push(boardRow);
    }
  }

  // 洗牌算法
  private shuffleArray(array: number[]) {
    for (let i = array.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      let temp = array[i];
      array[i] = array[j];
      array[j] = temp;
    }
  }

  // 处理方块点击
  handleTileClick(row: number, col: number) {
    if (this.gameOver || this.gameBoard[row][col] === -1) return;

    // 清除提示
    this.showHint1 = false;
    if (this.hintTimeout) clearTimeout(this.hintTimeout);

    if (this.selectedTile) {
      if (this.selectedTile.row === row && this.selectedTile.col === col) {
        this.selectedTile = null;
        return;
      }

      if (this.gameBoard[this.selectedTile.row][this.selectedTile.col] ===
      this.gameBoard[row][col]) {
        const path = this.findConnectionPath(this.selectedTile, {row, col});
        if (path.length > 0) {
          this.hintPath = path; // 显示连接路径
          setTimeout(() => {
            this.gameBoard[this.selectedTile!.row][this.selectedTile!.col] = -1;
            this.gameBoard[row][col] = -1;
            this.remainingPairs--;
            this.hintPath = [];
            this.selectedTile = null;

            if (this.remainingPairs === 0) {
              this.gameOver = true;
              this.clearTimers();
            }
          }, 300); // 短暂显示连接路径
          return;
        }
      }
    }
    this.selectedTile = {row, col};
  }

  // 查找连接路径 (实现多种连接方式)
  private findConnectionPath(start: SelectedTile, end: SelectedTile): SelectedTile[] {
    // 直线连接
    if (this.canConnectStraight(start, end)) {
      return [start, end];
    }

    // 一个拐角连接
    const oneCorner = this.canConnectWithOneCorner(start, end);
    if (oneCorner) {
      return [start, oneCorner, end];
    }

    // 两个拐角连接
    const twoCorners = this.canConnectWithTwoCorners(start, end);
    if (twoCorners.length > 0) {
      return [start, ...twoCorners, end];
    }

    return [];
  }

  // 直线连接检查
  private canConnectStraight(pos1: SelectedTile, pos2: SelectedTile): boolean {
    // 同一行
    if (pos1.row === pos2.row) {
      const minCol = Math.min(pos1.col, pos2.col);
      const maxCol = Math.max(pos1.col, pos2.col);
      for (let col = minCol + 1; col < maxCol; col++) {
        if (this.gameBoard[pos1.row][col] !== -1) return false;
      }
      return true;
    }

    // 同一列
    if (pos1.col === pos2.col) {
      const minRow = Math.min(pos1.row, pos2.row);
      const maxRow = Math.max(pos1.row, pos2.row);
      for (let row = minRow + 1; row < maxRow; row++) {
        if (this.gameBoard[row][pos1.col] !== -1) return false;
      }
      return true;
    }

    return false;
  }

  // 一个拐角连接检查
  private canConnectWithOneCorner(pos1: SelectedTile, pos2: SelectedTile): SelectedTile | null {
    // 检查可能的拐点
    const corner1: SelectedTile = {row: pos1.row, col: pos2.col};
    if (this.gameBoard[corner1.row][corner1.col] === -1 &&
    this.canConnectStraight(pos1, corner1) &&
    this.canConnectStraight(corner1, pos2)) {
      return corner1;
    }

    const corner2:SelectedTile = {row: pos2.row, col: pos1.col};
    if (this.gameBoard[corner2.row][corner2.col] === -1 &&
    this.canConnectStraight(pos1, corner2) &&
    this.canConnectStraight(corner2, pos2)) {
      return corner2;
    }

    return null;
  }

  // 两个拐角连接检查
  private canConnectWithTwoCorners(pos1: SelectedTile, pos2: SelectedTile): SelectedTile[] {
    // 检查行方向可能的连接
    for (let row = 0; row < this.ROWS; row++) {
      if (row === pos1.row || row === pos2.row) continue;

      const corner1:SelectedTile = {row, col: pos1.col};
      const corner2:SelectedTile = {row, col: pos2.col};

      if (this.gameBoard[corner1.row][corner1.col] === -1 &&
        this.gameBoard[corner2.row][corner2.col] === -1 &&
      this.canConnectStraight(pos1, corner1) &&
      this.canConnectStraight(corner1, corner2) &&
      this.canConnectStraight(corner2, pos2)) {
        return [corner1, corner2];
      }
    }

    // 检查列方向可能的连接
    for (let col = 0; col < this.COLS; col++) {
      if (col === pos1.col || col === pos2.col) continue;

      const corner1:SelectedTile = {row: pos1.row, col};
      const corner2:SelectedTile = {row: pos2.row, col};

      if (this.gameBoard[corner1.row][corner1.col] === -1 &&
        this.gameBoard[corner2.row][corner2.col] === -1 &&
      this.canConnectStraight(pos1, corner1) &&
      this.canConnectStraight(corner1, corner2) &&
      this.canConnectStraight(corner2, pos2)) {
        return [corner1, corner2];
      }
    }

    return [];
  }

  // 提示功能
  showHint() {
    if (this.gameOver || this.showHint1) return;

    // 查找可消除的对
    for (let row1 = 0; row1 < this.ROWS; row1++) {
      for (let col1 = 0; col1 < this.COLS; col1++) {
        if (this.gameBoard[row1][col1] === -1) continue;

        for (let row2 = 0; row2 < this.ROWS; row2++) {
          for (let col2 = 0; col2 < this.COLS; col2++) {
            if ((row1 === row2 && col1 === col2) || this.gameBoard[row2][col2] === -1) continue;

            if (this.gameBoard[row1][col1] === this.gameBoard[row2][col2]) {
              const path = this.findConnectionPath({row: row1, col: col1}, {row: row2, col: col2});
              if (path.length > 0) {
                this.hintPath = path;
                this.showHint1 = true;

                this.hintTimeout = setTimeout(() => {
                  this.showHint1 = false;
                  this.hintPath = [];
                }, this.HINT_DURATION);

                return;
              }
            }
          }
        }
      }
    }

    // 如果没有找到可消除的对
    this.message = "没有可消除的对,请重新开始游戏";
    setTimeout(() => this.message = `剩余: ${this.remainingPairs}对`, 2000);
  }

  // 格式化时间
  private formatTime(seconds: number): string {
    const mins = Math.floor(seconds / 60);
    const secs = seconds % 60;
    return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
  }

  build() {
    Column() {
      Text('动物连连看')
        .fontSize(26)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .margin({ bottom: 16 })

      Row() {
        Column() {
          Text('剩余')
            .fontSize(14)
            .fontColor('#666666')
          Text(`${this.remainingPairs}对`)
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
        }
        .layoutWeight(1)

        Column() {
          Text('时间')
            .fontSize(14)
            .fontColor('#666666')
          Text(this.formatTime(this.timer))
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
        }
        .layoutWeight(1)
      }
      .width('80%')
      .margin({ bottom: 20 })

      // 提示信息
      if (this.message !== `剩余: ${this.remainingPairs}对`) {
        Text(this.message)
          .fontSize(16)
          .fontColor('#FF9800')
          .margin({ bottom: 8 })
      }

      // 游戏棋盘
      Grid() {
        ForEach(this.gameBoard, (row: number[], rowIndex) => {
          ForEach(row, (tile: number, colIndex) => {
            GridItem() {
              this.AnimalTile(
                tile,
                this.selectedTile?.row === rowIndex && this.selectedTile?.col === colIndex,
                tile === -1,
                rowIndex,
                colIndex,
                this.isInHintPath(rowIndex, colIndex)
              )
            }
          })
        })
      }
      .columnsTemplate(Array(this.COLS).fill('1fr').join(' '))
      .rowsTemplate(Array(this.ROWS).fill('1fr').join(' '))
      .width('100%')
      .height('60%')
      .margin({ bottom: 24 })

      // 控制按钮
      Row() {
        Button('提示')
          .width('40%')
          .height(48)
          .backgroundColor('#2196F3')
          .fontColor(Color.White)
          .fontSize(18)
          .borderRadius(24)
          .margin({ right: 10 })
          .onClick(() => {
            this.showHint()
          })

        Button(this.gameOver ? '重新开始' : '新游戏')
          .width('40%')
          .height(48)
          .backgroundColor(this.gameOver ? '#FF5252' : '#4CAF50')
          .fontColor(Color.White)
          .fontSize(18)
          .borderRadius(24)
          .onClick(() => this.startNewGame())
      }
      .width('90%')
      .margin({ bottom: 20 })

      if (this.gameOver) {
        Text('恭喜通关!')
          .fontSize(20)
          .fontColor('#FF5252')
          .margin({ top: 10 })
      }
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#FAFAFA')
    .justifyContent(FlexAlign.Center)
  }

  // 检查是否在提示路径中
  private isInHintPath(row: number, col: number): boolean {
    return this.showHint1 && this.hintPath.some(pos => pos.row === row && pos.col === col);
  }

  @Builder
  AnimalTile(
    animalIndex: number,
    isSelected: boolean,
    isEmpty: boolean,
    rowIndex: number,
    colIndex: number,
    isHint: boolean
  ) {
    Stack() {
      if (!isEmpty) {
        Column() {
          Text(this.ANIMAL_ICONS[animalIndex % this.ANIMAL_ICONS.length])
            .fontSize(24)
        }
        .width('88%')
        .height('88%')
        .backgroundColor(isSelected ? '#FFEB3B' : (isHint ? '#FF9800' : '#FFFFFF'))
        .border({
          width: isHint ? 3 : 1,
          color: isHint ? '#FF9800' : Color.Black
        })
        .borderRadius(8)
        .transition(TransitionEffect.opacity(0.99).animation({ duration: 1000 }))
      } else {
        Column()
          .width('88%')
          .height('88%')
          .backgroundColor('#EEEEEE')
      }

      // 连接路径指示器
      if (isHint && !isEmpty) {
        Column()
          .width('88%')
          .height('88%')
          .backgroundColor('#ffec2727')
          .opacity(0.3)
          .borderRadius(8)
      }
    }
    .width('100%')
    .height('100%')
    .onClick(() => {
      // 点击处理
      this.handleTileClick(rowIndex, colIndex)
    })
  }
}

总结与展望

通过这个鸿蒙动物连连看游戏的开发,我们实践了鸿蒙应用开发的多个核心概念:

  1. 使用ArkUI框架构建响应式界面
  2. 实现复杂的游戏逻辑和状态管理
  3. 开发高效的算法解决核心游戏问题
  4. 优化用户体验和界面交互

未来可以进一步扩展的功能包括:

  • 增加音效和更丰富的动画效果
  • 实现难度等级选择
  • 添加积分排行榜
  • 支持多人对战模式
  • 增加更多主题和皮肤选择

这个项目完整展示了如何使用鸿蒙系统开发一个功能完善的游戏应用,希望能为鸿蒙开发者提供有价值的参考。

收藏00

登录 后评论。没有帐号? 注册 一个。