鸿蒙开发案例:2048

a26.gif

【实现的功能】

• 游戏逻辑:实现了2048游戏的核心逻辑,包括初始化游戏盘面、添加随机方块、处理四个方向的滑动操作等。

• UI展示:构建了游戏的用户界面,显示得分、游戏盘面,并提供了重新开始按钮。

• 用户交互:支持触摸屏上的手势识别,通过滑动手势控制游戏盘面上方块的移动。

【待实现功能】

• 方块移动动画:暂未实现 原理应该是在UI的Text上设置.translate({ x:, y: })并添加.animation({duration: 200}),然后在逻辑里通过修改x或y来实现位移动画。研究了一下操作时方块的移动动画,但效果不尽如人意(T_T)。继续努力学习如何实现动画效果。

【完整代码】

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

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

// 使用Entry和Component装饰器标记Game2048结构体,可能表示这是程序的入口点,并且该结构体定义了一个组件
@Entry
@Component
  // 定义Game2048结构体
struct Game2048 {
  // 使用State装饰器标记状态变量,可能表示这些变量是组件的状态
  @State board: Cell[][] = []; // 游戏盘面
  @State score: number = 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); // 填充空位
      }
      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); // 填充空位
      }
      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); // 填充空位
      }

      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); // 填充空位
      }
      // 将处理后的数组元素放回到棋盘的对应列中
      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 || ''}`)
            .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%')
    .height('100%')
    .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.slideRight(); // 向右滑动
            } else {
              this.slideLeft(); // 向左滑动
            }
          } else {
            if (swipeY > 0) {
              this.slideDown(); // 向下滑动
            } else {
              this.slideUp(); // 向上滑动
            }
          }
          this.addRandomTiles(1); // 添加一个随机方块
        })
    )
  }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容