鸿蒙开发案例:推箱子

推箱子游戏(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/…...
STM32+rt-thread判断是否联网
一、根据NETDEV_FLAG_INTERNET_UP位判断 static bool is_conncected(void) {struct netdev *dev RT_NULL;dev netdev_get_first_by_flags(NETDEV_FLAG_INTERNET_UP);if (dev RT_NULL){printf("wait netdev internet up...");return false;}else{printf("loc…...
基础测试工具使用经验
背景 vtune,perf, nsight system等基础测试工具,都是用过的,但是没有记录,都逐渐忘了。所以写这篇博客总结记录一下,只要以后发现新的用法,就记得来编辑补充一下 perf 比较基础的用法: 先改这…...
Redis:现代应用开发的高效内存数据存储利器
一、Redis的起源与发展 Redis最初由意大利程序员Salvatore Sanfilippo在2009年开发,其初衷是为了满足他自己的一个项目需求,即需要一个高性能的键值存储系统来解决传统数据库在高并发场景下的性能瓶颈。随着项目的开源,Redis凭借其简单易用、…...
【安全篇】金刚不坏之身:整合 Spring Security + JWT 实现无状态认证与授权
摘要 本文是《Spring Boot 实战派》系列的第四篇。我们将直面所有 Web 应用都无法回避的核心问题:安全。文章将详细阐述认证(Authentication) 与授权(Authorization的核心概念,对比传统 Session-Cookie 与现代 JWT(JS…...
ArcGIS Pro+ArcGIS给你的地图加上北回归线!
今天来看ArcGIS Pro和ArcGIS中如何给制作的中国地图或者其他大范围地图加上北回归线。 我们将在ArcGIS Pro和ArcGIS中一同介绍。 1 ArcGIS Pro中设置北回归线 1、在ArcGIS Pro中初步设置好经纬格网等,设置经线、纬线都以10间隔显示。 2、需要插入背会归线…...
js 设置3秒后执行
如何在JavaScript中延迟3秒执行操作 在JavaScript中,要设置一个操作在指定延迟后(例如3秒)执行,可以使用 setTimeout 函数。setTimeout 是JavaScript的核心计时器方法,它接受两个参数: 要执行的函数&…...
Element-Plus:popconfirm与tooltip一起使用不生效?
你们好,我是金金金。 场景 我正在使用Element-plus组件库当中的el-popconfirm和el-tooltip,产品要求是两个需要结合一起使用,也就是鼠标悬浮上去有提示文字,并且点击之后需要出现气泡确认框 代码 <el-popconfirm title"是…...
Linux 内存管理调试分析:ftrace、perf、crash 的系统化使用
Linux 内存管理调试分析:ftrace、perf、crash 的系统化使用 Linux 内核内存管理是构成整个内核性能和系统稳定性的基础,但这一子系统结构复杂,常常有设置失败、性能展示不良、OOM 杀进程等问题。要分析这些问题,需要一套工具化、…...
自定义线程池1.2
自定义线程池 1.2 1. 简介 上次我们实现了 1.1 版本,将线程池中的线程数量交给使用者决定,并且将线程的创建延迟到任务提交的时候,在本文中我们将对这个版本进行如下的优化: 在新建线程时交给线程一个任务。让线程在某种情况下…...
VUE3 ref 和 useTemplateRef
使用ref来绑定和获取 页面 <headerNav ref"headerNavRef"></headerNav><div click"showRef" ref"buttonRef">refbutton</div>使用ref方法const后面的命名需要跟页面的ref值一样 const buttonRef ref(buttonRef) cons…...
