纯血HarmonyOS5 打造小游戏实践:扫雷(附源文件)

发布于:2025-06-24 ⋅ 阅读:(22) ⋅ 点赞:(0)

鸿蒙扫雷游戏的核心架构设计

鸿蒙OS扫雷游戏采用了MVC(模型-视图-控制器)的架构思想,将游戏逻辑与UI展示分离,使得代码结构清晰且易于维护。整个游戏由以下几个核心部分构成:

数据模型设计

游戏的基础数据模型是Cell类,它代表扫雷游戏中的每个方块,包含位置信息、地雷状态、相邻地雷数量等关键属性:

@ObservedV2
class Cell {
  row: number;         // 行坐标
  column: number;      // 列坐标
  hasMine: boolean;    // 是否包含地雷
  neighborMines: number; // 相邻地雷数量
  @Trace isFlag: boolean; // 是否已标记
  @Trace value: string;   // 显示值(数字或"雷")

  constructor(row: number, column: number) {
    this.row = row;
    this.column = column;
    this.value = '';
  }
}

@ObservedV2装饰器使Cell类具备数据观察能力,当属性变化时能自动通知视图更新,这是鸿蒙OS声明式UI的核心特性之一。@Trace装饰器则用于追踪属性变化,帮助开发者调试数据流向。

游戏状态管理

游戏的状态管理集中在Index组件中,通过@State装饰器管理界面相关状态,确保数据变化时UI能自动刷新:

@Entry
@Component
struct Index {
  @State private gameBoard: Cell[][] = [];    // 游戏面板
  @State private mineCount: number = 10;      // 地雷数量
  @State private revealedCells: Set<string> = new Set(); // 已揭示方块
  @State private flaggedCells: Set<string> = new Set();  // 标记方块
  @State private cellSize: number = 60;       // 方块大小
  @State private isGameOver: boolean = false; // 游戏结束状态
  // 其他状态...
}

使用Set数据结构存储已揭示和标记的方块,利用其快速查找特性(时间复杂度O(1))提升游戏性能。字符串键"row,column"的设计巧妙地将二维坐标转换为一维键,便于状态管理。

双向绑定与视图更新

鸿蒙OS的声明式UI特性在扫雷游戏中体现为数据与视图的双向绑定,当gameBoard或状态集合发生变化时,build方法会自动重新渲染相关组件:

Text(this.isShowValue(cell))
  .width(`${this.cellSize}lpx`)
  .height(`${this.cellSize}lpx`)
  .backgroundColor(this.revealedCells.has(`${rowIndex},${colIndex}`) ?
    (this.isShowValue(cell) === '雷' ? Color.Red : Color.White) : Color.Gray)

上述代码中,背景颜色根据revealedCells集合中的状态动态变化,无需手动操作DOM,大大简化了开发流程。

核心算法与游戏逻辑实现

扫雷游戏的核心在于地雷放置、相邻计算和自动揭示算法,这些算法的效率直接影响游戏体验。

地雷放置与数字计算

地雷放置采用随机算法,确保在10x10的网格中均匀分布指定数量的地雷:

private placeMines() {
  let placed = 0;
  while (placed < this.mineCount) {
    let x = Math.floor(Math.random() * 10);
    let y = Math.floor(Math.random() * 10);
    if (!this.gameBoard[x][y].hasMine) {
      this.gameBoard[x][y].hasMine = true;
      placed++;
    }
  }
}

为避免地雷重叠,通过hasMine属性检查方块是否已放置地雷。这种实现方式简单直观,但在极端情况下(如剩余可放置位置较少时)可能出现性能问题,更优的实现可采用Fisher-Yates洗牌算法。

相邻地雷计算是扫雷游戏的核心算法之一,通过遍历当前方块周围8个方向的方块来统计地雷数量:

private countNeighborMines(row: number, col: number): number {
  let count = 0;
  for (let dx = -1; dx <= 1; dx++) {
    for (let dy = -1; dy <= 1; dy++) {
      if (dx === 0 && dy === 0) continue;
      let newRow = row + dx, newCol = col + dy;
      if (newRow >= 0 && newRow < 10 && newCol >= 0 && newCol < 10 && this.gameBoard[newRow][newCol].hasMine) {
        count++;
      }
    }
  }
  return count;
}

该算法的时间复杂度为O(1),因为每个方块最多检查8个相邻方块,确保了游戏的流畅运行。

自动揭示与胜利判定

自动揭示算法是扫雷游戏的灵魂,当玩家点击一个周围没有地雷的方块时,需要递归揭示其周围所有安全方块:

private revealCell(row: number, col: number) {
  if (this.isGameOver || this.revealedCells.has(`${row},${col}`)) return;
  
  const key = `${row},${col}`;
  this.revealedCells.add(key);

  if (this.gameBoard[row][col].hasMine) {
    this.showGameOverDialog();
  } else {
    if (this.gameBoard[row][col].neighborMines === 0) {
      for (let dx = -1; dx <= 1; dx++) {
        for (let dy = -1; dy <= 1; dy++) {
          if (dx === 0 && dy === 0) continue;
          let newRow = row + dx, newCol = col + dy;
          if (newRow >= 0 && newRow < 10 && newCol >= 0 && newCol < 10) {
            this.revealCell(newRow, newCol);
          }
        }
      }
    }
  }

  if (this.isVictory()) {
    this.showVictoryDialog();
  }
}

递归实现的自动揭示算法简洁明了,但在极端情况下(如点击一个大面积的空白区域)可能导致栈溢出。鸿蒙OS的JavaScript引擎支持尾递归优化,但更安全的实现可采用迭代方式(如BFS或DFS)。

胜利判定算法通过统计已揭示的非地雷方块数量来判断游戏是否胜利:

private isVictory() {
  let revealedNonMineCount = 0;
  const totalNonMineCells = 10 * 10 - this.mineCount;
  
  for (let i = 0; i < 10; i++) {
    for (let j = 0; j < 10; j++) {
      if (this.revealedCells.has(`${i},${j}`)) {
        revealedNonMineCount++;
      }
    }
  }

  return revealedNonMineCount === totalNonMineCells;
}

性能优化策略

扫雷游戏在鸿蒙OS上的性能优化主要体现在以下几个方面:

  1. 数据结构优化:使用Set存储已揭示和标记的方块,确保查找操作的高效性
  2. 避免重复计算:地雷相邻数量在初始化时计算完成,游戏过程中无需重复计算
  3. 事件节流:虽然代码中未显式实现,但鸿蒙OS的手势处理底层已做优化,避免高频点击导致的性能问题
  4. 组件复用:通过ForEach循环动态生成方块组件,避免静态创建100个组件的性能开销

交互设计与用户体验优化

鸿蒙扫雷游戏在交互设计上充分考虑了移动端的操作特点,通过手势识别和视觉反馈提升用户体验。

手势交互实现

游戏支持两种核心手势:单击揭示方块和长按标记地雷,通过parallelGesture实现手势组合:

.parallelGesture(GestureGroup(GestureMode.Exclusive,
  TapGesture({ count: 1, fingers: 1 })
    .onAction(() => this.revealCell(rowIndex, colIndex)),
  LongPressGesture({ repeat: true })
    .onAction(() => cell.isFlag = true)
));

GestureMode.Exclusive确保两种手势不会冲突,长按操作会优先于单击操作。repeat: true参数允许长按持续触发标记状态,提升操作流畅度。

视觉反馈设计

视觉反馈是扫雷游戏体验的重要组成部分,鸿蒙游戏通过以下方式提供清晰的反馈:

  1. 颜色变化:已揭示的方块背景色变为白色,地雷方块显示为红色
  2. 数字标识:显示相邻地雷数量,数字颜色根据数量不同(如1为蓝色,2为绿色)
  3. 标记旗帜:长按标记的方块显示"旗"字,帮助用户记录可疑地雷位置
  4. 游戏状态提示:通过对话框提示游戏结束或胜利,并显示用时
Text(this.isShowValue(cell))
  .backgroundColor(this.revealedCells.has(`${rowIndex},${colIndex}`) ?
    (this.isShowValue(cell) === '雷' ? Color.Red : Color.White) : Color.Gray)
  .fontColor(!this.revealedCells.has(`${rowIndex},${colIndex}`) || this.isShowValue(cell) === '雷' ?
    Color.White : Color.Black)

响应式布局设计

游戏界面采用响应式设计,通过cellSize状态动态调整方块大小,确保在不同尺寸的设备上都有良好的显示效果:

// 游戏面板容器
Flex({ wrap: FlexWrap.Wrap }) {
  // 方块生成逻辑...
}
.width(`${(this.cellSize + this.cellMargin * 2) * 10}lpx`);

这种设计使得游戏能够自适应手机、平板等不同设备的屏幕尺寸,无需为每种设备单独开发界面。

鸿蒙特性在游戏开发中的应用

扫雷游戏的鸿蒙实现充分利用了平台的特有能力,展现了鸿蒙OS在应用开发中的优势。

声明式UI与状态管理

鸿蒙OS的声明式UI模型使得游戏界面与数据状态紧密绑定,当gameBoard或状态集合变化时,UI会自动更新,大大简化了开发流程:

// 状态变化自动触发UI更新
this.revealedCells.add(`${row},${col}`);

// 视图根据状态动态渲染
Text(this.isShowValue(cell))
  .visibility(cell.isFlag && !this.isGameOver ? Visibility.Visible : Visibility.None)

这种数据驱动视图的模式避免了传统命令式UI中繁琐的DOM操作,提高了代码的可维护性。

组件化与复用

游戏中的每个方块都是一个可复用的组件单元,通过ForEach循环动态生成,这种组件化思想是鸿蒙应用开发的核心:

ForEach(this.gameBoard, (row: Cell[], rowIndex: number) => {
  ForEach(row, (cell: Cell, colIndex: number) => {
    Stack() {
      // 方块UI组件...
    }
  });
});

组件化设计使得游戏界面结构清晰,同时便于后续扩展,如添加不同难度级别或主题皮肤。

系统能力集成

游戏集成了鸿蒙OS的系统能力,如对话框提示功能:

promptAction.showDialog({
  title: '游戏结束: 游戏失败!',
  buttons: [{ text: '重新开始', color: '#ffa500' }]
}).then(() => {
  this.initializeGame();
});

promptAction是鸿蒙OS提供的系统对话框API,支持自定义标题、消息和按钮,使得游戏能够无缝融入系统交互体系。

游戏扩展与进阶优化

基于当前的扫雷游戏实现,还可以从以下几个方面进行扩展和优化:

难度级别扩展

添加不同难度级别是扫雷游戏的常见需求,可通过修改网格大小和地雷数量实现:

// 简单模式:8x8网格,10个地雷
private initEasyMode() {
  this.mineCount = 10;
  this.generateBoard(8, 8);
}

// 中级模式:16x16网格,40个地雷
private initMediumMode() {
  this.mineCount = 40;
  this.generateBoard(16, 16);
}

// 高级模式:30x16网格,99个地雷
private initHardMode() {
  this.mineCount = 99;
  this.generateBoard(30, 16);
}

同时需要调整cellSize以适应不同网格大小,确保界面在各种难度下都能良好显示。

主题与视觉优化

添加主题切换功能可以提升游戏体验,可通过定义不同的颜色方案实现:

// 主题接口
interface Theme {
  cellColor: ResourceColor;
  revealedColor: ResourceColor;
  mineColor: ResourceColor;
  numberColors: ResourceColor[];
}

// 亮色主题
const lightTheme: Theme = {
  cellColor: Color.Gray,
  revealedColor: Color.White,
  mineColor: Color.Red,
  numberColors: [Color.Blue, Color.Green, Color.Orange, Color.Red, 
                Color.Purple, Color.Teal, Color.Brown, Color.Black]
};

// 暗色主题
const darkTheme: Theme = {
  cellColor: Color.DarkGray,
  revealedColor: Color.DimGray,
  mineColor: Color.Red,
  numberColors: [Color.SkyBlue, Color.Lime, Color.Orange, Color.Red, 
                Color.Plum, Color.Aqua, Color.Chocolate, Color.White]
};

性能与体验优化

针对大规模网格(如高级模式30x16),可实现以下优化:

  1. 虚拟滚动:只渲染可见区域的方块,而非全部方块
  2. 异步初始化:使用setTimeout分批次初始化地雷和计算数字,避免UI卡顿
  3. 手势优化:添加双击快捷揭示功能(点击数字方块时揭示周围未标记方块)
  4. 动画效果:为方块揭示添加淡入动画,提升视觉体验
// 双击快捷揭示
doubleTapGesture()
  .onAction(() => {
    if (this.gameBoard[row][col].neighborMines > 0) {
      this.revealSurroundingCells(row, col);
    }
  })

附:代码

import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  // 游戏面板数据
  @State private gameBoard: Cell[][] = [];
  // 地雷总数
  @State private mineCount: number = 10;
  // 已经揭示的方块集合
  @State private revealedCells: Set<string> = new Set();
  // 标记为地雷的方块集合
  @State private flaggedCells: Set<string> = new Set();
  // 方块大小
  @State private cellSize: number = 60;
  // 方块之间的边距
  @State private cellMargin: number = 2;
  // 游戏开始时间
  private startTime: number = Date.now();
  // 游戏结束标志
  @State private isGameOver: boolean = false;

  // 在组件即将显示时初始化游戏
  aboutToAppear(): void {
    this.initializeGame();
  }

  // 初始化游戏
  private initializeGame() {
    this.isGameOver = false;
    this.startTime = Date.now();
    this.revealedCells.clear();
    this.flaggedCells.clear();
    this.generateBoard();
  }

  // 生成游戏面板
  private generateBoard() {
    this.gameBoard = [];
    for (let i = 0; i < 10; i++) {
      this.gameBoard.push([]);
      for (let j = 0; j < 10; j++) {
        this.gameBoard[i].push(new Cell(i, j));
      }
    }
    this.placeMines();
    this.calculateNumbers();
  }

  // 随机放置地雷
  private placeMines() {
    let placed = 0;
    while (placed < this.mineCount) {
      let x = Math.floor(Math.random() * 10);
      let y = Math.floor(Math.random() * 10);
      if (!this.gameBoard[x][y].hasMine) {
        this.gameBoard[x][y].hasMine = true;
        placed++;
      }
    }
  }

  // 计算每个方块周围的地雷数量
  private calculateNumbers() {
    for (let i = 0; i < 10; i++) {
      for (let j = 0; j < 10; j++) {
        if (!this.gameBoard[i][j].hasMine) {
          this.gameBoard[i][j].neighborMines = this.countNeighborMines(i, j);
          this.gameBoard[i][j].value = this.gameBoard[i][j].neighborMines.toString();
        } else {
          this.gameBoard[i][j].value = '雷';
        }
      }
    }
  }

  // 计算给定坐标周围地雷的数量
  private countNeighborMines(row: number, col: number): number {
    let count = 0;
    for (let dx = -1; dx <= 1; dx++) {
      for (let dy = -1; dy <= 1; dy++) {
        if (dx === 0 && dy === 0) {
          continue;
        }
        let newRow = row + dx, newCol = col + dy;
        if (newRow >= 0 && newRow < 10 && newCol >= 0 && newCol < 10 && this.gameBoard[newRow][newCol].hasMine) {
          count++;
        }
      }
    }
    return count;
  }

  // 揭示方块
  private revealCell(row: number, col: number) {
    if (this.isGameOver || this.revealedCells.has(`${row},${col}`)) {
      return;
    }

    const key = `${row},${col}`;
    this.revealedCells.add(key);

    if (this.gameBoard[row][col].hasMine) {
      this.showGameOverDialog();
    } else {
      if (this.gameBoard[row][col].neighborMines === 0) {
        for (let dx = -1; dx <= 1; dx++) {
          for (let dy = -1; dy <= 1; dy++) {
            if (dx === 0 && dy === 0) {
              continue;
            }
            let newRow = row + dx, newCol = col + dy;
            if (newRow >= 0 && newRow < 10 && newCol >= 0 && newCol < 10) {
              this.revealCell(newRow, newCol);
            }
          }
        }
      }
    }

    if (this.isVictory()) {
      this.showVictoryDialog();
    }
  }

  // 显示游戏结束对话框
  private showGameOverDialog() {
    this.isGameOver = true;
    promptAction.showDialog({
      title: '游戏结束: 游戏失败!',
      buttons: [{ text: '重新开始', color: '#ffa500' }]
    }).then(() => {
      this.initializeGame();
    });
  }

  // 显示胜利对话框
  private showVictoryDialog() {
    this.isGameOver = true;
    promptAction.showDialog({
      title: '恭喜你,游戏胜利!',
      message: `用时:${((Date.now() - this.startTime) / 1000).toFixed(3)}秒`,
      buttons: [{ text: '重新开始', color: '#ffa500' }]
    }).then(() => {
      this.initializeGame();
    });
  }

  // 判断游戏是否胜利
  private isVictory() {
    let revealedNonMineCount = 0;

    for (let i = 0; i < this.gameBoard.length; i++) {
      for (let j = 0; j < this.gameBoard[i].length; j++) {
        if (this.revealedCells.has(`${i},${j}`)) {
          revealedNonMineCount++;
        }
      }
    }

    return revealedNonMineCount == 90;
  }

  // 决定是否显示方块值
  private isShowValue(cell: Cell): string {
    if (this.isGameOver) {
      return cell.value === '0' ? '' : cell.value;
    } else {
      if (this.revealedCells.has(`${cell.row},${cell.column}`)) {
        return cell.value === '0' ? '' : cell.value;
      } else {
        return '';
      }
    }
  }

  build() {
    Column({ space: 10 }) {
      // 重置游戏按钮
      Button('重新开始').onClick(() => {
        this.initializeGame()
      });

      // 创建游戏面板容器
      Flex({ wrap: FlexWrap.Wrap }) {
        // 遍历每一行
        ForEach(this.gameBoard, (row: Cell[], rowIndex: number) => {
          // 遍历每一列
          ForEach(row, (cell: Cell, colIndex: number) => {
            Stack() {
              // 显示方块上的数字或雷
              Text(this.isShowValue(cell))
                .width(`${this.cellSize}lpx`)
                .height(`${this.cellSize}lpx`)
                .margin(`${this.cellMargin}lpx`)
                .fontSize(`${this.cellSize / 2}lpx`)
                .textAlign(TextAlign.Center)
                .backgroundColor(this.revealedCells.has(`${rowIndex},${colIndex}`) ?
                  (this.isShowValue(cell) === '雷' ? Color.Red : Color.White) : Color.Gray)
                .fontColor(!this.revealedCells.has(`${rowIndex},${colIndex}`) || this.isShowValue(cell) === '雷' ?
                Color.White : Color.Black)
                .borderRadius(5)
                .parallelGesture(GestureGroup(GestureMode.Exclusive,
                  TapGesture({ count: 1, fingers: 1 })
                    .onAction(() => this.revealCell(rowIndex, colIndex)),
                  LongPressGesture({ repeat: true })
                    .onAction(() => cell.isFlag = true)
                ));

              // 显示标记旗帜
              Text(`${!this.revealedCells.has(`${rowIndex},${colIndex}`) ? '旗' : ''}`)
                .width(`${this.cellSize}lpx`)
                .height(`${this.cellSize}lpx`)
                .margin(`${this.cellMargin}lpx`)
                .fontSize(`${this.cellSize / 2}lpx`)
                .textAlign(TextAlign.Center)
                .fontColor(Color.White)
                .visibility(cell.isFlag && !this.isGameOver ? Visibility.Visible : Visibility.None)
                .onClick(() => {
                  cell.isFlag = false;
                })
            }
          });
        });
      }
      .width(`${(this.cellSize + this.cellMargin * 2) * 10}lpx`);
    }
    .padding(20)
    .backgroundColor('#ffffff')
    .width('100%')
    .height('100%');
  }
}

// 方块类
@ObservedV2
class Cell {
  // 方块所在的行
  row: number;
  // 方块所在的列
  column: number;
  // 是否有地雷
  hasMine: boolean = false;
  // 周围地雷数量
  neighborMines: number = 0;
  // 是否被标记为地雷
  @Trace isFlag: boolean = false;
  // 方块值
  @Trace value: string;

  // 构造函数
  constructor(row: number, column: number) {
    this.row = row;
    this.column = column;
    this.value = '';
  }
}

鸿蒙OS扫雷游戏的开发实践展示了平台在游戏应用开发中的强大能力,从声明式UI的高效开发到系统能力的深度集成,再到性能优化和交互设计,鸿蒙OS为开发者提供了完整的解决方案。扫雷游戏作为经典的算法与交互结合的案例,其实现过程涵盖了数据结构、算法设计、用户体验等多个维度,对于理解移动应用开发具有重要参考价值。

随着鸿蒙OS生态的不断发展,游戏开发者可以进一步利用ArkTS语言的特性和平台的底层能力,开发出更复杂、更具沉浸感的游戏应用。从扫雷这样的轻量级游戏到更大型的3D游戏,鸿蒙OS都提供了从开发到部署的全流程支持,为开发者和用户带来全新的体验。


网站公告

今日签到

点亮在社区的每一天
去签到