鸿蒙NEXT开发案例:推箱子

zhongcx
发布于 2024-12-1 09:29
浏览
0收藏

鸿蒙NEXT开发案例:推箱子-鸿蒙开发者社区

【引言】
推箱子是一种经典的益智游戏,通过操作玩家角色将箱子推到指定位置,达到胜利的游戏目标。本文将介绍如何使用鸿蒙NEXT开发推箱子游戏,并展示相关代码实现。
【环境准备】
电脑系统:windows 10
开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806
工程版本:API 12
真机:Mate 60 Pro
语言:ArkTS、ArkUI
【算法分析】

  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;

    // 检查新位置是否合法
    // 箱子移动逻辑...

    // 动画效果控制...
}
  1. 胜利条件判断算法分析:
    算法思路:遍历所有箱子的位置,检查每个箱子是否在一个胜利位置上,如果所有箱子都在胜利位置上,则判定游戏胜利。
    实现逻辑:通过嵌套循环和数组方法,实现了对胜利条件的判断。这种算法适合用于检查游戏胜利条件是否满足的场景。
isVictoryConditionMet(): boolean {
    return this.cratePositions.every(crate => {
        return this.victoryPositions.some(victory => crate.x === victory.x && crate.y === victory.y);
    });
}
  1. 动画控制算法分析:
    算法思路:利用动画函数实现移动过程中的动画效果,包括移动过程的持续时间和结束后的处理逻辑。
    实现逻辑:通过嵌套调用动画函数,实现了移动过程中的动画效果控制。这种方式可以使移动过程更加流畅和生动。
animateToImmediately({
    duration: 150,
    onFinish: () => {
        animateToImmediately({
            duration: 0,
            onFinish: () => {
                // 动画结束后的处理...
            }
        }, () => {
            // 动画过程中的处理...
        });
    }
}, () => {
    // 动画效果控制...
});
4. 触摸操作和手势识别算法分析:
算法思路:监听触摸事件和手势事件,识别玩家的滑动方向,然后调用相应的移动函数处理玩家和箱子的移动。
实现逻辑:通过手势识别和事件监听,实现了玩家在屏幕上滑动操作的识别和响应。这种方式可以使玩家通过触摸操作来控制游戏的进行。

【完整代码】

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; // 设置(0,1)单元格的左上角圆角大小
    this.grid[0][5].topRight = 25; // 设置(0,5)单元格的右上角圆角大小
    this.grid[1][0].topLeft = 25; // 设置(1,0)单元格的左上角圆角大小
    this.grid[4][0].bottomLeft = 25; // 设置(4,0)单元格的左下角圆角大小
    this.grid[5][1].bottomLeft = 25; // 设置(5,1)单元格的左下角圆角大小
    this.grid[5][5].bottomRight = 25; // 设置(5,5)单元格的右下角圆角大小
    this.grid[1][1].bottomRight = 10; // 设置(1,1)单元格的右下角圆角大小
    this.grid[4][1].topRight = 10; // 设置(4,1)单元格的右上角圆角大小
    this.grid[2][4].topLeft = 10; // 设置(2,4)单元格的左上角圆角大小
    this.grid[2][4].bottomLeft = 10; // 设置(2,4)单元格的左下角圆角大小
    this.initializeGame(); // 初始化游戏
  }

  initializeGame() { // 初始化游戏状态
    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 }) // 向上移动的偏移量
    });
    // 获取当前移动方向的X和Y偏移量
    const dx: number = directions[direction]['dx']; //{ dx, dy }
    const dy: number = directions[direction]['dy']; //{ dx, dy }
    // 计算玩家新位置的X和Y坐标
    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; // 初始化箱子索引为-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; // 如果箱子索引为-1或者新位置后面有箱子,返回
      }
    }

    if (this.isAnimationRunning) {
      return; // 如果动画正在运行,返回
    }
    this.isAnimationRunning = true; // 设置动画运行标志为true
    animateToImmediately({
      // 立即执行动画
      duration: 150, // 动画持续时间
      onFinish: () => { // 动画结束时的回调
        animateToImmediately({
          // 立即执行动画
          duration: 0, // 动画持续时间为0,即立即完成
          onFinish: () => { // 动画结束时的回调
            this.isAnimationRunning = false; // 设置动画运行标志为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; // 设置玩家X坐标偏移量
      this.grid[this.playerPosition.x][this.playerPosition.y].y = dx * this.cellWidth; // 设置玩家Y坐标偏移量
      if (crateIndex !== -1) { // 如果有箱子移动
        this.grid[this.cratePositions[crateIndex].x][this.cratePositions[crateIndex].y].x =
          dy * this.cellWidth; // 设置箱子X坐标偏移量
        this.grid[this.cratePositions[crateIndex].x][this.cratePositions[crateIndex].y].y =
          dx * this.cellWidth; // 设置箱子Y坐标偏移量
      }
      console.info(`dx:${dx},dy:${dy}`); // 输出dx和dy的信息
    });
  }

  build() { // 构建游戏界面
    Column({ space: 20 }) { // 垂直布局,间距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 }) // 旋转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")// 设置背景颜色
                    .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")// 设置背景颜色
                    .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` }) // 根据单元格的X和Y坐标设置位置

              })
            }
          })
        }
      }

      // 创建重新开始按钮
      Button('重新开始')// 按钮文本为“重新开始”
        .clickEffect({ level: ClickEffectLevel.MIDDLE })// 设置点击效果
        .onClick(() => { // 按钮点击事件
          this.initializeGame(); // 重新初始化游戏
        });

    }
    .width('100%') // 设置组件宽度为100%
    .height('100%') // 设置组件高度为100%
    .backgroundColor("#fdb300") // 设置背景颜色
    .padding({ top: 20 }) // 设置顶部内边距为20
    .onTouch((e) => { // 触摸事件处理
      if (e.type === TouchType.Down && e.touches.length > 0) { // 触摸开始,记录初始位置
        this.screenStartX = e.touches[0].x; // 记录触摸开始时的X坐标
        this.screenStartY = e.touches[0].y; // 记录触摸开始时的Y坐标
      } else if (e.type === TouchType.Up && e.changedTouches.length > 0) { // 当手指抬起时,更新最后的位置
        this.lastScreenX = e.changedTouches[0].x; // 记录触摸结束时的X坐标
        this.lastScreenY = e.changedTouches[0].y; // 记录触摸结束时的Y坐标
      }
    })
    .gesture( // 添加手势识别
      SwipeGesture({ direction: SwipeDirection.All })// 支持所有方向的滑动手势
        .onAction((_event: GestureEvent) => { // 手势动作处理
          const swipeX = this.lastScreenX - this.screenStartX; // 计算X轴滑动距离
          const swipeY = this.lastScreenY - this.screenStartY; // 计算Y轴滑动距离
          // 清除开始位置记录,准备下一次滑动判断
          this.screenStartX = 0; // 重置初始X坐标
          this.screenStartY = 0; // 重置初始Y坐标
          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'); // 移动玩家向上
            }
          }
        })
    )
  }
}

分类
标签
收藏
回复
举报
回复