鸿蒙 ArkUI 实现 2048 小游戏

2048 是一款经典的益智游戏,玩家通过滑动屏幕合并相同数字的方块,最终目标是合成数字 2048。本文基于鸿蒙 ArkUI 框架,详细解析其实现过程,帮助开发者理解如何利用声明式 UI 和状态管理构建此类游戏。

一、核心数据结构与状态管理

1. 游戏网格与得分

游戏的核心是一个 4x4 的二维数组,用于存储每个格子的数字。通过 @State 装饰器管理网格状态,确保数据变化时 UI 自动刷新:

@State grid: number[][] = Array(4).fill(0).map(() => Array(4).fill(0));
@State score: number = 0; // 当前得分
@State bestScore: number = 0; // 历史最高分
2. 初始化游戏

initGame 方法负责重置网格、添加初始方块并重置得分。通过 addNewTile 在随机空位生成新方块(90% 概率生成 2,10% 概率生成 4):

initGame() {this.grid = this.grid.map(() => Array(4).fill(0));this.addNewTile();this.addNewTile();this.score = 0;
}

二、滑动逻辑与合并算法

1. 方向处理与矩阵旋转

游戏支持 上下左右四个方向的滑动。为简化代码逻辑,通过矩阵旋转将不同方向的移动统一转换为 左移操作

  • 左移:直接处理每一行。
  • 右移:将行反转后左移,再反转回来。
  • 上移/下移:旋转矩阵为行,处理后恢复为列。
// 矩阵旋转辅助方法
const rotate = (matrix: number[][]) => {return matrix[0].map((_, i) => matrix.map(row => row[i]).reverse());
};
2. 单行合并逻辑

每行处理分为三步:

  1. 移除空格:过滤出非零数字。
  2. 合并相同数字:相邻相同数字合并,并累加得分。
  3. 补齐长度:填充零至长度为 4。
const moveRow = (row: number[]) => {let newRow = row.filter(cell => cell !== 0);for (let i = 0; i < newRow.length - 1; i++) {if (newRow[i] === newRow[i + 1]) {newRow[i] *= 2;this.score += newRow[i]; // 得分累加newRow.splice(i + 1, 1);}}return [...newRow, ...Array(4 - newRow.length).fill(0)];
};

三、游戏结束判断

游戏结束的条件是 网格填满且无相邻可合并的方块。通过以下步骤检测:

  1. 检查是否有空格:存在空格则游戏未结束。
  2. 横向检测:遍历每一行,检查是否有相邻相同数字。
  3. 纵向检测:遍历每一列,检查是否有相邻相同数字。
isGameOver(): boolean {if (this.grid.some(row => row.includes(0))) return false;// 横向和纵向检测逻辑// ...return true;
}

四、UI 实现与交互设计

1. 网格渲染

使用 Grid 组件动态生成 4x4 网格,每个 GridItem 根据数字值显示不同背景色和文字颜色:

Grid() {ForEach(this.grid, (row: number[], i) => {ForEach(row, (value: number, j) => {GridItem() {Text(value ? `${value}` : '').backgroundColor(this.getTileColor(value)).fontColor(this.getTextColor(value));}})})
}

2. 触摸事件处理
通过 onTouch 监听滑动事件,计算起始和结束坐标的差值,判断滑动方向:

onTouch((event) => {if (event.type === TouchType.Down) {this.startX = event.touches[0].x;this.startY = event.touches[0].y;} else if (event.type === TouchType.Up) {const deltaX = event.touches[0].x - this.startX;const deltaY = event.touches[0].y - this.startY;// 判断方向并调用 move 方法}
});

五、本地存储与动画效果

1. 最高分持久化

使用 PreferencesUtil 存储和读取最高分,确保数据在应用重启后保留:

aboutToAppear() {this.bestScore = PreferencesUtil.getNumberSync("bestScore");
}// 更新最高分
if (this.score > this.bestScore) {PreferencesUtil.putSync('bestScore', this.score);
}
2. 动画与视觉效果

每个方块的文字变化添加了 150ms 的渐变动画,提升用户体验:

Text(value ? `${value}` : '').animation({ duration: 150, curve: Curve.EaseOut });

六、完整代码

import { HashMap } from '@kit.ArkTS'
import { AppUtil, PreferencesUtil, ToastUtil } from '@pura/harmony-utils'// index.ets
@Entry
@Component
struct Game2048 {@State grid: number[][] = Array(4).fill(0).map(() => Array(4).fill(0)) // 4x4游戏网格@State score: number = 0 // 当前得分@State bestScore: number = 0 // 历史最高分private startX: number = 0 // 触摸起始X坐标private startY: number = 0 // 触摸起始Y坐标// 生命周期方法:页面即将显示时触发aboutToAppear() {this.initGame()this.bestScore = PreferencesUtil.getNumberSync("bestScore") // 读取本地存储的最高分}// 初始化游戏initGame() {this.grid = this.grid.map(() => Array(4).fill(0)) // 重置网格this.addNewTile() // 添加两个新方块this.addNewTile() // 重置当前得分this.score = 0}addNewTile() {const emptyCells: [number, number][] = [] // 收集空单元格坐标this.grid.forEach((row, i) => {row.forEach((cell, j) => {if (cell === 0) {emptyCells.push([i, j])}})})if (emptyCells.length > 0) {let n = Math.floor(Math.random() * emptyCells.length) // 随机选择空单元格const i = emptyCells[n][0]const j = emptyCells[n][1]this.grid[i][j] = Math.random() < 0.9 ? 2 : 4 // 90%概率生成2,10%概率生成4}}// 处理移动逻辑move(direction: 'left' | 'right' | 'up' | 'down') {let newGrid = this.grid.map(row => [...row]) // 创建网格副本let moved = false // 移动标志位// 矩阵旋转辅助方法const rotate = (matrix: number[][]) => {return matrix[0].map((_, i) => matrix.map(row => row[i]).reverse())}const rotateReverse = (matrix: number[][]) => {return matrix[0].map((_, i) => matrix.map(row => row[row.length - 1 - i]))}// 处理单行移动和合并const moveRow = (row: number[]) => {let newRow = row.filter(cell => cell !== 0) // 移除空格for (let i = 0; i < newRow.length - 1; i++) {if (newRow[i] === newRow[i + 1]) { // 合并相同数字newRow[i] *= 2this.score += newRow[i] // 更新得分newRow.splice(i + 1, 1) // 移除合并后的元素}}// 补齐长度while (newRow.length < 4) {newRow.push(0)}return newRow}// 根据方向处理移动switch (direction) {case 'left':newGrid.forEach((row, i) => newGrid[i] = moveRow(row))breakcase 'right':newGrid.forEach((row, i) => newGrid[i] = moveRow(row.reverse()).reverse())breakcase 'up':let rotatedDown = rotate(newGrid)rotatedDown.forEach((row, i) => rotatedDown[i] = moveRow(row.reverse()).reverse())newGrid = rotateReverse(rotatedDown)breakcase 'down':let rotatedUp = rotate(newGrid)rotatedUp.forEach((row, i) => rotatedUp[i] = moveRow(row))newGrid = rotateReverse(rotatedUp)break}moved = JSON.stringify(newGrid) !== JSON.stringify(this.grid) // 判断是否发生移动this.grid = newGridif (moved) {this.addNewTile() // 移动后添加新方块if (this.score > this.bestScore) { // 更新最高分this.bestScore = this.scorePreferencesUtil.putSync('bestScore', this.bestScore) //保存最高分}}if (this.isGameOver()) { // 游戏结束检测ToastUtil.showToast('游戏结束!')}}// 游戏结束判断isGameOver(): boolean {// 检查空格子if (this.grid.some(row => row.includes(0))) {return false}// 检查横向可合并for (let i = 0; i < 4; i++) {for (let j = 0; j < 3; j++) {if (this.grid[i][j] === this.grid[i][j + 1]) {return false}}}// 检查纵向可合并for (let j = 0; j < 4; j++) {for (let i = 0; i < 3; i++) {if (this.grid[i][j] === this.grid[i + 1][j]) {return false}}}return true}build() {Column() {// 分数显示行Row() {Text(`得分: ${this.score}`).fontSize(20).margin(10)Text(`最高分: ${this.bestScore}`).fontSize(20).margin(10)Button('新游戏').onClick(() => this.initGame()).margin(10)}.margin({top:px2vp(AppUtil.getStatusBarHeight()) })// 游戏网格Grid() {ForEach(this.grid, (row: number[], i) => {ForEach(row, (value: number, j) => {GridItem() {Text(value ? `${value}` : '').textAlign(TextAlign.Center).fontSize(24).fontColor(this.getTextColor(value)).width('100%').height('100%').backgroundColor(this.getTileColor(value)).animation({duration: 150,curve: Curve.EaseOut})}.key(`${i}-${j}`)})})}.columnsTemplate('1fr 1fr 1fr 1fr')    // 4等分列.rowsTemplate('1fr 1fr 1fr 1fr')       // 4等分行.width('90%').aspectRatio(1)                        // 保持正方形.margin(10).onTouch((event) => {                  // 触摸事件处理if (event.type === TouchType.Down) {this.startX = event.touches[0].xthis.startY = event.touches[0].y} else if (event.type === TouchType.Up) {const deltaX = event.touches[0].x - this.startXconst deltaY = event.touches[0].y - this.startY// 根据滑动方向判断移动if (Math.abs(deltaX) > Math.abs(deltaY)) {deltaX > 0 ? this.move('right') : this.move('left')} else {deltaY > 0 ? this.move('down') : this.move('up')}}})}.width('100%')}// 获取方块背景色getTileColor(value: number): string {const colors = new HashMap<number, string>()colors.set(0, '#CDC1B4')colors.set(2, '#EEE4DA')colors.set(4, '#EDE0C8')colors.set(8, '#F2B179')colors.set(16, '#F59563')colors.set(32, '#F67C5F')colors.set(64, '#F65E3B')colors.set(128, '#EDCF72')colors.set(256, '#EDCF72')colors.set(512, '#EDCC61')colors.set(1024, '#EDC850')colors.set(2048, '#EDC22E')return colors.get(value) || '#CDC1B4'}// 获取文字颜色getTextColor(value: number): Color {return value > 4 ? Color.White : Color.Black}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/70984.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/70984.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Milvus高性能向量数据库与大模型结合

Milvus | 高性能向量数据库&#xff0c;为规模而构建Milvus 是一个为 GenAI 应用构建的开源向量数据库。使用 pip 安装&#xff0c;执行高速搜索&#xff0c;并扩展到数十亿个向量。https://milvus.io/zh Milvus 是什么&#xff1f; Milvus 是一种高性能、高扩展性的向量数据…

kettle插件-自定义函数-数据脱敏

平常我们在使用kettle抽取数据的时候会涉及到敏感数据邀请脱敏或者进行掩码的需求&#xff0c;今天我们使用自定义函数插件来实现这些需求。 1、将自定义函数插件&#xff08;kettle-func-plugin.zip&#xff09;解压后放到kettle的plugins目录下面&#xff0c;然后重启服务。…

LeetCode 每日一题 2025/2/24-2025/3/2

记录了初步解题思路 以及本地实现代码&#xff1b;并不一定为最优 也希望大家能一起探讨 一起进步 目录 2/24 1656. 设计有序流2/25 2502. 设计内存分配器2/26 1472. 设计浏览器历史记录2/27 2296. 设计一个文本编辑器2/28 2353. 设计食物评分系统3/1 131. 分割回文串3/2 2/24 …

C++动态与静态转换区别详解

文章目录 前言一、 类型检查的时机二、安全性三、适用场景四、代码示例对比总结 前言 在 C 中&#xff0c;dynamic_cast 和 static_cast 是两种不同的类型转换操作符&#xff0c;主要区别体现在类型检查的时机、安全性和适用场景上。以下是它们的核心区别&#xff1a; 一、 类…

探秘《矩阵之美》:解锁矩阵的无限魅力

在这个数据驱动的时代&#xff0c;矩阵作为数学中的瑰宝&#xff0c;不仅在理论研究中占据核心地位&#xff0c;更在工程技术、计算机科学、物理学、经济学等众多领域发挥着不可替代的作用。今天&#xff0c;让我们通过中科院大学耿修瑞老师&#xff08;中科院空天信息研究院研…

【MySQL】(2) 库的操作

SQL 关键字&#xff0c;大小写不敏感。 一、查询数据库 show databases; 注意加分号&#xff0c;才算一句结束。 二、创建数据库 {} 表示必选项&#xff0c;[] 表示可选项&#xff0c;| 表示任选其一。 示例&#xff1a;建议加上 if not exists 选项。 三、字符集编码和排序…

Vue3实现文件上传、下载及预览全流程详解(含完整接口调用)

文章目录 一、环境准备1.1 创建Vue3项目1.2 安装依赖1.3 配置Element Plus 二、文件上传实现2.1 基础上传组件2.2 自定义上传逻辑&#xff08;Axios实现&#xff09; 三、文件下载实现3.1 直接下载&#xff08;已知文件URL&#xff09;3.2 后端接口下载&#xff08;二进制流&am…

分布式数据存储:提升系统弹性与性能的技术之路

分布式数据存储:提升系统弹性与性能的技术之路 在当今数据爆炸式增长的时代,传统的单机存储系统已无法满足大规模、高并发、低延迟的需求。尤其是在大数据、云计算和物联网的推动下,数据存储面临着前所未有的挑战。分布式数据存储应运而生,通过将数据分布在多个物理节点上…

在编译Linux的内核镜像和模块时,必须先编译内核镜像,再编译模块,顺序不可随意调整的原因

问&#xff1a;在编译Linux的内核镜像和模块时,必须先编译内核镜像,再编译模块,顺序不可随意调整 答&#xff1a;在编译 Linux 内核和模块时&#xff0c;必须先编译内核镜像&#xff0c;再编译模块&#xff0c;顺序不可随意调整。 原因&#xff1a; 模块依赖内核的头文件和符…

免费使用 DeepSeek API 教程及资源汇总

免费使用 DeepSeek API 教程及资源汇总 一、DeepSeek API 资源汇总1.1 火山引擎1.2 百度千帆1.3 阿里百炼1.4 腾讯云 二、其他平台2.1 华为云2.2 硅基流动 三、总结 DeepSeek-R1 作为 2025 年初发布的推理大模型&#xff0c;凭借其卓越的逻辑推理能力和成本优势&#xff0c;迅速…

千峰React:案例二

完成对html文档还有css的引入&#xff0c;引入一下数据&#xff1a; import { func } from prop-types import ./购物车样式.css import axios from axios import { useImmer } from use-immer import { useEffect } from reactfunction Item() {return (<li classNameacti…

用DeepSeek生成批量删除处理 PDF第一页工具

安装依赖库 在运行程序之前&#xff0c;请确保安装所需的库&#xff1a; pip install pymupdf python-docx Python 程序代码 import os import fitz # PyMuPDF from docx import Documentdef delete_pdf_first_page(input_path, output_path):"""删除 PDF…

redis的下载和安装详解

一、下载redis安装包 进入redis官网查看当前稳定版本&#xff1a; https://redis.io/download/发现此时的稳定版本是6.2.4&#xff0c; 此时可以去这个网站下载6.2.4稳定版本的tar包。 暂时不考虑不在windows上使用redis&#xff0c;那样将无法发挥redis的性能 二、上传tar…

如何使用 Jenkins 实现 CI/CD 流水线:从零开始搭建自动化部署流程

如何使用 Jenkins 实现 CI/CD 流水线:从零开始搭建自动化部署流程 在软件开发过程中,持续集成(CI)和持续交付(CD)已经成为现代开发和运维的标准实践。随着代码的迭代越来越频繁,传统的手动部署方式不仅低效,而且容易出错。为了提高开发效率和代码质量,Jenkins作为一款…

Python基于Django的网络课程在线学习平台【附源码】

博主介绍&#xff1a;✌Java老徐、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&…

Pytorch为什么 nn.CrossEntropyLoss = LogSoftmax + nn.NLLLoss?

为什么 nn.CrossEntropyLoss LogSoftmax nn.NLLLoss&#xff1f; 在使用 PyTorch 时&#xff0c;我们经常听说 nn.CrossEntropyLoss 是 LogSoftmax 和 nn.NLLLoss 的组合。这句话听起来简单&#xff0c;但背后到底是怎么回事&#xff1f;为什么这两个分开的功能加起来就等于…

rabbitmq 延时队列

要使用 RabbitMQ Delayed Message Plugin 实现延时队列&#xff0c;首先需要确保插件已安装并启用。以下是实现延时队列的步骤和代码示例。 1. 安装 RabbitMQ Delayed Message Plugin 首先&#xff0c;确保你的 RabbitMQ 安装了 rabbitmq-delayed-message-exchange 插件。你可…

在 Vue 单文件组件(SFC)中,标签的显式关闭与隐式关闭有着重要的区别

一、显式关闭标签 1、定义&#xff1a; 所有的 HTML 标签都必须有一个对应的结束标签。 自闭合标签也必须使用 / 来关闭。 <template> <div> <p>这是一个段落</p> <img src"image.png"…

第四届大数据、区块链与经济管理国际学术会议

重要信息 官网&#xff1a;www.icbbem.com 时间&#xff1a;2025年3月14-16日 地点&#xff1a;中国-武汉 &#xff08;线上召开&#xff09; 简介 第四届大数据、区块链与经济管理国际学术会议(ICBBEM 2025)&#xff0c;将于2025年3月14-16日在中国湖北省武汉市召开。…

每日十个计算机专有名词 (7)

Metasploit 词源&#xff1a;Meta&#xff08;超越&#xff0c;超出&#xff09; exploit&#xff08;漏洞利用&#xff09; Metasploit 是一个安全测试框架&#xff0c;用来帮助安全专家&#xff08;也叫渗透测试人员&#xff09;发现和利用计算机系统中的漏洞。你可以把它想…