写点什么

鸿蒙开发案例:推箱子

作者:zhongcx
  • 2024-10-18
    北京
  • 本文字数:7531 字

    阅读完需:约 25 分钟

推箱子游戏(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 = 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'); } } }) ) }}
复制代码


用户头像

zhongcx

关注

还未添加个人签名 2024-09-27 加入

还未添加个人简介

评论

发布
暂无评论
鸿蒙开发案例:推箱子_zhongcx_InfoQ写作社区