纯血Harmony NETX 5小游戏实践:趣味三消游戏(附源文件)
在移动游戏领域,三消类游戏凭借其简单易上手的规则和充满策略性的玩法,一直占据着重要地位。本文将以鸿蒙系统的ArkTS语言为基础,深入解析一款三消游戏的完整实现过程,从核心消除算法到动画交互效果,带您领略鸿蒙应用开发的魅力。
游戏架构与核心数据结构设计
三消游戏的核心在于网格数据的管理与状态变化,在该实现中,我们通过精心设计的数据结构来支撑游戏逻辑:
// 核心游戏状态定义
@Component
export struct play_5 {// 5x5的游戏网格数据,数值代表不同类型的方块@State private gridData: number[][] = [[1, 2, 3, 2, 1],[3, 1, 2, 3, 2],[2, 3, 1, 2, 3],[1, 2, 3, 2, 1],[3, 1, 2, 3, 2]];// 方块视觉尺寸,支持屏幕适配private blockSize: number = 60;// 游戏状态标记@State private showRules: boolean = true;// 选中方块位置记录@State private selectedBlock: GeneratedTypeLiteralInterface_1 | null = null;// 动画状态矩阵@State private eliminationAnimation: boolean[][] = Array(5).fill(false).map(() => Array(5).fill(false));@State private dropAnimation: boolean[][] = Array(5).fill(false).map(() => Array(5).fill(false));// 自动检查开关@State private autoCheckEnabled: boolean = true;
}
这里采用了@State
装饰器来管理响应式数据,当gridData
等状态发生变化时,UI会自动更新。特别值得注意的是两个动画状态矩阵eliminationAnimation
和dropAnimation
,它们通过布尔值标记每个方块是否需要播放消除或下落动画,实现了视觉效果与逻辑数据的解耦。
核心消除算法的实现与优化
三消游戏的灵魂在于消除逻辑的实现,该算法需要高效地检测可消除组合并处理连锁反应:
private async eliminateMatches(): Promise<void> {// 创建标记矩阵let toEliminate: boolean[][] = this.gridData.map(row => Array(row.length).fill(false));// 横向检测for (let row = 0; row < this.gridData.length; row++) {for (let col = 0; col < this.gridData[row].length - 2; col++) {if (this.gridData[row][col] === this.gridData[row][col + 1] && this.gridData[row][col] === this.gridData[row][col + 2]) {toEliminate[row][col] = toEliminate[row][col + 1] = toEliminate[row][col + 2] = true;}}}// 纵向检测for (let row = 0; row < this.gridData.length - 2; row++) {for (let col = 0; col < this.gridData[row].length; col++) {if (this.gridData[row][col] === this.gridData[row + 1][col] && this.gridData[row][col] === this.gridData[row + 2][col]) {toEliminate[row][col] = toEliminate[row + 1][col] = toEliminate[row + 2][col] = true;}}}// 无匹配且自动检查开启时直接返回if (!toEliminate.some(row => row.some(val => val)) && this.autoCheckEnabled) {return;}// 动画与数据处理逻辑(省略部分代码)
}
算法采用两次遍历的方式:首次横向扫描检测水平方向的三个连续相同方块,第二次纵向扫描检测垂直方向的匹配。这种双重检测机制确保了所有可能的消除组合都能被识别。值得注意的是,代码中使用了some
方法来高效判断是否存在可消除方块,避免了不必要的后续处理。
连锁消除与下落逻辑的实现
消除后的方块下落与新方块生成是三消游戏的关键环节,该实现采用了高效的列优先处理策略:
// 下落逻辑核心部分
for (let col = 0; col < newGridData[0].length; col++) {let writeIndex = newGridData.length - 1;for (let row = newGridData.length - 1; row >= 0; row--) {if (newGridData[row][col] !== 0) {newGridData[writeIndex][col] = newGridData[row][col];if (writeIndex !== row) {// 记录下落位置用于动画dropPositions.push({ row, col, value: newGridData[row][col] });}writeIndex--;}}// 顶部生成新方块for (let row = 0; row <= writeIndex; row++) {const newValue = Math.floor(Math.random() * 3) + 1;newGridData[row][col] = newValue;dropPositions.push({ row, col, value: newValue });}
}
这段代码以列为单位处理方块下落,从底部向上遍历每个单元格,将非零元素移动到列的底部,顶部则生成随机新方块。这种"列优先"的处理方式比逐行处理更高效,尤其在处理多列同时下落时优势明显。同时,dropPositions
数组记录了所有需要动画的方块位置,实现了逻辑与视觉的同步。
交互体验与动画效果设计
在鸿蒙ArkTS中,通过声明式UI结合动画API可以轻松实现流畅的交互效果:
// 方块UI组件与交互逻辑
Column() {Image(this.getBlockImage(item)).width(this.blockSize).height(this.blockSize)// 消除动画:缩放效果.scale({x: this.eliminationAnimation[rowIndex][colIndex] ? 1.2 : 1,y: this.eliminationAnimation[rowIndex][colIndex] ? 1.2 : 1}).animation({ duration: 300, curve: Curve.EaseOut })// 下落动画:位移效果.translate({ y: this.dropAnimation[rowIndex][colIndex] ? -10 : 0 }).animation({ duration: 500, curve: Curve.Linear })
}
.onClick(() => {// 交互逻辑:选择与交换方块if (this.selectedBlock === null) {this.selectedBlock = { row: rowIndex, col: colIndex };} else {this.swapBlocks(this.selectedBlock, { row: rowIndex, col: colIndex });this.selectedBlock = null;}
})
.border({width: this.selectedBlock && this.selectedBlock.row === rowIndex && this.selectedBlock.col === colIndex ? 3 : 1,color: this.selectedBlock ? '#FF0000' : '#AAAAAA'
})
这里通过scale
和translate
修饰符结合animation
API,实现了消除时的缩放动画和下落时的位移动画。Curve.EaseOut
曲线让消除动画有"弹性"效果,而Curve.Linear
则让下落动画更加匀速。选中方块时的红色边框高亮效果,通过border
修饰符动态切换,提升了交互反馈的清晰度。
鸿蒙ArkTS开发的特色与优势
该三消游戏的实现充分体现了鸿蒙ArkTS开发的诸多优势:
- 响应式状态管理:通过
@State
装饰器实现数据与UI的自动同步,减少了手动更新UI的繁琐工作 - 声明式UI编程:UI布局以组件化方式声明,代码结构清晰且易于维护
- 丰富的动画API:内置的动画修饰符和曲线函数,无需额外库即可实现专业级动画效果
- 高效的组件化设计:通过
ForEach
循环动态生成网格组件,实现了数据与UI的解耦
附:源文件
interface GeneratedTypeLiteralInterface_1 {row: number;col: number;
}interface GeneratedTypeLiteralInterface_2 {row: number;col: number;value: number;
}@Component
export struct play_5 {// 游戏网格数据@State private gridData: number[][] = [[1, 2, 3, 2, 1],[3, 1, 2, 3, 2],[2, 3, 1, 2, 3],[1, 2, 3, 2, 1],[3, 1, 2, 3, 2]];// 当前方块大小(适配屏幕)private blockSize: number = 60;// 游戏说明可见性@State private showRules: boolean = true;// 存储选中的方块位置@State private selectedBlock: GeneratedTypeLiteralInterface_1 | null = null;// 消除动画状态@State private eliminationAnimation: boolean[][] = Array(5).fill(false).map(() => Array(5).fill(false));// 新增下落动画状态@State private dropAnimation: boolean[][] = Array(5).fill(false).map(() => Array(5).fill(false));// 自动检查开关@State private autoCheckEnabled: boolean = true;// 消除匹配的方块private async eliminateMatches(): Promise<void> {// 创建标记矩阵用于标记消除位置let toEliminate: boolean[][] = this.gridData.map(row => Array(row.length).fill(false));// 标记需要消除的方块(横向)for (let row = 0; row < this.gridData.length; row++) {for (let col = 0; col < this.gridData[row].length - 2; col++) {if (this.gridData[row][col] === this.gridData[row][col + 1] &&this.gridData[row][col] === this.gridData[row][col + 2]) {toEliminate[row][col] = true;toEliminate[row][col + 1] = true;toEliminate[row][col + 2] = true;}}}// 标记需要消除的方块(纵向)for (let row = 0; row < this.gridData.length - 2; row++) {for (let col = 0; col < this.gridData[row].length; col++) {if (this.gridData[row][col] === this.gridData[row + 1][col] &&this.gridData[row][col] === this.gridData[row + 2][col]) {toEliminate[row][col] = true;toEliminate[row + 1][col] = true;toEliminate[row + 2][col] = true;}}}// 如果没有可消除的方块且自动检查开启,直接返回if (!toEliminate.some(row => row.some(val => val)) && this.autoCheckEnabled) {return;}// 设置消除动画状态this.eliminationAnimation = JSON.parse(JSON.stringify(toEliminate));try {// 触发动画this.eliminationAnimation = toEliminate.map(row => row.map(val => val));// 等待动画完成setTimeout(() => {// 动画完成后的处理逻辑}, 300);// 实际消除方块for (let row = 0; row < this.gridData.length; row++) {for (let col = 0; col < this.gridData[row].length; col++) {if (toEliminate[row][col]) {this.gridData[row][col] = 0; // 设置为 0 表示消除}}}// 重置消除动画状态this.eliminationAnimation = this.eliminationAnimation.map(row => row.map(() => false));// 下落逻辑:将非零元素下落到底部const newGridData = [...this.gridData.map(row => [...row])]; // 创建副本避免直接修改状态const dropPositions: GeneratedTypeLiteralInterface_2[] = [];for (let col = 0; col < newGridData[0].length; col++) {let writeIndex = newGridData.length - 1;for (let row = newGridData.length - 1; row >= 0; row--) {if (newGridData[row][col] !== 0) {newGridData[writeIndex][col] = newGridData[row][col];if (writeIndex !== row) {// 记录下落的位置用于动画dropPositions.push({ row, col, value: newGridData[row][col] });}writeIndex--;}}// 在顶部补充新的随机方块for (let row = 0; row <= writeIndex; row++) {const newValue = Math.floor(Math.random() * 3) + 1;newGridData[row][col] = newValue;// 记录新增方块的位置用于动画dropPositions.push({ row, col, value: newValue });}}// 触发下落动画this.dropAnimation = Array(this.gridData.length).fill(0).map(() => Array(this.gridData[0].length).fill(false));// 更新网格数据并触发重新渲染this.gridData = newGridData;// 为所有下落的方块触发动画dropPositions.forEach(pos => {this.dropAnimation[pos.row][pos.col] = true;});// 再次检查是否有新的可消除组合if (this.checkForMatches(this.gridData)) {// 添加小延迟让UI有时间更新setTimeout(() => {// 模拟微任务延迟,鸿蒙ArkTS中实现异步延迟更新}, 50);this.eliminateMatches(); // 递归调用以处理连续消除}// 重置下落动画状态setTimeout(() => {this.dropAnimation = this.dropAnimation.map(row => row.map(() => false));}, 300);} catch (error) {console.error('Error during elimination:', error);}}// 交换两个相邻方块private swapBlocks(first: GeneratedTypeLiteralInterface_1, second: GeneratedTypeLiteralInterface_1): void {// 检查是否是相邻方块const rowDiff = Math.abs(first.row - second.row);const colDiff = Math.abs(first.col - second.col);if ((rowDiff === 1 && colDiff === 0) || (rowDiff === 0 && colDiff === 1)) {// 创建新数组进行交换const newGridData = this.gridData.map(row => [...row]);// 直接交换方块值const temp = newGridData[first.row][first.col];newGridData[first.row][first.col] = newGridData[second.row][second.col];newGridData[second.row][second.col] = temp;// 检查是否有可消除的方块const hasMatches = this.checkForMatches(newGridData);if (hasMatches) {// 如果有可消除的方块,更新数据this.gridData = newGridData;this.eliminateMatches();} else {// 如果没有可消除的方块,撤销交换console.log('No matches after swap, reverting');}}}// 检查是否有可消除的方块private checkForMatches(grid: number[][]): boolean {// 检查横向匹配for (let row = 0; row < grid.length; row++) {for (let col = 0; col < grid[row].length - 2; col++) {if (grid[row][col] === grid[row][col + 1] && grid[row][col] === grid[row][col + 2]) {return true;}}}// 检查纵向匹配for (let row = 0; row < grid.length - 2; row++) {for (let col = 0; col < grid[row].length; col++) {if (grid[row][col] === grid[row + 1][col] && grid[row][col] === grid[row + 2][col]) {return true;}}}return false;}build() {Column() {// 控制面板// 游戏规则说明if (this.showRules) {Column() {Image($r('app.media.ic_game_icon')).width(80).height(80).margin({ bottom: 15 })Text('游戏规则').fontSize(32).fontWeight(FontWeight.Bold).fontFamily('Comic Sans MS').margin({ bottom: 20 })Text('1. 点击任意两个相邻方块交换位置\n2. 三个或以上相同方块连成一线即可消除\n3. 消除更多方块获得更高分数\n4. 继续游戏直到无法再消除').fontSize(20).fontFamily('Comic Sans MS').textAlign(TextAlign.Center).margin({ bottom: 30 })Button('开始游戏').fontFamily('Comic Sans MS').fontSize(24).width(180).height(50).backgroundColor('#FFD700').onClick(() => {this.showRules = false;})}.width('90%').padding({ top: 30, bottom: 35, left: 25, right: 25 }).backgroundColor('rgba(255, 255, 255, 0.9)').borderRadius(15).shadow({ color: '#E0E0E0', radius: 10 }) // 添加柔和阴影效果} else {Row() {Text('无尽模式').fontSize(16).fontFamily('Comic Sans MS').fontWeight(FontWeight.Bold).margin({ left: 10 })}.width('90%').justifyContent(FlexAlign.Start).margin({ bottom: 10 })// 构建游戏界面ForEach(this.gridData, (row: number[], rowIndex: number) => {Row() {ForEach(row, (item: number, colIndex: number) => {// 方块组件Column() {Image(this.getBlockImage(item)).width(this.blockSize).height(this.blockSize).scale({x: this.eliminationAnimation[rowIndex][colIndex] ? 1.2 : 1,y: this.eliminationAnimation[rowIndex][colIndex] ? 1.2 : 1}).animation({duration: 300,curve: Curve.EaseOut})// 添加下落动画效果.translate({y: this.dropAnimation[rowIndex][colIndex] ? -10 : 0}).animation({duration: 500,curve: Curve.Linear})}.onClick(() => {if (this.selectedBlock === null) {// 第一次选择方块this.selectedBlock = { row: rowIndex, col: colIndex };console.log(`Selected block: ${rowIndex}, ${colIndex}`)} else {// 第二次选择,尝试交换this.swapBlocks(this.selectedBlock, { row: rowIndex, col: colIndex });// 重置选中方块状态this.selectedBlock = null;console.log('Cleared selected block state');}}).border({width: this.selectedBlock && this.selectedBlock.row === rowIndex &&this.selectedBlock.col === colIndex ? 3 : 1,color: this.selectedBlock && this.selectedBlock.row === rowIndex &&this.selectedBlock.col === colIndex ? '#FF0000' : '#AAAAAA'}) // 高亮显示当前选中的方块})}.margin({ bottom: 15 }).width('100%').justifyContent(FlexAlign.SpaceEvenly)})}}.width('100%').height('100%').backgroundColor('#FFF8DC') // 卡通风格背景色.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)}// 根据方块数值获取对应的图片资源private getBlockImage(value: number): Resource {switch (value) {case 1:return $r('app.media.block_1')case 2:return $r('app.media.block_2')case 3:return $r('app.media.block_3')default:return $r('app.media.background')}}
}