本文记录我基于 ArkTS 6.0 自己实现的一款类似「开心消消乐」小游戏的完整设计过程,包括棋盘生成、交换判定、消除逻辑、特殊方块生成、下落补充等完整玩法逻辑。
通过阅读本文,你可以清晰了解一个三消类游戏是如何从底层逻辑到前端 UI 实现的。


目录

  1. 项目简介

  2. 整体架构设计

  3. 数据结构设计

  4. 游戏主要流程

  5. 核心功能设计与实现

    • 初始化棋盘

    • 点击与交换逻辑

    • 匹配检测(查找三消)

    • 特殊方块(条纹 7、彩色炸弹 8)

    • 消除链处理

    • 重力下落与顶部补充

  6. UI 层设计

  7. 总结


1. 项目简介

本项目基于 HarmonyOS ArkTS 完整实现一个可玩的三消类消除游戏,包含:

普通消除(3 连、4 连、5 连)
特殊方块

  • 条纹方块(7):可消整行/列

  • 彩色炸弹(8):可消除全屏同色
     连锁反应
    得分系统
    关卡系统
    消除提示动画


2. 整体架构设计

项目基于 组件式结构 @Component,所有逻辑集中在一个页面中:

Index.ets

游戏由以下几部分组成:

模块 职责
状态管理 grid、score、level、selected 控制游戏运行状态
初始化模块 生成无初始连消的棋盘
点击交换模块 控制选中、判断是否可交换、执行交换
匹配检测 查找所有可消除位置(横向/纵向)
消除模块 执行清除、加分、生成特殊方块
重力模块 消除后下落所有非空方块
顶部补充 在顶部补充随机新方块
UI 展示层 网格渲染、按钮、提示框

整个项目的逻辑顺序如下:

点击 → 判定 → 交换 → 检测 → 消除 → 下落 → 填充 → 再次检测(连锁)

这就是一个完整的三消游戏循环。


3. 数据结构设计

● 1) 网格 Grid

使用二维数组表示 8×8 棋盘:

@State grid: number[][] = []

方块类型:
1~6 为普通颜色
7 = 横/竖条纹
8 = 彩色炸弹

● 2) 选择状态

用于记录当前玩家选中的方块:

@State selected: GridPosition | null = null

结构:

interface GridPosition { r: number; // 行 c: number; // 列 }

● 3) 匹配结构

用于返回所有匹配到的方块:

interface MatchesResult { cells: GridPosition[]; // 所有待消除的位置 groups: GridPosition[][]; // 每一组连续消除块 }


4. 游戏主要流程

图示流程如下:

玩家点击
    ↓
若连续方块则交换
    ↓
(若包含特殊方块 → 直接触发效果)
    ↓
普通交换 → 查找是否有消除
    ↓
没有消除 → 交换回去
    ↓
有消除 → 进入消除链
    ↓
清除 → 生成特殊方块(如果 ≥4)
    ↓
下落 + 补充
    ↓
再次检测(可能连消)

该流程可以无限循环直到没有新的消除出现。


5. 核心功能设计与实现

5.1 棋盘初始化(避免初始连消)

为了保证游戏可玩性,我们必须保证初始化棋盘不能直接出现三连块

实现思路:

  • 随机生成一个颜色

  • 放入格子前检查这个位置是否会产生 3 连

  • 如果会 → 重新随机颜色

伪代码:

do { t = 随机 1~6 } while (createsMatchAt(r, c, t))

避免初始三连的算法非常简单但很重要。


5.2 点击与交换逻辑

用户点击一次 → 选中方块
再次点击相邻方块 → 尝试交换

判断是否相邻:

(dr === 1 && dc === 0) || (dr === 0 && dc === 1)

交换方式分两种:

● 普通交换

使用 swap() 完成:

swap(r1, c1, r2, c2)

之后调用:

  • findMatches() 检查能否消除

  • 如果不能 → 回退交换

● 特殊交换(重要)

如果交换涉及 7 或 8,会触发特殊效果,不需要检查是否能消除。

例如:

8 + 任何颜色 = 清除该颜色全部
8 + 8 = 全盘清空
7 左右交换 = 横向消除一整行
7 上下交换 = 竖向消除一整列

特殊方块全部在:

performSwapWithSpecial()

中处理完毕。

这个逻辑极大地提升游戏体验。


5.3 匹配检测(查找可消除的三连)

核心目标:

✔ 找出所有连续 ≥3 的横向块
✔ 找出所有连续 ≥3 的纵向块
✔ 返回全部坐标(cells)与分组(groups)

结构如下:

MatchesResult {
  cells: 所有需要清除的点
  groups: 每一条连续块,如 [ (3,1),(3,2),(3,3),(3,4) ]
}

算法思路:

  1. 扫描每一行,找连续颜色(>=3)

  2. 扫描每一列,找连续颜色(>=3)

  3. 用 Map 去重(避免横纵重叠)

  4. 返回全部结果

这是三消游戏核心算法,无论是 ArkTS、JS、C、Unity 写法都差不多。


5.4 特殊方块生成逻辑(7 / 8)

生成条件:

连续数量 生成方块
3 普通消除
4 条纹方块(7)
≥5 彩色炸弹(8)

生成方式:

在 group 的中间块生成:

const center = group[Math.floor(len / 2)]

注意:
要先对 group 清零,再把特殊方块放回到 center 位置。


5.5 消除链(连锁反应)

当某一次消除完成后,方块下落,然后又形成新的三连,就会继续触发消除。

游戏逻辑:

while(true) {
    查找三连
    如果没有 → break
    有 → 清除 + 生成特殊方块
    gravity()
    fillTop()
}

这就是三消游戏的「爽感来源」。


5.6 下落与补充

● 重力下落 gravity()

从下往上扫描,把非 0 的方块往底部移动:

7 0 3 2 0 变成: 0 0 7 3 2

● 顶部补充 fillTop()

将空位置补充为新的随机颜色:

if (g[r][c] === 0) { g[r][c] = 随机1~6 }


6. UI 层设计(ArkTS 声明式 UI)

UI 非常简洁:

  • 使用 Image 渲染不同类型的方块

  • 通过 border 高亮选中方块

  • 通过 ForEach 渲染 8×8 棋盘

  • 通过 Column/Row 排版

  • 消除提示使用浮层

棋盘渲染核心代码:

ForEach(this.grid, (row, r) => {
  Row() {
    ForEach(row, (cell, c) => {
      Image(this.tileSrc(cell))
        .width(40)
        .height(40)
        .margin(2)
        .border(...)
        .onClick(() => this.onTap(r, c))
    })
  }
})

ArkTS 声明式 UI 写法非常现代,与 Flutter、React 非常相似,学习成本低。


7. 总结

本项目用 ArkTS 实现了一个完整的三消游戏,内容包括:

✔ 棋盘初始化(避免初始消除)
✔ 点击选中 + 交换逻辑
✔ 普通交换与特殊方块触发
✔ 查找所有连消点的算法
✔ 条纹方块(行/列消除)
✔ 彩色炸弹(同色清除 / 全屏清除)
✔ 消除链逻辑(连锁反应)
✔ 下落补充系统
✔ 简洁的 UI 渲染

下面是完整代码:

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)
  }
}
Logo

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

更多推荐