鸿蒙NEXT开发案例:2048

zhongcx
发布于 2024-12-1 08:53
浏览
0收藏

鸿蒙NEXT开发案例:2048-鸿蒙开发者社区

【引言】
本文将介绍如何使用鸿蒙NEXT框架开发经典的2048游戏。我们将通过代码示例展示游戏的核心逻辑,包括游戏初始化、滑动操作、合并单元格以及界面构建。
【环境准备】
电脑系统:windows 10
开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806
工程版本:API 12
真机:Mate 60 Pro
语言:ArkTS、ArkUI

  1. 类与装饰器
    在代码中,我们定义了两个主要的类:Cell和Game2048。Cell类表示游戏中的单元格,使用了@ObservedV2和@Trace装饰器,表明该类具有可观测性,并且其value属性的变化会被追踪。
@ObservedV2
class Cell {
  @Trace value: number;

  constructor() {
    this.value = 0;
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

Game2048结构体则是游戏的核心,使用了@Entry和@Component装饰器,表示这是程序的入口点,并且定义了一个组件。
2. 游戏状态管理
Game2048类中定义了多个状态变量,包括游戏盘面board、分数score、单元格大小cellSize等。通过@State装饰器标记这些变量,使其能够在界面中自动更新。

@State board: Cell[][] = [];
@State score: number = 0;
  • 1.
  • 2.
  1. 游戏逻辑
    3.1 初始化游戏
    aboutToAppear方法重置分数并初始化游戏盘面,调用initBoard方法创建4x4的单元格矩阵,并在随机位置添加两个初始方块。
aboutToAppear() {
  this.score = 0;
  this.initBoard();
  this.addRandomTiles(2);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

3.2 添加随机方块
addRandomTiles方法负责在空单元格中随机添加2或4。通过遍历盘面,记录空单元格的位置,并随机选择位置进行填充。

addRandomTiles(count: number) {
  // 逻辑代码
}
  • 1.
  • 2.
  • 3.

3.3 滑动与合并
游戏的核心操作是滑动,分别实现了slideLeft、slideRight、slideUp和slideDown方法。这些方法通过临时数组存储非零值,并处理合并逻辑,更新分数。

slideLeft() {
  // 逻辑代码
}
  • 1.
  • 2.
  • 3.
  1. 界面构建
    游戏界面通过build方法构建,使用Column和Flex布局显示得分和单元格。每个单元格的颜色和字体大小根据其值动态调整。
build() {
  Column({ space: 10 }) {
    // 显示得分和单元格
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  1. 触摸与手势处理
    通过onTouch和gesture方法处理用户的触摸事件,判断滑动方向并调用相应的滑动方法。
.onTouch((e) => {
  // 触摸事件处理
})
.gesture(
  SwipeGesture({ direction: SwipeDirection.All })
);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

【完整代码】

// 使用装饰器标记Cell类,可能表示该类具有可观测性
@ObservedV2
class Cell {
  // 使用Trace装饰器标记value属性,可能表示该属性的变化会被追踪
  @Trace value: number;

  // 构造函数初始化单元格的值为0
  constructor() {
    this.value = 0; // 设置单元格初始值为0
  }
}

// 使用Entry和Component装饰器标记Game2048结构体,可能表示这是程序的入口点,并且该结构体定义了一个组件
@Entry
@Component
  // 定义Game2048结构体
struct Game2048 {
  // 使用State装饰器标记状态变量,可能表示这些变量是组件的状态
  @State board: Cell[][] = []; // 游戏盘面,存储单元格
  @State score: number = 0; // 分数,初始化为0
  @State cellSize: number = 200; // 单元格大小
  @State cellMargin: number = 5; // 单元格之间的边距
  @State screenStartX: number = 0; // 触摸开始时的屏幕X坐标
  @State screenStartY: number = 0; // 触摸开始时的屏幕Y坐标
  @State lastScreenX: number = 0; // 触摸结束时的屏幕X坐标
  @State lastScreenY: number = 0; // 触摸结束时的屏幕Y坐标

  // 定义颜色数组,表示不同数字对应的颜色
  colors: string[] = [
    '#CCCCCC', // 0 - 灰色
    '#FFC107', // 2 - 黄色
    '#FF9800', // 4 - 橙色
    '#FF5722', // 8 - 深橙色
    '#F44336', // 16 - 红色
    '#9C27B0', // 32 - 紫色
    '#3F51B5', // 64 - 蓝紫色
    '#00BCD4', // 128 - 蓝色
    '#009688', // 256 - 深青色
    '#4CAF50', // 512 - 绿色
    '#8BC34A', // 1024 - 浅绿色
    '#CDDC39', // 2048 - 柠檬黄
    '#FFEB3B', // 4096 - 淡黄色
    '#795548', // 8192 - 棕色
    '#607D8B', // 16384 - 深灰色
    '#9E9E9E', // 32768 - 灰色
    '#000000' // 以上 - 黑色
  ];

  // 游戏即将出现时执行的方法
  aboutToAppear() {
    this.score = 0; // 重置分数
    this.initBoard(); // 重新初始化游戏板
    this.addRandomTiles(2); // 添加两个随机方块
  }

  // 初始化游戏盘面
  initBoard() {
    if (this.board.length == 0) { // 如果盘面为空
      for (let i = 0; i < 4; i++) { // 遍历行
        let cellArr: Cell[] = []; // 创建新行
        for (let j = 0; j < 4; j++) { // 遍历列
          cellArr.push(new Cell()); // 创建新单元格并添加到行中
        }
        this.board.push(cellArr); // 将行添加到盘面
      }
    } else { // 如果盘面不为空
      for (let i = 0; i < this.board.length; i++) { // 遍历已有行
        for (let j = 0; j < this.board[i].length; j++) { // 遍历列
          this.board[i][j].value = 0; // 清空单元格值
        }
      }
    }
  }

  // 在盘面上添加指定数量的随机方块
  addRandomTiles(count: number) {
    let emptyCells: object[] = []; // 存储空单元格位置
    for (let row = 0; row < 4; row++) { // 遍历行
      for (let col = 0; col < 4; col++) { // 遍历列
        if (this.board[row][col].value === 0) { // 如果单元格为空
          emptyCells.push(Object({ row: row, col: col })); // 记录空单元格位置
        }
      }
    }

    for (let i = 0; i < count; i++) { // 添加指定数量的随机方块
      if (emptyCells.length > 0) { // 如果还有空单元格
        let randomIndex = Math.floor(Math.random() * emptyCells.length); // 随机选择一个空单元格
        let obj = emptyCells[randomIndex]; // 获取该空单元格的位置
        this.board[obj['row']][obj['col']].value = Math.random() < 0.9 ? 2 : 4; // 随机生成2或4
        emptyCells.splice(randomIndex, 1); // 移除已使用的空单元格位置
      }
    }
  }

  // 向左滑动
  slideLeft() {
    for (let row = 0; row < 4; row++) { // 遍历每一行
      let tempRow: number[] = []; // 临时存储行数据
      let merged: boolean[] = new Array(4).fill(false); // 标记是否已经合并过

      for (let col = 0; col < 4; col++) { // 遍历列
        if (this.board[row][col].value !== 0) { // 如果单元格非零
          tempRow.push(this.board[row][col].value); // 移动非零值到临时数组
        }
      }

      let mergePos = 0; // 合并位置指针
      while (mergePos < tempRow.length - 1) { // 遍历临时数组
        if (tempRow[mergePos] === tempRow[mergePos + 1] && !merged[mergePos]) { // 如果可以合并
          tempRow[mergePos] *= 2; // 合并
          this.score += tempRow[mergePos]; // 更新分数
          merged[mergePos] = true; // 标记已合并
          tempRow.splice(mergePos + 1, 1); // 移除合并过的值
        } else {
          mergePos++; // 移动到下一个位置
        }
      }

      while (tempRow.length < 4) { // 填充空位
        tempRow.push(0); // 添加0到临时数组
      }
      for (let col = 0; col < 4; col++) { // 更新盘面
        this.board[row][col].value = tempRow[col]; // 将临时数组的值放回盘面
      }
    }
  }

  // 向右滑动
  slideRight() {
    for (let row = 0; row < 4; row++) { // 遍历每一行
      let tempRow: number[] = []; // 临时存储行数据
      let merged: boolean[] = new Array(4).fill(false); // 标记是否已经合并过

      for (let col = 3; col >= 0; col--) { // 从右向左遍历列
        if (this.board[row][col].value !== 0) { // 如果单元格非零
          tempRow.unshift(this.board[row][col].value); // 移动非零值到临时数组
        }
      }
      let mergePos = tempRow.length - 1; // 合并位置指针
      while (mergePos > 0) { // 遍历临时数组
        if (tempRow[mergePos] === tempRow[mergePos - 1] && !merged[mergePos - 1]) { // 如果可以合并
          tempRow[mergePos] *= 2; // 合并
          this.score += tempRow[mergePos]; // 更新分数
          merged[mergePos - 1] = true; // 标记已合并
          tempRow.splice(mergePos - 1, 1); // 移除合并过的值
        } else {
          mergePos--; // 移动到上一个位置
        }
      }

      while (tempRow.length < 4) { // 填充空位
        tempRow.unshift(0); // 添加0到临时数组
      }
      for (let col = 0; col < 4; col++) { // 更新盘面
        this.board[row][col].value = tempRow[col]; // 将临时数组的值放回盘面
      }
    }
  }

  // 向上滑动
  slideUp() {
    for (let col = 0; col < 4; col++) { // 遍历每一列
      let tempCol: number[] = [];
      let merged: boolean[] = new Array(4).fill(false); // 标记是否已经合并过

      for (let row = 0; row < 4; row++) { // 遍历行
        if (this.board[row][col].value !== 0) { // 如果单元格非零
          tempCol.push(this.board[row][col].value); // 移动非零值到临时数组
        }
      }

      let mergePos = 0; // 合并位置指针
      while (mergePos < tempCol.length - 1) { // 遍历临时数组
        if (tempCol[mergePos] === tempCol[mergePos + 1] && !merged[mergePos]) { // 如果可以合并
          tempCol[mergePos] *= 2; // 合并
          this.score += tempCol[mergePos]; // 更新分数
          merged[mergePos] = true; // 标记已合并
          tempCol.splice(mergePos + 1, 1); // 移除合并过的值
        } else {
          mergePos++; // 移动到下一个位置
        }
      }

      while (tempCol.length < 4) { // 填充空位
        tempCol.push(0); // 添加0到临时数组
      }

      for (let newRow = 0; newRow < 4; newRow++) { // 更新盘面
        this.board[newRow][col].value = tempCol[newRow]; // 将临时数组的值放回盘面
      }
    }
  }

  // 向下滑动
  slideDown() {
    for (let col = 0; col < 4; col++) { // 遍历每一列
      let tempCol: number[] = []; // 临时存储列数据
      let merged: boolean[] = new Array(4).fill(false); // 标记是否已经合并过

      // 从下往上遍历列
      for (let row = 3; row >= 0; row--) { // 从底部开始遍历
        if (this.board[row][col].value !== 0) { // 如果单元格非零
          tempCol.unshift(this.board[row][col].value); // 移动非零值到临时数组
        }
      }
      let mergePos = tempCol.length - 1; // 合并位置指针
      while (mergePos > 0) { // 遍历临时数组
        if (tempCol[mergePos] === tempCol[mergePos - 1] && !merged[mergePos - 1]) { // 如果可以合并
          tempCol[mergePos] *= 2; // 合并
          this.score += tempCol[mergePos]; // 更新分数
          merged[mergePos - 1] = true; // 标记已合并
          tempCol.splice(mergePos - 1, 1); // 移除合并过的值
        } else {
          mergePos--; // 移动到上一个位置
        }
      }

      // 如果数组长度小于4,用0填充
      while (tempCol.length < 4) {
        tempCol.unshift(0); // 添加0到临时数组
      }
      // 将处理后的数组元素放回到棋盘的对应列中
      for (let row = 0; row < 4; row++) {
        this.board[3 - row][col].value = tempCol[3 - row]; // 注意反转顺序
      }
    }
  }

  // 构建游戏界面
  build() {
    // 布局容器
    Column({ space: 10 }) {
      // 显示得分
      Text(`得分: ${this.score}`)
        .fontSize(24) // 设置字体大小
        .margin({ top: 20 }) // 设置上边距

      // 底层背景布局
      Flex({ wrap: FlexWrap.Wrap, direction: FlexDirection.Row }) {
        // 遍历每个单元格
        ForEach(this.board.flat(), (cell: Cell, index: number) => {
          // 显示单元格上的数字
          Text(`${cell.value || ''}`) // 显示单元格值,值为0时显示空
            .width(`${this.cellSize}px`) // 设置单元格宽度
            .height(`${this.cellSize}px`) // 设置单元格高度
            .margin(`${this.cellMargin}px`) // 设置单元格边距
            .fontSize(`${cell.value >= 100 ? this.cellSize / 3 : this.cellSize / 2}px`) // 根据数字大小调整字体大小
            .textAlign(TextAlign.Center) // 设置文本居中
            .backgroundColor(this.colors[cell.value == 0 ? 0 : Math.floor(Math.log2(cell.value))]) // 设置背景颜色
            .fontColor(cell.value === 0 ? '#000' : '#fff') // 设置字体颜色
            .borderRadius(5) // 设置圆角
        })
      }
      .width(`${(this.cellSize + this.cellMargin * 2) * 4}px`) // 设置容器宽度

      // 重新开始按钮
      Button('重新开始').onClick(() => {
        this.aboutToAppear(); // 重新开始游戏
      })
    }
    .width('100%') // 设置容器宽度为100%
    .height('100%') // 设置容器高度为100%
    .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.slideRight(); // 向右滑动
            } else {
              this.slideLeft(); // 向左滑动
            }
          } else {
            if (swipeY > 0) {
              this.slideDown(); // 向下滑动
            } else {
              this.slideUp(); // 向上滑动
            }
          }
          this.addRandomTiles(1); // 添加一个随机方块
        })
    )
  }
}
  • 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.

分类
标签
收藏
回复
举报


回复
    相关推荐