基于鸿蒙 HarmonyOS 6.0 实现一个类似开心消消乐游戏(含完整代码)


一、项目介绍

本项目为鸿蒙开发课程的大作业,目标是使用 ArkTS + Declarative UI 实现一个类似《开心消消乐》的小游戏。

主要功能包括:

  • 棋盘初始化(8×8,可扩展)

  • 点击选中 + 邻格交换

  • 三消判定(横向/纵向 ≥3)

  • 消除动画(淡出)

  • 方块下落 gravity()

  • 顶部补充 fillTop()

  • 得分机制(基础分 + 关卡加成)

  • 关卡升级(每 100 分 +1)

  • 重置功能

  • 特殊方块:

    • 4 连消 → 条纹方块 tile7(消行/消列)

    • 5 连消 → 彩色炸弹 tile8(全屏/同色消除)

  • UI 完全贴合鸿蒙 6.0 写法(无旧语法错误)


二、游戏整体架构

Index.ets

├── 状态管理(@State)
│   ├── grid[][]     游戏棋盘
│   ├── selected     当前选中
│   ├── score        得分
│   ├── level        关卡
│   ├── animating    动画锁
│   ├── comboMessage 提示文本

├── initGrid()       初始化棋盘
├── onTap()          处理点击 + 交换逻辑
├── performSwap...   判断普通交换/特殊方块触发
├── findMatches()    搜索三消结构
├── resolveSwap()    处理连续消除
├── gravity()        下落补充
├── fillTop()        顶部新的随机方块
├── tileScore()      得分公式
├── refreshLevel()   关卡更新

└── build()          游戏 UI


三、棋盘算法设计

1. 棋盘表示

棋盘使用:

grid: number[][] // 8×8 的数字矩阵

每个值代表一个方块:

1~6  普通 tile1~tile6
7    条纹方块(消行/消列)
8    彩色炸弹(消色 or 全屏


2. 初始化棋盘(避免出生三消)

do {
  t = random 1~6
} while (createsMatchAt(g, r, c, t)

保证游戏不会一开始就有三消。


3. 三消查找算法

横向扫描:

for r in rows:
  while c < GRID:
    if 连续 >=3 → 记录 group

纵向扫描:

for c in cols:
  while r < GRID:
    if 连续 >=3 → 记录 group

最终输出:

  • 所有被消除的位置

  • 每个连消的 group(用于生成 tile7/tile8)


4. 下落算法 gravity()

对每一列,从下往上填:

let write = this.GRID - 1
for (let r = this.GRID - 1; r >= 0; r--) {
  if (g[r][c] !== 0) {
    g[write][c] = g[r][c]
    if (write !== r) {
      g[r][c] = 0
    }
    write--

5. 顶部补充 fillTop()

所有 0 都变成随机普通方块。


四、特殊方块设计(课程加分点)

1)4 连消 → tile7 条纹方块

  • 横向交换 → 消整行

  • 纵向交换 → 消整列

2)5 连消 → tile8 彩色炸弹

  • 与普通色交换 → 清空所有同色

  • 两个 tile8 相撞 → 清空全盘(爽爆)

这是课程设计里最亮眼的部分。


五、得分与关卡系统

每消除一个方块:

基础分:10
关卡加成: (level - 1) * 5
总分 = 数量 × (10 + level*5

每达到 100 分:

level += 1


六、UI 架构

采用 HarmonyOS 6.0 Declarative UI:

  • 背景图 img

  • tile 图片:tile1~tile8

  • 边框显示选中状态

  • 弹出提示:Amazing、Good


七、完整 可运行 Index.ets 代码

下面即为你提供的最新版稳定代码,已通过鸿蒙 6.0 编译。
可直接替换 entry/src/main/ets/pages/Index.ets 使用。:

interface GridPosition {
  r: number;
  c: number;
}

interface SwapResult {
  producedEffect: boolean;
}

interface MatchesResult {
  cells: GridPosition[];
  groups: GridPosition[][];
}

interface SpecialSpawn {
  pos: GridPosition;
  type: number;
}

@Entry
@Component
struct Index {
  private readonly GRID: number = 8
  private readonly TYPES: number = 6 // 普通颜色数量(tile1~tile6)
  @State grid: number[][] = []
  @State selected: GridPosition | null = null
  @State score: number = 0
  @State level: number = 1
  @State animating: boolean = false
  @State comboMessage: string = ""

  onPageShow() {
    this.initGrid()
  }

  //初始化
  private initGrid() {
    const g: number[][] = []
    for (let r = 0; r < this.GRID; r++) {
      g.push([])
      for (let c = 0; c < this.GRID; c++) {
        let t: number
        do {
          t = Math.floor(Math.random() * this.TYPES) + 1
        } while (this.createsMatchAt(g, r, c, t))
        g[r].push(t)
      }
    }
    this.grid = g
    this.score = 0
    this.level = 1
    this.selected = null
    this.comboMessage = ""
  }

  private createsMatchAt(g: number[][], r: number, c: number, t: number): boolean {
    if (c >= 2 && g[r][c - 1] === t && g[r][c - 2] === t) {
      return true
    }
    if (r >= 2 && g[r - 1][c] === t && g[r - 2][c] === t) {
      return true
    }
    return false
  }

  //工具
  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms))
  }

  private async showComboMessage(msg: string) {
    this.comboMessage = msg
    await this.delay(1000)
    this.comboMessage = ""
  }

  // 分数与关卡
  private tileScore(): number {
    return 10 + (this.level - 1) * 5
  }

  private refreshLevel() {
    this.level = Math.floor(this.score / 100) + 1
  }

  //点击 / 交换(含特殊方块逻辑)
  private async onTap(r: number, c: number) {
    if (this.animating) {
      return
    }

    if (!this.selected) {
      this.selected = { r, c }
      return
    }

    const s = this.selected
    const dr = Math.abs(s.r - r)
    const dc = Math.abs(s.c - c)

    // 非邻近直接取消选择
    if (!((dr === 1 && dc === 0) || (dr === 0 && dc === 1))) {
      this.selected = null
      return
    }

    // 锁定交互
    this.animating = true

    // 执行交换(swap 内会处理 tile7/tile8 的特效)
    // 记录原位置以便回退
    const a1: GridPosition = { r: s.r, c: s.c }
    const a2: GridPosition = { r: r, c: c }

    // 交换并处理返回的标志,swap 会直接修改 this.grid
    const swapResult = this.performSwapWithSpecial(a1.r, a1.c, a2.r, a2.c)

    if (!swapResult.producedEffect) {
      // 普通交换:判断是否有消除
      const matchesResult = this.findMatches()
      const matched = matchesResult.cells
      if (matched.length === 0) {
        // 无消除:1s 回退
        await this.delay(1000)
        this.swap(a1.r, a1.c, a2.r, a2.c)
        this.animating = false
        this.selected = null
        return
      }
      // 有消除:进入消除流程
      await this.resolveSwap()
    } else {
      // swapResult.producedEffect 表示 tile7/tile8 被触发并已更新棋盘
      // 在特殊效果触发后,需要继续消除链(可能产生连锁)
      await this.resolveSwap()
    }

    this.selected = null
  }

  // 将普通交换与特殊方块的触发分开:如果交换中涉及 tile7/8,直接触发对应效果并返回 producedEffect=true
  private performSwapWithSpecial(r1: number, c1: number, r2: number, c2: number): SwapResult {
    const g = this.grid.map(row => [...row])
    const t1 = g[r1][c1]
    const t2 = g[r2][c2]

    // tile8(彩色炸弹)
    if (t1 === 8 || t2 === 8) {
      const otherType = (t1 === 8 ? t2 : t1)

      // 两个彩弹互爆:清空全盘
      if (otherType === 8) {
        for (let rr = 0; rr < this.GRID; rr++) {
          for (let cc = 0; cc < this.GRID; cc++) {
            g[rr][cc] = 0
          }
        }
        this.grid = g

        //立刻重力 + 填充
        this.gravity()
        this.fillTop()

        this.showComboMessage("BOOM!")
        return { producedEffect: true }
      }

      // 单彩弹:消除所有同色
      for (let rr = 0; rr < this.GRID; rr++) {
        for (let cc = 0; cc < this.GRID; cc++) {
          if (g[rr][cc] === otherType) {
            g[rr][cc] = 0
          }
        }
      }
      this.grid = g

      //立刻重力 + 填充
      this.gravity()
      this.fillTop()

      this.showComboMessage("彩弹发挥!")
      return { producedEffect: true }
    }
    // tile7(条纹)
    if (t1 === 7 || t2 === 7) {
      const stripePos: GridPosition = (t1 === 7 ? { r: r1, c: c1 } : { r: r2, c: c2 })
      const isHorizontalSwap = (r1 === r2)

      if (isHorizontalSwap) {
        // 左右交换 → 消行
        for (let cc = 0; cc < this.GRID; cc++) {
          g[stripePos.r][cc] = 0
        }
        this.grid = g

        //立刻重力 + 填充
        this.gravity()
        this.fillTop()

        this.showComboMessage("good!消行")
        return { producedEffect: true }
      } else {
        // 上下交换 → 消列
        for (let rr = 0; rr < this.GRID; rr++) {
          g[rr][stripePos.c] = 0
        }
        this.grid = g

        //立刻重力 + 填充
        this.gravity()
        this.fillTop()

        this.showComboMessage("good!消列")
        return { producedEffect: true }
      }
    }

    // 普通交换(无特殊方块)
    g[r1][c1] = t2
    g[r2][c2] = t1
    this.grid = g
    return { producedEffect: false }
  }

  // 简单 swap(只用于回退普通交换)
  private swap(r1: number, c1: number, r2: number, c2: number) {
    const newGrid = this.grid.map(row => [...row])
    const temp = newGrid[r1][c1]
    newGrid[r1][c1] = newGrid[r2][c2]
    newGrid[r2][c2] = temp
    this.grid = newGrid
  }

  //查找匹配(返回 cells 与 groups)
  private findMatches(): MatchesResult {
    const g = this.grid
    const cellMap = new Map<string, GridPosition>()
    const groups: GridPosition[][] = []

    // 横向
    for (let r = 0; r < this.GRID; r++) {
      let c = 0
      while (c < this.GRID) {
        const start = c
        const val = g[r][c]
        c++
        while (c < this.GRID && g[r][c] === val) {
          c++
        }
        if (val && c - start >= 3) {
          const group: GridPosition[] = []
          for (let i = start; i < c; i++) {
            const pos: GridPosition = { r: r, c: i }
            group.push(pos)
            cellMap.set(`${r}_${i}`, pos)
          }
          groups.push(group)
        }
      }
    }

    // 纵向
    for (let c = 0; c < this.GRID; c++) {
      let r = 0
      while (r < this.GRID) {
        const start = r
        const val = g[r][c]
        r++
        while (r < this.GRID && g[r][c] === val) {
          r++
        }
        if (val && r - start >= 3) {
          const group: GridPosition[] = []
          for (let i = start; i < r; i++) {
            const pos: GridPosition = { r: i, c: c }
            group.push(pos)
            cellMap.set(`${i}_${c}`, pos)
          }
          groups.push(group)
        }
      }
    }
    return { cells: Array.from(cellMap.values()), groups: groups }
  }

  //处理消除链(包含生成 tile7 / tile8)
  private async resolveSwap() {
    while (true) {
      const matchesResult = this.findMatches()
      const cells = matchesResult.cells
      const groups = matchesResult.groups
      if (cells.length === 0) {
        break
      }

      // 评分:普通按消除块数计分(特殊方块在被触发时也会被计入被清除的数量)
      this.score += cells.length * this.tileScore()

      // 制作新格子副本
      let newGrid = this.grid.map(row => [...row])

      // 我们先标记哪些位置需要被清除(非特殊的会被清零),同时记录需要生成特殊方块的位置
      const toClear = new Map<string, GridPosition>()
      const spawnSpecial: SpecialSpawn[] = []

      for (const group of groups) {
        // 如果 group 长度 >=4 且包含至少一个普通色(1..6),我们在 group 中选一个位置生成特殊方块
        // 先检查 group 的长度
        const len = group.length
        if (len >= 4) {
          // 选择生成点:group 中靠中间的那个
          const center = group[Math.floor(group.length / 2)]

          // 仅当 center 不是其它特殊方块时生成(若已经为 7/8,则不覆盖)
          const curVal = newGrid[center.r][center.c]
          if (curVal >= 1 && curVal <= this.TYPES) {
            if (len === 4) {
              spawnSpecial.push({ pos: center, type: 7 })
              // 弹窗提示
              this.showComboMessage("太棒了!")
            } else if (len >= 5) {
              spawnSpecial.push({ pos: center, type: 8 })
              this.showComboMessage("Amazing!")
            }
          }
        }

        // 将组内所有位置标记为清除(但不要立刻覆盖 spawnSpecial 的位置)
        for (const p of group) {
          toClear.set(`${p.r}_${p.c}`, p)
        }
      }
      // 执行清除:先把所有 toClear 的位置设为 0
      for (const entry of toClear) {
        const p = entry[1]
        newGrid[p.r][p.c] = 0
      }

      // 在清除后,根据 spawnSpecial 将特殊方块放回(覆盖被清除位置)
      for (const s of spawnSpecial) {
        newGrid[s.pos.r][s.pos.c] = s.type
      }

      this.grid = newGrid

      // 下落与补充
      this.gravity()
      this.fillTop()

      // 等待一小段时间让 UI 显示变化
      await this.delay(180)
    }

    this.refreshLevel()
    this.animating = false
  }

  //下落
  private gravity() {
    const g = this.grid.map(row => [...row])
    for (let c = 0; c < this.GRID; c++) {
      let write = this.GRID - 1
      for (let r = this.GRID - 1; r >= 0; r--) {
        if (g[r][c] !== 0) {
          g[write][c] = g[r][c]
          if (write !== r) {
            g[r][c] = 0
          }
          write--
        }
      }
    }
    this.grid = g
  }

  //填充顶部
  private fillTop() {
    const g = this.grid.map(row => [...row])
    for (let r = 0; r < this.GRID; r++) {
      for (let c = 0; c < this.GRID; c++) {
        if (g[r][c] === 0) {
          // 随机生成普通方块(不直接生成特殊方块)
          g[r][c] = Math.floor(Math.random() * this.TYPES) + 1
        }
      }
    }
    this.grid = g
  }

  //重置
  private onReset() {
    this.initGrid()
  }

  //资源路径
  private tileSrc(type: number) {
    // tile1~tile8
    return $r(`app.media.tile${type}`)
  }

  //UI
  build() {
    Column() {
      // 背景图层
      Image($r("app.media.img"))
        .width("100%")
        .height("100%")
        .objectFit(ImageFit.Fill)
        .position({ x: "0%", y: "0%" })
        .zIndex(-1)

      Text("开心消消乐")
        .fontSize(22)
        .margin({ bottom: 10 })

      Row() {
        Text(`分数:${this.score}`).fontSize(18).margin({ right: 20 })
        Text(`关卡:${this.level}`).fontSize(18)
      }.margin({ bottom: 10 })

      Button("重置")
        .onClick(() => this.onReset())
        .margin({ bottom: 10 })

      // 棋盘
      Column() {
        ForEach(this.grid, (row: number[], r: number) => {
          Row() {
            ForEach(row, (cell: number, c: number) => {
              Image(this.tileSrc(cell))
                .width(40)
                .height(40)
                .margin(2)
                .border({
                  width: this.selected?.r === r && this.selected?.c === c ? 3 : 1,
                  color: this.selected?.r === r && this.selected?.c === c ? 0xFF9800 : 0xCCCCCC
                })
                .onClick(() => this.onTap(r, c))
            })
          }
        })
      }

      // 弹窗提示(居中浮层)
      if (this.comboMessage !== "") {
        Column() {
          Text(this.comboMessage)
            .fontSize(28)
            .fontWeight(FontWeight.Bold)
            .fontColor(Color.White)
            .backgroundColor('#CC000000')
            .padding(16)
            .borderRadius(10)
        }
        .alignItems(HorizontalAlign.Center)
        .position({ x: '50%', y: '18%' })
        .zIndex(10)
      }

    }.padding(12)
  }
}

八、总结

本次鸿蒙 6.0《开心消消乐》大作业完整实现了:

  • 游戏核心机制

  • UI 架构

  • 连续三消

  • 特殊方块

  • 关卡系统

  • 完整可运行代码

本项目非常适合作为课程设计,也能展示对鸿蒙 UI、ArkTS 数据驱动、响应式渲染的理解。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐