鸿蒙开发案例:推箱子

推箱子游戏(Sokoban)的实现。游戏由多个单元格组成,每个单元格可以是透明的、墙或可移动的区域。游戏使用Cell类定义单元格的状态,如类型(透明、墙、可移动区域)、圆角大小及坐标偏移。而MyPosition类则用于表示位置信息,并提供设置位置的方法。
游戏主体结构Sokoban定义了游戏的基本元素,包括网格单元格的状态、胜利位置、箱子的位置以及玩家的位置等,并提供了初始化游戏状态的方法。游戏中还包含有动画效果,当玩家尝试移动时,会检查目标位置是否允许移动,并根据情况决定是否需要移动箱子。此外,游戏支持触摸输入,并在完成一次移动后检查是否所有箱子都在目标位置上,如果是,则游戏胜利,并显示一个对话框展示游戏用时。
【算法分析】
1. 移动玩家和箱子算法分析:
算法思路:根据玩家的滑动方向,计算新的位置坐标,然后检查新位置的合法性,包括是否超出边界、是否是墙等情况。如果新位置是箱子,则需要进一步判断箱子后面的位置是否为空,以确定是否可以推动箱子。
实现逻辑:通过定义方向对象和计算新位置坐标的方式,简化了移动操作的逻辑。在移动过程中,需要考虑动画效果的控制,以提升用户体验。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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. 胜利条件判断算法分析:
算法思路:遍历所有箱子的位置,检查每个箱子是否在一个胜利位置上,如果所有箱子都在胜利位置上,则判定游戏胜利。
实现逻辑:通过嵌套循环和数组方法,实现了对胜利条件的判断。这种算法适合用于检查游戏胜利条件是否满足的场景。
1 2 3 4 5 | isVictoryConditionMet(): boolean { return this .cratePositions.every(crate => { return this .victoryPositions.some(victory => crate.x === victory.x && crate.y === victory.y); }); } |
3. 动画控制算法分析:
算法思路:利用动画函数实现移动过程中的动画效果,包括移动过程的持续时间和结束后的处理逻辑。
实现逻辑:通过嵌套调用动画函数,实现了移动过程中的动画效果控制。这种方式可以使移动过程更加流畅和生动。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | animateToImmediately({ duration: 150, onFinish: () => { animateToImmediately({ duration: 0, onFinish: () => { // 动画结束后的处理... } }, () => { // 动画过程中的处理... }); } }, () => { // 动画效果控制... }); |
4. 触摸操作和手势识别算法分析:
算法思路:监听触摸事件和手势事件,识别玩家的滑动方向,然后调用相应的移动函数处理玩家和箱子的移动。
实现逻辑:通过手势识别和事件监听,实现了玩家在屏幕上滑动操作的识别和响应。这种方式可以使玩家通过触摸操作来控制游戏的进行。
1 2 3 4 5 6 | gesture( SwipeGesture({ direction: SwipeDirection.All }) .onAction((_event: GestureEvent) => { // 手势识别和处理逻辑... }) ) |
【完整代码】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 | 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 = true animateToImmediately({ 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 = 0 this .grid[ this .playerPosition.x][ this .playerPosition.y].y = 0 this .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' ); } } }) ) } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了