纯血Harmony NETX 5小游戏实践:2048(附源文件)
本文详细介绍了基于鸿蒙OS ArkUI框架开发2048游戏的全过程。重点分析了游戏的核心数据结构设计(4×4网格二维数组)、移动合并算法实现(压缩与合并逻辑)以及界面渲染优化(动态单元格样式)。文章通过代码示例展示了方向控制、积分计算和游戏状态管理等关键技术点,包括边界条件处理(游戏结束判断、胜利条件检测)。该项目充分利用鸿蒙的声明式UI特性和响应式状态管理,为开发者提供了移动应用开发的典型案例参
在移动应用开发领域,2048游戏因其简洁的规则和富有挑战性的玩法成为经典案例。本文将基于鸿蒙OS的ArkUI框架,深入解析2048游戏的实现原理,从数据结构设计到动画交互优化,带您全面了解这款益智游戏的开发全过程。
游戏核心架构与数据模型设计
2048游戏的核心是一个4×4的网格系统,每个单元格可以包含数字2的幂次或为空。在鸿蒙实现中,我们采用二维数组作为基础数据结构:
@State grid: number[][] = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
];
这种设计简洁高效,便于实现网格的遍历、合并和更新操作。游戏初始化时,通过addRandomTile()
方法在网格中随机生成两个初始方块(2或4,10%概率生成4):
addRandomTile() {
const emptyCells: GeneratedTypeLiteralInterface_1[] = [];
// 收集所有空单元格坐标
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (this.grid[i][j] === 0) {
emptyCells.push({ row: i, col: j });
}
}
}
// 处理无空位的情况
if (emptyCells.length === 0 && !this.checkMovesAvailable()) {
this.gameOver = true;
return;
}
// 随机放置新方块
if (emptyCells.length > 0) {
const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
this.grid[randomCell.row][randomCell.col] = Math.random() < 0.1 ? 4 : 2;
}
}
数据模型中还包含了分数统计score
、游戏状态gameOver
和胜利标志gameWon
,这些状态通过@State
装饰器实现响应式更新,确保UI与数据保持同步。
核心游戏逻辑:移动与合并算法
2048游戏的核心在于四个方向的移动逻辑,其本质是对网格数据的压缩与合并操作。以向左移动为例,核心算法分为两步:压缩非零元素到左侧,然后合并相邻相同元素。
moveLeft() {
let moved = false;
const newGrid = this.grid.map(row => {
const newRow = this.compressAndMerge(row);
// 检测数据是否变更
if (JSON.stringify(newRow) !== JSON.stringify(row)) {
moved = true;
}
return newRow;
});
// 仅在数据变更时更新状态
if (moved) {
this.grid = newGrid;
this.checkWinCondition();
this.addRandomTile();
}
}
compressAndMerge()
方法是移动逻辑的核心,它不仅负责将非零元素紧凑排列,还会处理相同元素的合并并计算得分:
compressAndMerge(row: number[]): number[] {
const filteredRow = row.filter(val => val !== 0);
const mergedRow: number[] = [];
let scoreIncrease = 0;
for (let i = 0; i < filteredRow.length; i++) {
// 合并相邻相同元素
if (i < filteredRow.length - 1 && filteredRow[i] === filteredRow[i + 1]) {
mergedRow.push(filteredRow[i] * 2);
scoreIncrease += filteredRow[i] * 2;
// 检查是否达成2048胜利条件
if (filteredRow[i] * 2 === 2048) {
this.gameWon = true;
}
i++; // 跳过已合并元素
} else {
mergedRow.push(filteredRow[i]);
}
}
// 补零操作,保持4格长度
while (mergedRow.length < 4) {
mergedRow.push(0);
}
// 更新积分
if (scoreIncrease > 0) {
this.score += scoreIncrease;
}
return mergedRow;
}
向右、向上、向下的移动逻辑基于向左移动算法演变而来,通过行列转换和数组反转实现方向适配。例如向右移动时,先将行数据反转,应用向左移动逻辑后再反转回来:
moveRight() {
let moved = false;
const newGrid = this.grid.map(row => {
const reversedRow = [...row].reverse();
const mergedRow = this.compressAndMerge(reversedRow);
const newRow = mergedRow.reverse();
if (JSON.stringify(newRow) !== JSON.stringify(row)) {
moved = true;
}
return newRow;
});
// 状态更新逻辑与向左移动相同
}
界面渲染与交互体验优化
鸿蒙ArkUI的声明式UI特性让2048游戏的界面实现变得简洁直观。我们通过ForEach
循环动态渲染4×4网格,每个单元格的样式根据其值动态变化:
ForEach(this.grid, (row: number[], rowIndex: number) => {
Row({ space: 15 }) {
ForEach(row, (cell: number, colIndex: number) => {
Column() {
CellComponent({ value: cell })
}
.width(60)
.height(60)
.borderRadius(20)
.backgroundColor(this.getCellBackgroundColor(cell))
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)
})
getCellBackgroundColor()
方法根据单元格值返回对应的背景色,实现2048游戏经典的视觉层次感:
getCellBackgroundColor(value: number): ResourceColor {
switch (value) {
case 0: return '#cdc1b4';
case 2: return '#eee4da';
case 4: return '#ede0c8';
case 8: return '#f2b179';
// 省略中间值...
case 2048: return '#edc22e';
default: return '#3c3a32';
}
}
交互控制采用四个方向按钮实现,点击事件绑定对应的移动逻辑:
Row({ space: 20 }) {
Button('←')
.width(60)
.height(60)
.fontColor('#ffffff')
.onClick(() => this.moveLeft())
.backgroundColor('#776e65')
.borderRadius(10);
// 其他方向按钮类似...
}
游戏状态管理与边界条件处理
2048游戏的难点在于边界条件的处理,包括:
- 游戏结束判断:当网格已满且没有可合并的元素时,游戏结束:
checkMovesAvailable(): boolean {
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
// 检查右侧和下侧是否有可合并元素
if (j < 3 && this.grid[i][j] === this.grid[i][j + 1]) return true;
if (i < 3 && this.grid[i][j] === this.grid[i + 1][j]) return true;
}
}
return false;
}
- 胜利条件判断:当网格中出现2048时,游戏胜利:
checkWinCondition() {
if (!this.gameWon && this.grid.some(row => row.includes(2048))) {
this.gameWon = true;
}
}
- 积分系统:每次合并操作会根据合并后的数值增加积分,例如合并两个1024得到2048时,积分增加2048。
附:源文件
interface GeneratedTypeLiteralInterface_1 {
row: number;
col: number;
}
@Component
export struct play_7 {
// 游戏数据矩阵
@State grid: number[][] = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
];
// 积分属性
@State score: number = 0;
// 游戏状态
@State gameOver: boolean = false;
@State gameWon: boolean = false;
// 初始化游戏
aboutToAppear() {
this.addRandomTile();
this.addRandomTile();
this.score = 0; // 初始化积分为0
this.gameOver = false;
this.gameWon = false;
}
// 添加随机方块并更新积分
addRandomTile() {
const emptyCells: GeneratedTypeLiteralInterface_1[] = [];
// 找出所有空位
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (this.grid[i][j] === 0) {
emptyCells.push({ row: i, col: j });
}
}
}
// 如果没有空位则检查游戏结束
if (emptyCells.length === 0 && !this.checkMovesAvailable()) {
this.gameOver = true;
return;
}
// 随机选择一个位置放置2或4(10%概率)
if (emptyCells.length > 0) {
const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
this.grid[randomCell.row][randomCell.col] = Math.random() < 0.1 ? 4 : 2;
}
}
// 检查是否有可用移动
checkMovesAvailable(): boolean {
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
// 检查右侧是否有可合并
if (j < 3 && this.grid[i][j] === this.grid[i][j + 1]) return true;
// 检查下方是否有可合并
if (i < 3 && this.grid[i][j] === this.grid[i + 1][j]) return true;
}
}
return false;
}
// 向左移动逻辑并计算积分
moveLeft() {
let moved = false;
const newGrid = this.grid.map(row => {
const newRow = this.compressAndMerge(row);
if (JSON.stringify(newRow) !== JSON.stringify(row)) {
moved = true;
}
return newRow;
});
if (moved) {
this.grid = newGrid;
this.checkWinCondition();
this.addRandomTile();
}
}
// 向右移动逻辑并计算积分
moveRight() {
let moved = false;
const newGrid = this.grid.map(row => {
const reversedRow = [...row].reverse();
const mergedRow = this.compressAndMerge(reversedRow);
const newRow = mergedRow.reverse();
if (JSON.stringify(newRow) !== JSON.stringify(row)) {
moved = true;
}
return newRow;
});
if (moved) {
this.grid = newGrid;
this.checkWinCondition();
this.addRandomTile();
}
}
// 向上移动逻辑并计算积分
moveUp() {
let moved = false;
const newGrid = [...this.grid];
for (let col = 0; col < 4; col++) {
const column = [newGrid[0][col], newGrid[1][col], newGrid[2][col], newGrid[3][col]];
const newColumn = this.compressAndMerge(column);
for (let row = 0; row < 4; row++) {
if (newGrid[row][col] !== newColumn[row]) {
moved = true;
}
newGrid[row][col] = newColumn[row];
}
}
if (moved) {
this.grid = newGrid;
this.checkWinCondition();
this.addRandomTile();
}
}
// 向下移动逻辑并计算积分
moveDown() {
let moved = false;
const newGrid = [...this.grid];
for (let col = 0; col < 4; col++) {
const column = [newGrid[3][col], newGrid[2][col], newGrid[1][col], newGrid[0][col]];
const newColumn = this.compressAndMerge(column);
for (let row = 0; row < 4; row++) {
if (newGrid[3 - row][col] !== newColumn[row]) {
moved = true;
}
newGrid[3 - row][col] = newColumn[row];
}
}
if (moved) {
this.grid = newGrid;
this.checkWinCondition();
this.addRandomTile();
}
}
// 压缩并合并单元格,增加积分计算
compressAndMerge(row: number[]): number[] {
const filteredRow = row.filter(val => val !== 0);
const mergedRow: number[] = [];
let scoreIncrease = 0;
for (let i = 0; i < filteredRow.length; i++) {
if (i < filteredRow.length - 1 && filteredRow[i] === filteredRow[i + 1]) {
mergedRow.push(filteredRow[i] * 2);
scoreIncrease += filteredRow[i] * 2;
i++;
// 检查是否达成2048胜利条件
if (filteredRow[i] * 2 === 2048) {
this.gameWon = true;
}
} else {
mergedRow.push(filteredRow[i]);
}
}
// 补零
while (mergedRow.length < 4) {
mergedRow.push(0);
}
// 更新积分
if (scoreIncrease > 0) {
this.score += scoreIncrease;
}
return mergedRow;
}
// 检查胜利条件
checkWinCondition() {
if (!this.gameWon && this.grid.some(row => row.includes(2048))) {
this.gameWon = true;
}
}
build() {
Column({ space: 20 }) {
// 标题区和积分显示
Row() {
Text('2048')
.fontSize(36)
.fontWeight(FontWeight.Bold)
.textAlign(TextAlign.Center)
.width('70%')
.padding({ top: 10, bottom: 10 })
.borderRadius(15)
.backgroundColor('#fdf6ec')
.height(60)
// 积分显示
Text(`分数: ${this.score}`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.textAlign(TextAlign.Center)
.width('30%')
.padding({ top: 10, bottom: 10 })
.borderRadius(15)
.backgroundColor('#f1eeee')
.height(60)
.shadow({ color: '#ccc', radius: 6 })
}
.width('90%')
// 显示游戏网格
Column({ space: 15 }) {
ForEach(this.grid, (row: number[], rowIndex: number) => {
Row({ space: 15 }) {
ForEach(row, (cell: number, colIndex: number) => {
// 每个单元格的显示
Column() {
CellComponent({ value: cell })
}
.width(60)
.height(60)
.borderRadius(20)
.backgroundColor(this.getCellBackgroundColor(cell))
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.shadow({ color: '#a394894d', radius: 8, offsetX: 2, offsetY: 2 })
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)
})
}
.width('90%')
.padding(10)
.borderRadius(20)
.backgroundColor('#bbada0')
.shadow({ color: '#888', radius: 10 })
// 添加控制按钮
Row({ space: 20 }) {
Button('←')
.width(60)
.height(60)
.fontColor('#ffffff')
.onClick(() => {
this.moveLeft()
})
.backgroundColor('#776e65')
.borderRadius(10)
Button('↑')
.width(60)
.height(60)
.fontColor('#ffffff')
.onClick(() => {
this.moveUp()
})
.backgroundColor('#776e65')
.borderRadius(10)
Button('↓')
.width(60)
.height(60)
.fontColor('#ffffff')
.onClick(() => {
this.moveDown()
})
.backgroundColor('#776e65')
.borderRadius(10)
Button('→')
.width(60)
.height(60)
.fontColor('#ffffff')
.onClick(() => {
this.moveRight()
})
.backgroundColor('#776e65')
.borderRadius(10)
}
.width('90%')
.justifyContent(FlexAlign.Center)
// 游戏状态提示
if (this.gameOver) {
Text('Game Over!')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#ffffff')
.backgroundColor('#bbada0')
.padding({ left: 20, right: 20, top: 10, bottom: 10 })
.borderRadius(10)
}
if (this.gameWon) {
Text('Congratulations!\nYou reached 2048!')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#ffffff')
.backgroundColor('#edc22e')
.padding({ left: 20, right: 20, top: 10, bottom: 10 })
.borderRadius(10)
.textAlign(TextAlign.Center)
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Start)
.alignItems(HorizontalAlign.Center)
.padding({ top: 20, bottom: 20 })
}
// 获取单元格背景颜色
getCellBackgroundColor(value: number): ResourceColor
{
switch (value) {
case 0:
return '#cdc1b4'
case 2:
return '#eee4da'
case 4:
return '#ede0c8'
case 8:
return '#f2b179'
case 16:
return '#f59563'
case 32:
return '#f67c5f'
case 64:
return '#f65e3b'
case 128:
return '#edcf72'
case 256:
return '#edcc61'
case 512:
return '#edc850'
case 1024:
return '#edc53f'
case 2048:
return '#edc22e'
default:
return '#3c3a32'
}
}
}
@Component
struct CellComponent {
@Prop value: number
build() {
Text(this.value === 0 ? '' : this.value.toString())
.fontSize(28)
.fontColor('#776e65')
.fontWeight(FontWeight.Bold)
.fontFamily( 'cursive')
}
}
更多推荐
所有评论(0)