鸿蒙开发案例:推箱子
推箱子游戏(Sokoban)的实现。游戏由多个单元格组成,每个单元格可以是透明的、墙或可移动的区域。游戏使用Cell类定义单元格的状态,如类型(透明、墙、可移动区域)、圆角大小及坐标偏移。而MyPosition类则用于表示位置信息,并提供设置位置的方法。
游戏主体结构Sokoban定义了游戏的基本元素,包括网格单元格的状态、胜利位置、箱子的位置以及玩家的位置等,并提供了初始化游戏状态的方法。游戏中还包含有动画效果,当玩家尝试移动时,会检查目标位置是否允许移动,并根据情况决定是否需要移动箱子。此外,游戏支持触摸输入,并在完成一次移动后检查是否所有箱子都在目标位置上,如果是,则游戏胜利,并显示一个对话框展示游戏用时。
【算法分析】
1. 移动玩家和箱子算法分析:
算法思路:根据玩家的滑动方向,计算新的位置坐标,然后检查新位置的合法性,包括是否超出边界、是否是墙等情况。如果新位置是箱子,则需要进一步判断箱子后面的位置是否为空,以确定是否可以推动箱子。
实现逻辑:通过定义方向对象和计算新位置坐标的方式,简化了移动操作的逻辑。在移动过程中,需要考虑动画效果的控制,以提升用户体验。
movePlayer(direction: string) {const directions: object = Object({'right': Object({ dx: 0, dy: 1}),'left': Object({ dx:0 , dy:-1 }),'down': Object({ dx: 1, dy: 0 }),'up': Object({ dx: -1, dy: 0 })});const dx: number = directions[direction]['dx']; //{ dx, dy }const dy: number = directions[direction]['dy']; //{ dx, dy }const newX: number = this.playerPosition.x + dx;const newY: number = this.playerPosition.y + dy;// 检查新位置是否合法// 箱子移动逻辑...// 动画效果控制...
}
2. 胜利条件判断算法分析:
算法思路:遍历所有箱子的位置,检查每个箱子是否在一个胜利位置上,如果所有箱子都在胜利位置上,则判定游戏胜利。
实现逻辑:通过嵌套循环和数组方法,实现了对胜利条件的判断。这种算法适合用于检查游戏胜利条件是否满足的场景。
isVictoryConditionMet(): boolean {return this.cratePositions.every(crate => {return this.victoryPositions.some(victory => crate.x === victory.x && crate.y === victory.y);});
}
3. 动画控制算法分析:
算法思路:利用动画函数实现移动过程中的动画效果,包括移动过程的持续时间和结束后的处理逻辑。
实现逻辑:通过嵌套调用动画函数,实现了移动过程中的动画效果控制。这种方式可以使移动过程更加流畅和生动。
animateToImmediately({duration: 150,onFinish: () => {animateToImmediately({duration: 0,onFinish: () => {// 动画结束后的处理...}}, () => {// 动画过程中的处理...});}
}, () => {// 动画效果控制...
});
4. 触摸操作和手势识别算法分析:
算法思路:监听触摸事件和手势事件,识别玩家的滑动方向,然后调用相应的移动函数处理玩家和箱子的移动。
实现逻辑:通过手势识别和事件监听,实现了玩家在屏幕上滑动操作的识别和响应。这种方式可以使玩家通过触摸操作来控制游戏的进行。
gesture(SwipeGesture({ direction: SwipeDirection.All }).onAction((_event: GestureEvent) => {// 手势识别和处理逻辑...})
)
【完整代码】
import { promptAction } from '@kit.ArkUI' // 导入ArkUI工具包中的提示操作模块
@ObservedV2 // 观察者模式装饰器
class Cell { // 定义游戏中的单元格类@Trace // 跟踪装饰器,标记属性以被跟踪type: number = 0; // 单元格类型,0:透明,1:墙,2:可移动区域@Trace topLeft: number = 0; // 左上角圆角大小@Trace topRight: number = 0; // 右上角圆角大小@Trace bottomLeft: number = 0; // 左下角圆角大小@Trace bottomRight: number = 0; // 右下角圆角大小@Trace x: number = 0; // 单元格的X坐标偏移量@Trace y: number = 0; // 单元格的Y坐标偏移量constructor(cellType: number) { // 构造函数this.type = cellType; // 初始化单元格类型}
}
@ObservedV2 // 观察者模式装饰器
class MyPosition { // 定义位置类@Trace // 跟踪装饰器,标记属性以被跟踪x: number = 0; // X坐标@Trace y: number = 0; // Y坐标setPosition(x: number, y: number) { // 设置位置的方法this.x = x; // 更新X坐标this.y = y; // 更新Y坐标}
}
@Entry // 入口装饰器
@Component // 组件装饰器
struct Sokoban { // 定义游戏主结构cellWidth: number = 100; // 单元格宽度@State grid: Cell[][] = [ // 游戏网格状态[new Cell(0), new Cell(1), new Cell(1), new Cell(1), new Cell(1), new Cell(1)],[new Cell(1), new Cell(1), new Cell(2), new Cell(2), new Cell(2), new Cell(1)],[new Cell(1), new Cell(2), new Cell(2), new Cell(2), new Cell(1), new Cell(1)],[new Cell(1), new Cell(2), new Cell(2), new Cell(2), new Cell(2), new Cell(1)],[new Cell(1), new Cell(1), new Cell(2), new Cell(2), new Cell(2), new Cell(1)],[new Cell(0), new Cell(1), new Cell(1), new Cell(1), new Cell(1), new Cell(1)],];@State victoryPositions: MyPosition[] = [new MyPosition(), new MyPosition()]; // 胜利位置数组@State cratePositions: MyPosition[] = [new MyPosition(), new MyPosition()]; // 箱子位置数组playerPosition: MyPosition = new MyPosition(); // 玩家位置@State screenStartX: number = 0; // 触摸开始时的屏幕X坐标@State screenStartY: number = 0; // 触摸开始时的屏幕Y坐标@State lastScreenX: number = 0; // 触摸结束时的屏幕X坐标@State lastScreenY: number = 0; // 触摸结束时的屏幕Y坐标@State startTime: number = 0; // 游戏开始时间isAnimationRunning: boolean = false // 动画是否正在运行aboutToAppear(): void { // 游戏加载前的准备工作// 初始化某些单元格的圆角大小...this.grid[0][1].topLeft = 25;this.grid[0][5].topRight = 25;this.grid[1][0].topLeft = 25;this.grid[4][0].bottomLeft = 25;this.grid[5][1].bottomLeft = 25;this.grid[5][5].bottomRight = 25;this.grid[1][1].bottomRight = 10;this.grid[4][1].topRight = 10;this.grid[2][4].topLeft = 10;this.grid[2][4].bottomLeft = 10;this.initializeGame(); // 初始化游戏}initializeGame() { // 初始化游戏状态this.startTime = Date.now(); // 设置游戏开始时间为当前时间// 设置胜利位置和箱子位置...this.startTime = Date.now(); // 设置游戏开始时间为当前时间this.victoryPositions[0].setPosition(1, 3);this.victoryPositions[1].setPosition(1, 4);this.cratePositions[0].setPosition(2, 2);this.cratePositions[1].setPosition(2, 3);this.playerPosition.setPosition(1, 2);}isVictoryPositionVisible(x: number, y: number): boolean { // 判断位置是否为胜利位置return this.victoryPositions.some(position => position.x === x && position.y === y); // 返回是否有胜利位置与给定位置匹配}isCratePositionVisible(x: number, y: number): boolean { // 判断位置是否为箱子位置return this.cratePositions.some(position => position.x === x && position.y === y); // 返回是否有箱子位置与给定位置匹配}isPlayerPositionVisible(x: number, y: number): boolean { // 判断位置是否为玩家位置return this.playerPosition.x === x && this.playerPosition.y === y; // 返回玩家位置是否与给定位置相同}movePlayer(direction: string) {const directions: object = Object({'right': Object({ dx: 0, dy: 1}),'left': Object({ dx:0 , dy:-1 }),'down': Object({ dx: 1, dy: 0 }),'up': Object({ dx: -1, dy: 0 })});const dx: number = directions[direction]['dx']; //{ dx, dy }const dy: number = directions[direction]['dy']; //{ dx, dy }const newX: number = this.playerPosition.x + dx;const newY: number = this.playerPosition.y + dy;const targetCell = this.grid[newX][newY];// 检查新位置是否超出边界if (!targetCell) {return;}// 如果新位置是墙,则不能移动if (targetCell.type === 1) {return;}let crateIndex = -1;if (this.isCratePositionVisible(newX, newY)) {const crateBehindCell = this.grid[newX + dx][newY + dy];if (!crateBehindCell || crateBehindCell.type !== 2) {return;}crateIndex = this.cratePositions.findIndex(crate => crate.x === newX && crate.y === newY);if (crateIndex === -1 || this.isCratePositionVisible(newX + dx, newY + dy)) {return;}}if (this.isAnimationRunning) {return}this.isAnimationRunning = trueanimateToImmediately({duration: 150,onFinish: () => {animateToImmediately({duration: 0,onFinish: () => {this.isAnimationRunning = false}}, () => {if (crateIndex !== -1) {this.grid[this.cratePositions[crateIndex].x][this.cratePositions[crateIndex].y].x = 0;this.grid[this.cratePositions[crateIndex].x][this.cratePositions[crateIndex].y].y = 0;this.cratePositions[crateIndex].x += dx;this.cratePositions[crateIndex].y += dy;}this.grid[this.playerPosition.x][this.playerPosition.y].x = 0this.grid[this.playerPosition.x][this.playerPosition.y].y = 0this.playerPosition.setPosition(newX, newY);// 检查是否获胜const isAllCrateOnTarget = this.cratePositions.every(crate => {return this.victoryPositions.some(victory => crate.x === victory.x && crate.y === victory.y);});if (isAllCrateOnTarget) {console.log("恭喜你,你赢了!");// 可以在这里添加胜利处理逻辑promptAction.showDialog({// 显示对话框title: '游戏胜利!', // 对话框标题message: '恭喜你,用时:' + ((Date.now() - this.startTime) / 1000).toFixed(3) + '秒', // 对话框消息buttons: [{ text: '重新开始', color: '#ffa500' }] // 对话框按钮}).then(() => { // 对话框关闭后执行this.initializeGame(); // 重新开始游戏});}})}}, () => {this.grid[this.playerPosition.x][this.playerPosition.y].x = dy * this.cellWidth;this.grid[this.playerPosition.x][this.playerPosition.y].y = dx * this.cellWidth;if (crateIndex !== -1) {this.grid[this.cratePositions[crateIndex].x][this.cratePositions[crateIndex].y].x = dy * this.cellWidth;this.grid[this.cratePositions[crateIndex].x][this.cratePositions[crateIndex].y].y = dx * this.cellWidth;}console.info(`dx:${dx},dy:${dy}`)})}build() {Column({ space: 20 }) {//游戏区Stack() {//非零区加瓷砖Column() {ForEach(this.grid, (row: [], rowIndex: number) => {Row() {ForEach(row, (item: Cell, colIndex: number) => {Stack() {Text().width(`${this.cellWidth}lpx`).height(`${this.cellWidth}lpx`).backgroundColor(item.type == 0 ? Color.Transparent :((rowIndex + colIndex) % 2 == 0 ? "#cfb381" : "#e1ca9f")).borderRadius({topLeft: item.topLeft > 10 ? item.topLeft : 0,topRight: item.topRight > 10 ? item.topRight : 0,bottomLeft: item.bottomLeft > 10 ? item.bottomLeft : 0,bottomRight: item.bottomRight > 10 ? item.bottomRight : 0})//如果和是胜利坐标,显示叉号Stack() {Text().width(`${this.cellWidth / 2}lpx`).height(`${this.cellWidth / 8}lpx`).backgroundColor(Color.White)Text().width(`${this.cellWidth / 8}lpx`).height(`${this.cellWidth / 2}lpx`).backgroundColor(Color.White)}.rotate({ angle: 45 }).visibility(this.isVictoryPositionVisible(rowIndex, colIndex) ? Visibility.Visible : Visibility.None)}})}})}Column() {ForEach(this.grid, (row: [], rowIndex: number) => {Row() {ForEach(row, (item: Cell, colIndex: number) => {//是否显示箱子Stack() {Text().width(`${this.cellWidth}lpx`).height(`${this.cellWidth}lpx`).backgroundColor(item.type == 1 ? "#412c0f" : Color.Transparent).borderRadius({topLeft: item.topLeft,topRight: item.topRight,bottomLeft: item.bottomLeft,bottomRight: item.bottomRight})Text('箱').fontColor(Color.White).textAlign(TextAlign.Center).fontSize(`${this.cellWidth / 2}lpx`).width(`${this.cellWidth - 5}lpx`).height(`${this.cellWidth - 5}lpx`).backgroundColor("#cb8321")//#995d12 #cb8321.borderRadius(10).visibility(this.isCratePositionVisible(rowIndex, colIndex) ? Visibility.Visible : Visibility.None)Text('我').fontColor(Color.White).textAlign(TextAlign.Center).fontSize(`${this.cellWidth / 2}lpx`).width(`${this.cellWidth - 5}lpx`).height(`${this.cellWidth - 5}lpx`).backgroundColor("#007dfe")//#995d12 #cb8321.borderRadius(10).visibility(this.isPlayerPositionVisible(rowIndex, colIndex) ? Visibility.Visible : Visibility.None)}.width(`${this.cellWidth}lpx`).height(`${this.cellWidth}lpx`).translate({ x: `${item.x}lpx`, y: `${item.y}lpx` })})}})}}Button('重新开始').clickEffect({ level: ClickEffectLevel.MIDDLE }).onClick(() => {this.initializeGame();});}.width('100%').height('100%').backgroundColor("#fdb300").padding({ top: 20 }).onTouch((e) => {if (e.type === TouchType.Down && e.touches.length > 0) { // 触摸开始,记录初始位置this.screenStartX = e.touches[0].x;this.screenStartY = e.touches[0].y;} else if (e.type === TouchType.Up && e.changedTouches.length > 0) { // 当手指抬起时,更新最后的位置this.lastScreenX = e.changedTouches[0].x;this.lastScreenY = e.changedTouches[0].y;}}).gesture(SwipeGesture({ direction: SwipeDirection.All })// 支持方向中 all可以是上下左右.onAction((_event: GestureEvent) => {const swipeX = this.lastScreenX - this.screenStartX;const swipeY = this.lastScreenY - this.screenStartY;// 清除开始位置记录,准备下一次滑动判断this.screenStartX = 0;this.screenStartY = 0;if (Math.abs(swipeX) > Math.abs(swipeY)) {if (swipeX > 0) {// 向右滑动this.movePlayer('right');} else {// 向左滑动this.movePlayer('left');}} else {if (swipeY > 0) {// 向下滑动this.movePlayer('down');} else {// 向上滑动this.movePlayer('up');}}}))}
}
相关文章:

鸿蒙开发案例:推箱子
推箱子游戏(Sokoban)的实现。游戏由多个单元格组成,每个单元格可以是透明的、墙或可移动的区域。游戏使用Cell类定义单元格的状态,如类型(透明、墙、可移动区域)、圆角大小及坐标偏移。而MyPosition类则用于…...

mysql--表的约束
目录 理解表的约束和操作 如何理解? 1、空属性null 2、默认值default 3、列描述comment 4、自动填充zorefill 5、主键primary key (1)创建表时指定可以 (2)创建表后指定key (3)删除主…...

Ubuntu 上安装 docker 并配置 Docker Compose 详细步骤
💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「storm…...

MySQL去除空白字符(如非标准空格、制表符等)
在 MySQL 中,需要去除 site_name 字段的空格,可以使用 TRIM() 函数。这个函数可以去掉字符串开头和结尾的空格。以下是一个示例查询,演示如何选择去除空格后的 site_name: SELECT TRIM(site_name) AS site_name FROM site_info;如…...

2063:【例1.4】牛吃牧草
【题目描述】 有一个牧场,牧场上的牧草每天都在匀速生长,这片牧场可供15头牛吃20天,或可供20头牛吃10天,那么,这片牧场每天新生的草量可供几头牛吃1天? 【输入】 (无) 【输出】 如题…...

QT开发:深入掌握 QtGui 和 QtWidgets 布局管理:QVBoxLayout、QHBoxLayout 和 QGridLayout 的高级应用
目录 引言 1. QVBoxLayout:垂直布局管理器 基本功能 创建 QVBoxLayout 添加控件 添加控件和设置对齐方式 设置对齐方式 示例代码与详解 2. QHBoxLayout:水平布局管理器 基本功能 创建 QHBoxLayout 添加控件 添加控件和设置对齐方式 设置对齐…...

Bootstrapping、Bagging 和 Boosting
bagging方法如下: bagging和boosting比较...

板块龙头公司
高通 高通(Qualcomm)是一家总部位于美国加利福尼亚州的全球领先半导体和电信设备公司。成立于1985年,高通专注于无线通信技术的研发和创新。 移动处理器: 高通开发的骁龙(Snapdragon)系列芯片广泛用于智能手机和平板电…...

Java项目-基于Springboot的招生管理系统项目(源码+说明).zip
作者:计算机学长阿伟 开发技术:SpringBoot、SSM、Vue、MySQL、ElementUI等,“文末源码”。 开发运行环境 开发语言:Java数据库:MySQL技术:SpringBoot、Vue、Mybaits Plus、ELementUI工具:IDEA/…...

使用 MongoDB 构建 AI:利用实时客户数据优化产品生命周期
在《使用 MongoDB 构建 AI》系列博文中,我们看到越来越多的企业正在利用 AI 技术优化产品研发和用户支持流程。例如,我们介绍了以下案例: Ventecon 的 AI 助手帮助产品经理生成和优化新产品规范 Cognigy 的对话式 AI 帮助企业使用任意语言&a…...

【React】React18核心源码解读
前言 本文使用 React18.2.0 的源码,如果想回退到某一版本执行git checkout tags/v18.2.0即可。如果打开源码发现js文件报ts类型错误请看本人另一篇文章:VsCode查看React源码全是类型报错如何解决。 阅读源码的过程: 下载源码 观察 package…...

部署私有仓库以及docker web ui应用
官方地址:https://hub.docker.com/_/registry/tags 一、拉取registry私有仓库镜像 docker pull registry:latest 二、运⾏容器 docker run -itd -v /home/dockerdata/registry:/var/lib/registry --name "pri_registry1" --restartalways -p 5000:5000 …...

DAY57WEB 攻防-SSRF 服务端请求Gopher 伪协议无回显利用黑白盒挖掘业务功能点
知识点: 1、SSRF-原理-外部资源加载 2、SSRF-利用-伪协议&无回显 3、SSRF-挖掘-业务功能&URL参数 SSRF-原理&挖掘&利用&修复 漏洞原理:SSRF(Server-Side Request Forgery:服务器端请求伪造) ,一种由攻击者构造形成由服务…...

光盘刻录大文件时分卷操作
可以使用 split 命令来将大文件 finetune.tar 分卷为适合光盘大小的文件片段,然后在离线服务器上合并这些分卷文件。以下是具体的操作步骤: 步骤1:分卷文件 假设你的文件 finetune.tar 大小为35GB,并且你想分卷为每个4.7GB&…...

Kafka系列之:生产者性能调优
Kafka系列之:生产者性能调优 一、producer.type二、request.required.acks三、max.request.size四、batch.size五、buffer.memory一、producer.type 在Kafka中,producer.type是一个配置属性,用于指定Producer的类型。它有两个可能的值: sync:同步发送模式。当设置为sync时…...

【linux】进程创建与进程终止
🔥个人主页:Quitecoder 🔥专栏:linux笔记仓 目录 01.进程创建02.进程终止异常终止如何终止exit()_exit() 01.进程创建 #include <unistd.h> pid_t fork(void);返回值:自进程中返回0,父进程返回子进…...

QT的文件操作类 QFile
QFile 是 Qt 框架中用于文件处理的一个类。它提供了读取和写入文件的功能,支持文本和二进制文 件。 QFile 继承自 QIODevice ,因此它可以像其他IO设备一样使用。 主要功能 文件读写: QFile 支持打开文件进行读取或写入操作文件信息&#x…...

java项目篇-用户脱敏展示
用户敏感信息脱敏展示 定义手机号和证件号的 Jackson 自定义序列化器,并在对应需要脱敏的敏感字段上指定自定义序列化器。在进行指定的需要脱敏的字段(身份证号,手机号,银行卡号等)序列化的时候,该字段自动…...

《C++计算引擎:驱动高效计算的强大动力》
在当今数字化时代,高效的计算能力是推动科技进步和创新的关键。而 C作为一种强大的编程语言,在构建高性能计算引擎方面发挥着重要作用。本文将深入探讨 C计算引擎的特点、优势以及在不同领域的应用,带您领略 C在计算领域的独特魅力。 一、C计…...

Linux的hadoop集群部署
1.hadoop是一个分布式系统基础架构,主要解决海量数据额度存储与海量数据的分析计算问题 hdfs提供存储能力,yarn提供资源管理能力,MapReduce提供计算能力 2.安装 一:调整虚拟机内存,4G即可 二:下载安装包 网址:https://mirrors.aliyun.com/apache/hadoop/common/hadoop-3.4.0/…...

请问:ESModule 与 CommonJS 的异同点是什么?
前言 本篇文章不会介绍模块的详细用法,因为核心是重新认识和理解模块的本质内容是什么,直奔主题,下面先给出最后结论,接下来在逐个进行分析。 ECMAScript Module 和 CommonJS 的相同点: 都拥有自己的缓存机制&#…...

【数据结构与算法】力扣 59. 螺旋矩阵 II
题目描述 给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。 示例 1: 输入: n 3 输出: [[1,2,3],[8,9,4],[7,6,5]]示例 2: 输入:…...

HarmonyOS Next模拟器异常问题及解决方法
1、问题1:Failed to get the device apiVersion. 解决方法:关闭模拟器清除用户数据重启...

求最大公约数(c语言)
先看题👇 我这里介绍的方法:辗转相除法: 最大公约数: 最大公约数是指同时能整除俩个或更多整数的最大正整数。 欧几里得算法就是求最大公约数的算法 求最大公约数涉及到一个数学原理的转换: 俩个数的最大公约数等于其中一个数和…...

Android Camera2在textureView中的预览和拍照
Camera2预览和拍照 1、Camera2相机模型2、Camera2的重要类3、Camera2调用流程4、Camera2调用实现 1)定义TextureView作为预览界面2)设置相机参数3)开启相机4)开启相机预览5)实现PreviewCallback6)拍照 1、Camera2相机模型 解释上诉示意图,假如想要同时拍摄两张不同…...

Redis的缓存问题
缓存雪崩 定义:缓存雪崩是指在某个时间段内,缓存中的大量数据同时失效或者大量的请求集中到某一个时间点发生,导致数据库压力骤增,甚至引起服务崩溃的现象。 原因:通常是由于缓存中的大量数据同时过期或者大量的请求集…...

C语言小游戏--猜数字
游戏过程: 由电脑随机在某个范围内生成一个数字,玩家猜数字并且输入,电脑判断是否正确,正确则游戏结束,错误则给出提示,直到玩家所给的答案正确为止 思路分析: 1.生成随机数 2.玩家可以多次…...

代理IP在爬虫中的作用是什么?
在爬虫中,代理IP的主要作用包括以下几个方面: 防止IP被封禁:每个网站都有反爬机制,会记录并封禁同一个IP地址的频繁请求。使用代理IP可以让爬虫更换源头,减少被目标网站识别为恶意爬虫的风险。 提高抓取效率ÿ…...

卡尔曼讲解与各种典型进阶MATLAB编程(专栏目录,持续更新……)
专栏链接:https://blog.csdn.net/callmeup/category_12574912.html 文章目录 专栏介绍重点文章卡尔曼滤波的原理卡尔曼滤波的例程 进阶MATLAB编程后续更新 专栏介绍 本专栏旨在深入探讨卡尔曼滤波及其在各类应用中的实现,尤其是通过MATLAB编程进行的典…...

Java项目-基于Springboot的智慧养老平台项目(源码+文档).zip
作者:计算机学长阿伟 开发技术:SpringBoot、SSM、Vue、MySQL、ElementUI等,“文末源码”。 开发运行环境 开发语言:Java数据库:MySQL技术:SpringBoot、SpringClud、Vue、Mybaits Plus、ELementUI工具&…...