【江鸟中原】鸿蒙实现类似开心消消乐(HarmonyOS 6.0.0)——完整项目设计文档(含思路、架构、核心算法讲解)
一款类似「开心消消乐」小游戏的完整设计过程,包括棋盘生成、交换判定、消除逻辑、特殊方块生成、下落补充等完整玩法逻辑。
本文记录我基于 ArkTS 6.0 自己实现的一款类似「开心消消乐」小游戏的完整设计过程,包括棋盘生成、交换判定、消除逻辑、特殊方块生成、下落补充等完整玩法逻辑。
通过阅读本文,你可以清晰了解一个三消类游戏是如何从底层逻辑到前端 UI 实现的。
目录
-
项目简介
-
整体架构设计
-
数据结构设计
-
游戏主要流程
-
核心功能设计与实现
-
初始化棋盘
-
点击与交换逻辑
-
匹配检测(查找三消)
-
特殊方块(条纹 7、彩色炸弹 8)
-
消除链处理
-
重力下落与顶部补充
-
-
UI 层设计
-
总结
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) ]
}
算法思路:
-
扫描每一行,找连续颜色(>=3)
-
扫描每一列,找连续颜色(>=3)
-
用 Map 去重(避免横纵重叠)
-
返回全部结果
这是三消游戏核心算法,无论是 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)
}
}
更多推荐




所有评论(0)