HarmonyOS鸿蒙Next中应用如何实现贪吃蛇小游戏?

HarmonyOS鸿蒙Next中应用如何实现贪吃蛇小游戏? 应用如何实现贪吃蛇小游戏?

3 回复

https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtybbs/567/739/627/0030086000567739627.20251223232029.38727429692218748621621964082148:50001231000000:2800:551ECD6265FD89536E3FB3839F2E55B4F81B174C8BF31DCBC010D689D602EF15.png

引言

贪吃蛇,这款承载了无数人童年回忆的经典游戏,不仅是编程入门的绝佳练手项目,也能很好地帮助开发者熟悉一个新的开发环境和其 UI 框架。本文将带你从零开始,一步步使用 HarmonyOS 的 ArkUI 框架(基于 TypeScript)来构建一个功能完整、逻辑清晰的贪吃蛇游戏。我们将从最基础的地图绘制,到最终的面向对象封装,深入探索游戏开发的乐趣和 ArkUI 的强大之处。

思路

开发准备与环境搭建

第一步:绘制游戏地图

第二步:随机生成食物

第三步:创建并移动蛇

第四步:键盘控制与游戏逻辑

第五步:实现吃食物与计分

第六步:边界碰撞与游戏结束

第七步:游戏重置功能

进阶:面向对象封装

第一步:绘制游戏地图

游戏的舞台是地图。我们将创建一个 20x20 的网格来作为我们的游戏区域。

核心思路:使用 ForEach 循环嵌套,动态生成一个由小方块组成的二维网格。

// Index.ets
@Entry
@Component
struct SnakeGame {
  // 创建一个 20x20 的二维数组,用于表示地图网格
  private map: number[][] = Array(20).fill(null).map(() => Array(20).fill(0));
 
  build() {
    Column() {
      Text('贪吃蛇').fontSize(30).fontWeight(FontWeight.Bold).margin({ bottom: 10 });
 
      // 使用 Stack 来叠加地图、蛇和食物
      Stack() {
        // 绘制地图背景
        Column() {
          ForEach(this.map, (row: number[]) => {
            Row() {
              ForEach(row, () => {
                // 每个小方块代表一个单元格
                Column() {}.width(15).height(15)
                  .backgroundColor('#f0f0f0') // 浅灰色背景
                  .border({ width: 0.5, color: '#dddddd' }); // 加上边框,看起来更像网格
              })
            }
          })
        }
      }
      .margin({ top: 20 })
      .backgroundColor('#ffffff') // 地图容器背景
      .padding(5)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Start)
    .alignItems(ItemAlign.Center)
    .backgroundColor('#e8e8e8');
  }
}

代码解析:

我们定义了一个 map 数组,它的唯一目的是提供一个数据源,让 ForEach 能够循环指定的次数(20 行,每行 20 列)。

Stack 组件是实现游戏层叠效果的关键,后续的蛇和食物都将作为子组件放在这个 Stack 中。

每个单元格是一个 Column,通过设置固定的 width 和 height 来控制其大小。

第二步:随机生成食物

食物是蛇的目标。我们需要在地图上随机位置生成一个红色的方块。

核心思路:在组件初始化时,随机生成食物的 X 和 Y 坐标,并使用 [@State](/user/State) 装饰器使其成为响应式数据,以便在位置改变时 UI 能够自动更新。

// Index.ets
// ... (省略之前的代码)
struct SnakeGame {
  private map: number[][] = Array(20).fill(null).map(() => Array(20).fill(0));
  // 使用 [@State](/user/State) 装饰器,让食物位置变化时能刷新UI
  [@State](/user/State) food: number[] = []; // [y, x]
 
  // 封装一个生成食物的函数
  private generateFood() {
    this.food = [
      Math.floor(Math.random() * 20), // 随机 Y 坐标 (0-19)
      Math.floor(Math.random() * 20)  // 随机 X 坐标 (0-19)
    ];
  }
 
  // 组件即将出现时调用
  aboutToAppear() {
    this.generateFood();
  }
 
  build() {
    Column() {
      // ... (省略标题)
      Stack() {
        // ... (省略地图绘制)
 
        // 绘制食物
        if (this.food.length > 0) {
          Text()
            .width(15)
            .height(15)
            .backgroundColor(Color.Red)
            .position({
              top: this.food[0] * 15,  // Y坐标 * 单元格高度
              left: this.food[1] * 15  // X坐标 * 单元格宽度
            });
        }
      }
      // ... (省略其他样式)
    }
    // ... (省略外层样式)
  }
}

代码解析:

@State food: number[]:food 数组的第一个元素是 Y 轴坐标,第二个是 X 轴坐标。@State 确保了一旦 food 的值改变,使用它的 UI 组件(这里是食物的 position)会重新渲染。

aboutToAppear():这是一个生命周期回调,在组件即将在界面上显示时执行。我们在这里调用 generateFood(),确保游戏一开始就有食物。

position({ top: …, left: … }):通过绝对定位将食物放置在计算好的位置上。

第三步:创建并移动蛇

蛇是游戏的主角。它由多个方块组成,需要根据规则移动。

核心思路:用一个二维数组 snake 来存储蛇身体每个部分的坐标。移动时,从蛇尾开始,每个身体部分都移动到前一个部分的位置,最后再根据当前方向移动蛇头。

// Index.ets
// ... (省略之前的代码)
struct SnakeGame {
  // ... (省略 map, food)
  [@State](/user/State) snake: number[][] = [
    [10, 6], // 蛇头 [y, x]
    [10, 5],
    [10, 4],
    [10, 3],
    [10, 2]  // 蛇尾
  ];
  private direction: 'top' | 'left' | 'bottom' | 'right' = 'right'; // 初始方向向右
  private timer: number = 0; // 定时器ID
 
  // ... (省略 generateFood, aboutToAppear)
 
  // 蛇移动的核心逻辑
  private moveSnake() {
    // 从蛇尾开始,依次向前移动
    for (let i = this.snake.length - 1; i > 0; i--) {
      this.snake[i] = [...this.snake[i - 1]]; // 复制前一个节点的坐标
    }
 
    // 根据方向移动蛇头
    switch (this.direction) {
      case 'top':
        this.snake[0][0]--;
        break;
      case 'bottom':
        this.snake[0][0]++;
        break;
      case 'left':
        this.snake[0][1]--;
        break;
      case 'right':
        this.snake[0][1]++;
        break;
    }
    
    // 在 HarmonyOS 中,直接修改数组元素可能无法触发UI更新,
    // 因此我们创建一个新数组来触发重新渲染
    this.snake = [...this.snake];
  }
 
  build() {
    Column() {
      // ... (省略标题)
      Stack() {
        // ... (省略地图绘制)
        // ... (省略食物绘制)
 
        // 绘制蛇
        ForEach(this.snake, (segment: number[], index: number) => {
          Text()
            .width(15)
            .height(15)
            .backgroundColor(index === 0 ? Color.Pink : Color.Black) // 蛇头用粉色,身体用黑色
            .position({
              top: segment[0] * 15,
              left: segment[1] * 15
            });
        })
      }
      // ... (省略其他样式)
    }
    // ... (省略外层样式)
  }
}

代码解析:

@State snake: number[][]:snake 数组中的每个元素都是一个 [y, x] 坐标对,代表蛇身体的一个部分。数组的第一个元素是蛇头。

moveSnake():这是蛇移动的核心。通过循环,我们让蛇的每一节身体都 “继承” 前一节的位置,从而实现整体移动的效果。最后再单独处理蛇头的位置。

this.snake = […this.snake];:这是一个在 ArkUI 中非常重要的技巧。由于 snake 是一个引用类型(数组),直接修改其内部元素(如 this.snake[0][0]–)并不会触发 UI 的重新渲染。通过创建一个新的数组(使用扩展运算符 …)并赋值给 this.snake,我们可以强制触发 UI 更新。

第四步:键盘控制与游戏逻辑

现在,我们需要让蛇动起来,并能通过按钮控制它的方向。

核心思路:

添加 “开始”、“暂停”、“重置” 按钮。

使用 setInterval 定时器来周期性地调用 moveSnake 函数,让蛇自动移动。

添加方向控制按钮(上、下、左、右),并在点击时改变 direction 变量的值。

// Index.ets
// ... (省略之前的代码)
struct SnakeGame {
  // ... (省略 map, food, snake, direction, timer)
  [@State](/user/State) score: number = 0;
  [@State](/user/State) gameOverStr: string = '';
 
  // ... (省略 generateFood, aboutToAppear, moveSnake)
 
  // 开始游戏
  private startGame() {
    this.pauseGame(); // 先停止现有定时器,防止重复启动
    this.timer = setInterval(() => {
      this.moveSnake();
      this.checkCollisions(); // 移动后检查碰撞
    }, 200); // 每200毫秒移动一次
  }
 
  // 暂停游戏
  private pauseGame() {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = 0;
    }
  }
 
  build() {
    Column() {
      Text(`分数: ${this.score}`).fontSize(20).margin({ top: 10 });
 
      // 游戏控制按钮
      Row() {
        Button('开始').onClick(() => this.startGame());
        Button('暂停').margin({ left: 10 }).onClick(() => this.pauseGame());
        Button('重置').margin({ left: 10 }).onClick(() => this.resetGame());
      }.margin({ bottom: 10 });
 
      // ... (省略 Stack 中的地图、蛇、食物)
 
      // 方向控制按钮
      Column() {
        Button('↑').onClick(() => this.direction = 'top');
        Row() {
          Button('←').onClick(() => this.direction = 'left');
          Button('→').margin({ left: 20 }).onClick(() => this.direction = 'right');
        }.margin({ top: 5, bottom: 5 });
        Button('↓').onClick(() => this.direction = 'bottom');
      }
      .enabled(this.gameOverStr === '') // 游戏结束时禁用按钮
      .margin({ top: 20 });
    }
    // ... (省略外层样式)
  }
}

注意checkCollisionsresetGame 函数我们将在后续步骤中实现。

第五步:实现吃食物与计分

当蛇头移动到食物的位置时,蛇的身体应该变长,分数应该增加,并且食物应该在新的位置重新生成。

核心思路:在 moveSnake 之后,检查蛇头坐标是否与食物坐标重合。如果重合,则在蛇尾添加一个新的身体部分,并重新生成食物。

// Index.ets
// ... (省略之前的代码)
struct SnakeGame {
  // ... (省略所有变量和其他函数)
 
  // 检查碰撞(包括边界、自己、食物)
  private checkCollisions() {
    const head = this.snake[0];
 
    // 1. 检查是否吃到食物
    if (head[0] === this.food[0] && head[1] === this.food[1]) {
      this.score += 10;
      this.snake.push([...this.snake[this.snake.length - 1]]); // 在蛇尾添加一节
      this.generateFood();
      return; // 吃到食物后,不再检查死亡
    }
 
    // 后续将添加边界和自撞检测...
  }
 
  private startGame() {
    this.pauseGame();
    this.timer = setInterval(() => {
      this.moveSnake();
      this.checkCollisions(); // 调用检查函数
    }, 200);
  }
 
  // ... (省略 build 方法)
}

代码解析:

checkCollisions 函数被放在 setInterval 中,在每次蛇移动后调用。

通过比较蛇头 head 和食物 food 的坐标来判断是否吃到食物。

this.snake.push([…this.snake[this.snake.length - 1]]):当吃到食物时,我们在蛇数组的末尾添加一个新的元素,其坐标与当前蛇尾相同。由于下一次移动时,所有身体部分都会向前移动,这个新的部分就会自然地成为新的蛇尾,从而实现蛇身变长的效果。

第六步:边界碰撞与游戏结束

游戏需要有结束条件。最常见的就是蛇头撞到地图边界或者撞到自己的身体。

核心思路:在 checkCollisions 函数中,添加对边界和自身身体的检测。

// Index.ets
// ... (省略之前的代码)
struct SnakeGame {
  // ... (省略所有变量和其他函数)
 
  // 检查碰撞(包括边界、自己、食物)
  private checkCollisions() {
    const head = this.snake[0];
 
    // 1. 检查是否吃到食物 (上面已实现)
    if (head[0] === this.food[0] && head[1] === this.food[1]) {
      // ... (省略吃食物的逻辑)
      return;
    }
 
    // 2. 检查边界碰撞
    if (head[0] < 0 || head[0] >= 20 || head[1] < 0 || head[1] >= 20) {
      this.gameOver('游戏结束!撞到墙了!');
      return;
    }
 
    // 3. 检查自撞
    for (let i = 1; i < this.snake.length; i++) {
      if (head[0] === this.snake[i][0] && head[1] === this.snake[i][1]) {
        this.gameOver('游戏结束!撞到自己了!');
        return;
      }
    }
  }
 
  // 游戏结束
  private gameOver(message: string) {
    this.pauseGame();
    this.gameOverStr = message;
  }
 
  build() {
    Column() {
      // ... (省略其他UI)
 
      Stack() {
        // ... (省略地图、蛇、食物绘制)
 
        // 游戏结束提示
        if (this.gameOverStr) {
          Column() {
            Text(this.gameOverStr).fontSize(30).fontColor(Color.Red).fontWeight(FontWeight.Bold);
            Text('点击重置按钮重新开始').fontSize(16).fontColor(Color.Gray).margin({ top: 10 });
          }
          .justifyContent(FlexAlign.Center)
          .alignItems(ItemAlign.Center)
          .backgroundColor('rgba(255, 255, 255, 0.8)')
          .width('100%')
          .height('100%');
        }
      }
      // ... (省略其他UI)
    }
    // ... (省略外层样式)
  }
}

7. 第六步:边界碰撞与游戏结束

游戏需要有结束条件。最常见的就是蛇头撞到地图边界或者撞到自己的身体。

核心思路:在 checkCollisions 函数中,添加对边界和自身身体的检测。

// Index.ets
// ... (省略之前的代码)
struct SnakeGame {
  // ... (省略所有变量和其他函数)
 
  // 检查碰撞(包括边界、自己、食物)
  private checkCollisions() {
    const head = this.snake[0];
 
    // 1. 检查是否吃到食物 (上面已实现)
    if (head[0] === this.food[0] && head[1] === this.food[1]) {
      // ... (省略吃食物的逻辑)
      return;
    }
 
    // 2. 检查边界碰撞
    if (head[0] < 0 || head[0] >= 20 || head[1] < 0 || head[1] >= 20) {
      this.gameOver('游戏结束!撞到墙了!');
      return;
    }
 
    // 3. 检查自撞
    for (let i = 1; i < this.snake.length; i++) {
      if (head[0] === this.snake[i][0] && head[1] === this.snake[i][1]) {
        this.gameOver('游戏结束!撞到自己了!');
        return;
      }
    }
  }
 
  // 游戏结束
  private gameOver(message: string) {
    this.pauseGame();
    this.gameOverStr = message;
  }
 
  build() {
    Column() {
      // ... (省略其他UI)
 
      Stack() {
        // ... (省略地图、蛇、食物绘制)
 
        // 游戏结束提示
        if (this.gameOverStr) {
          Column() {
            Text(this.gameOverStr).fontSize(30).fontColor(Color.Red).fontWeight(FontWeight.Bold);
            Text('点击重置按钮重新开始').fontSize(16).fontColor(Color.Gray).margin({ top: 10 });
          }
          .justifyContent(FlexAlign.Center)
          .alignItems(ItemAlign.Center)
          .backgroundColor('rgba(255, 255, 255, 0.8)')
          .width('100%')
          .height('100%');
        }
      }
      //

更多关于HarmonyOS鸿蒙Next中应用如何实现贪吃蛇小游戏?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS Next中实现贪吃蛇游戏

技术栈

  • ArkTS语言
  • ArkUI框架

核心实现

1. 游戏界面绘制

使用 Canvas组件 绘制游戏界面,包括:

  • 蛇身
  • 食物
  • 网格

2. 游戏逻辑控制

  • 定时器:控制蛇的移动速度
  • 方向键事件监听:改变蛇头移动方向
  • 蛇身存储:使用数组存储坐标点
  • 食物生成:随机生成位置

3. 碰撞检测

  • 边界碰撞检测
  • 自身碰撞检测

4. 状态管理

使用 @State装饰器 管理游戏状态,实现UI的实时更新。

在HarmonyOS Next中实现贪吃蛇小游戏,核心在于利用ArkUI框架进行界面绘制与交互,并结合状态管理控制游戏逻辑。以下是关键实现步骤:

  1. 项目与界面搭建

    • 使用DevEco Studio创建Empty Ability项目,选择Stage模型与ArkTS语言。
    • pages目录下的ArkTS文件中,使用Canvas组件作为游戏画布,并通过CanvasRenderingContext2DAPI进行图形绘制。
  2. 游戏数据与状态管理

    • 定义蛇身(数组存储坐标)、食物位置、移动方向、游戏状态(进行中/结束)等状态变量,使用@State装饰器使其响应式更新。
    • 蛇身移动通过定时器(setInterval)驱动,每帧更新蛇头坐标(根据方向增减),并同步更新蛇身数组。
  3. 核心逻辑实现

    • 绘制功能:在Canvas的onReady回调中,使用context绘制网格背景、蛇身(矩形块)和食物(圆形或矩形)。
    • 方向控制:监听屏幕触控或键盘事件(如onKeyEvent),根据滑动方向或按键更新移动方向变量(如上、下、左、右)。
    • 碰撞检测:每帧检查蛇头是否触碰边界或自身身体,若碰撞则结束游戏;同时检测是否与食物坐标重合,若重合则增长蛇身并重新随机生成食物。
  4. 代码结构示例

    [@Entry](/user/Entry)
    [@Component](/user/Component)
    struct SnakeGame {
      @State snake: number[][] = [[10, 10]]; // 蛇身坐标
      @State food: number[] = [15, 15]; // 食物坐标
      @State dir: string = 'right'; // 移动方向
      private gridSize: number = 20; // 网格大小
    
      build() {
        Column() {
          // 游戏画布
          Canvas(this.onDraw)
            .width('100%')
            .height('80%')
            .onReady(() => {
              setInterval(() => this.moveSnake(), 200); // 游戏循环
            })
        }
      }
    
      onDraw(context: CanvasRenderingContext2D) {
        // 绘制蛇身与食物
        this.snake.forEach(seg => {
          context.fillRect(seg[0]*this.gridSize, seg[1]*this.gridSize, this.gridSize, this.gridSize);
        });
        context.fillCircle(this.food[0]*this.gridSize, this.food[1]*this.gridSize, this.gridSize/2);
      }
    
      moveSnake() {
        // 更新蛇头位置、检测碰撞与食物
        let head = [...this.snake[0]];
        switch(this.dir) {
          case 'up': head[1]--; break;
          case 'down': head[1]++; break;
          case 'left': head[0]--; break;
          case 'right': head[0]++; break;
        }
        // 碰撞检测与游戏逻辑更新
        if (this.isCollision(head)) { /* 结束游戏 */ }
        if (head[0]===this.food[0] && head[1]===this.food[1]) { /* 增长蛇身 */ }
        // 更新蛇身数组
        this.snake = [head, ...this.snake.slice(0, -1)];
      }
    }
    
  5. 交互优化

    • 添加触控事件(如onTouch)实现滑动手势控制方向,提升移动端操作体验。
    • 通过@Prop@Link装饰器分离游戏逻辑与界面组件,提高代码可维护性。

注意:实际开发需处理画布清晰度适配、游戏暂停/重启、分数计算等功能。建议参考HarmonyOS官方文档中的Canvas绘制与事件处理示例进行扩展。

回到顶部