
在移动游戏领域,三消类游戏凭借其简单易上手的规则和充满策略性的玩法,一直占据着重要地位。本文将以鸿蒙系统的ArkTS语言为基础,深入解析一款三消游戏的完整实现过程,从核心消除算法到动画交互效果,带您领略鸿蒙应用开发的魅力。
游戏架构与核心数据结构设计
三消游戏的核心在于网格数据的管理与状态变化,在该实现中,我们通过精心设计的数据结构来支撑游戏逻辑:
typescript
// 核心游戏状态定义
@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
,它们通过布尔值标记每个方块是否需要播放消除或下落动画,实现了视觉效果与逻辑数据的解耦。
核心消除算法的实现与优化
三消游戏的灵魂在于消除逻辑的实现,该算法需要高效地检测可消除组合并处理连锁反应:
ini
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
方法来高效判断是否存在可消除方块,避免了不必要的后续处理。
连锁消除与下落逻辑的实现
消除后的方块下落与新方块生成是三消游戏的关键环节,该实现采用了高效的列优先处理策略:
ini
// 下落逻辑核心部分
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可以轻松实现流畅的交互效果:
kotlin
// 方块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的解耦
附:源文件
ini
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')
}
}
}