纯血Harmony NETX 5小游戏实践:贪吃蛇(附源文件)

发布于:2025-06-11 ⋅ 阅读:(22) ⋅ 点赞:(0)

一、项目缘起:对鸿蒙应用开发的探索

在鸿蒙系统生态逐渐丰富的当下,我一直想尝试开发一款简单又有趣的应用,以此深入了解鸿蒙应用开发的流程与特性。贪吃蛇作为经典游戏,规则易懂、逻辑清晰,非常适合用来实践。于是,基于鸿蒙的ArkTS语言,开启了这款“贪吃蛇大冒险”小游戏的开发之旅。

二、核心逻辑拆解:构建游戏的“大脑”

(一)数据结构与初始状态

首先定义SnakeSegment接口,用于描述蛇的身体片段和食物的坐标信息,包含xy两个数值属性。游戏初始时,蛇的主体snake是一个包含单个片段(初始位置{x: 150, y: 150} )的数组;食物food通过generateFood方法随机生成,其坐标范围基于350x350的游戏区域,确保在合理范围内;方向控制directionR初始为right,游戏状态gameOverfalse,分数score从0开始累积 。

interface SnakeSegment {
  x: number;
  y: number;
}
@Preview
@Component
export struct play_4 {
  @State snake: Array<SnakeSegment> = [{x: 150, y: 150}];
  @State food: SnakeSegment = this.generateFood();
  @State directionR: string = 'right';
  @State gameOver: boolean = false;
  @State score: number = 0;

  generateFood(): SnakeSegment {
    const x = Math.floor(Math.random() * 35) * 10; 
    const y = Math.floor(Math.random() * 35) * 10;
    return {x, y};
  }
  // 后续方法...
}

(二)蛇的移动与成长机制

updateSnakePosition方法是蛇移动逻辑的核心。先根据当前方向directionR计算新蛇头的坐标:向上则headY减10,向下则加10,向左headX减10,向右则加10 。创建新蛇头后,判断是否吃到食物:若新蛇头坐标与食物坐标重合,分数增加10分,重新生成食物,且蛇身增长(将新蛇头添加到数组开头);若未吃到食物,蛇身移动(新蛇头添加到开头,同时移除最后一个片段 )。最后调用checkCollision方法检查碰撞情况。

updateSnakePosition(): void {
  let headX = this.snake[0].x;
  let headY = this.snake[0].y;

  switch (this.directionR) {
    case 'up':
      headY -= 10;
      break;
    case 'down':
      headY += 10;
      break;
    case 'left':
      headX -= 10;
      break;
    case 'right':
      headX += 10;
      break;
  }

  const newHead: SnakeSegment = {x: headX, y: headY};

  if (headX === this.food.x && headY === this.food.y) {
    this.score += 10;
    this.food = this.generateFood();
    this.snake = [newHead, ...this.snake];
  } else {
    this.snake = [newHead, ...this.snake.slice(0, -1)];
  }

  this.checkCollision(headX, headY);
}

(三)碰撞检测:游戏结束的判定

checkCollision方法负责判断游戏是否结束。一方面检查蛇头是否撞墙,即坐标是否超出350x350的游戏区域范围(headX < 0 || headX >= 350 || headY < 0 || headY >= 350 );另一方面检查是否撞到自己,通过遍历蛇身(从第二个片段开始),判断蛇头坐标是否与蛇身某片段坐标重合。若满足任一碰撞条件,将gameOver设为true

checkCollision(headX: number, headY: number): void {
  if (headX < 0 || headX >= 350 || headY < 0 || headY >= 350) {
    this.gameOver = true;
  }

  for (let i = 1; i < this.snake.length; i++) {
    if (this.snake[i].x === headX && this.snake[i].y === headY) {
      this.gameOver = true;
      break;
    }
  }
}

(四)游戏循环与重置

gameLoop方法实现游戏的持续运行,利用setTimeout模拟循环:若游戏未结束(!this.gameOver ),调用updateSnakePosition更新蛇的位置,然后递归调用自身,设置200毫秒的间隔控制游戏速度 。resetGame方法用于重置游戏状态,将蛇、食物、方向、游戏状态、分数恢复到初始值,并重新启动游戏循环。

gameLoop(): void {
  if (!this.gameOver) {
    this.updateSnakePosition();
    setTimeout(() => {
      this.gameLoop();
    }, 200); 
  }
}

resetGame(): void {
  this.snake = [{x: 150, y: 150}];
  this.food = this.generateFood();
  this.directionR = 'right';
  this.gameOver = false;
  this.score = 0;
  this.gameLoop();
}

三、界面搭建:给游戏穿上“外衣”

(一)整体布局框架

通过Column组件构建垂直布局的页面结构,设置背景颜色、内边距等样式,确保界面美观且有良好的布局层次。标题“贪吃蛇大冒险”以较大的字体、特定颜色和加粗样式展示,游戏结束时显示“游戏结束!”的提示文本 。

build() {
  Column() {
    Text("贪吃蛇大冒险")
      .fontSize(28)
      .fontColor("#4A90E2")
      .fontWeight(FontWeight.Bold)
      .margin({ top: 20 })
    if (this.gameOver) {
      Text("游戏结束!")
        .fontSize(24)
        .fontColor("#FF4757")
        .margin({ top: 10 })
    }
    // 游戏区域、控制面板、分数与重启按钮等组件...
  }
  .width('100%')
  .height('100%')
  .backgroundColor("#F0F8FF")
  .padding({ left: 10, right: 10, top: 10, bottom: 20 })
}

(二)游戏区域设计

Stack组件作为游戏区域的容器,背景设置为特定颜色和边框样式。内部通过ForEach遍历蛇的身体片段,用Text组件(临时模拟,实际可替换为图片)展示蛇身;同样用Text组件展示食物(以苹果emoji为例),并通过zIndex控制层级,确保蛇和食物在背景之上可见 。onAppear生命周期钩子在组件显示时启动游戏循环。

Stack() {
  ForEach(this.snake, (segment: SnakeSegment) => {
    Text("蛇")
      .width(10)
      .height(10)
      .position({ x: segment.x, y: segment.y })
      .zIndex(1) 
  })

  Text('🍎')
    .width(10)
    .height(10)
    .position({ x: this.food.x, y: this.food.y })
    .zIndex(1) 
}
.backgroundColor("#ffb0d2fc")
.width('350')
.height('350')
.borderWidth(3)
.borderColor("#6CDBD3")
.borderStyle(BorderStyle.Solid)
.onAppear(() => {
  this.gameLoop();
})

(三)控制面板与交互

ColumnRow组件搭建方向控制面板,四个方向按钮(上、下、左、右 )通过Image组件(需确保图片资源存在,实际开发要替换正确路径 )展示,点击事件中通过判断当前方向,防止蛇反向移动(如向上时不能直接向下 )。按钮设置了背景颜色、圆角等样式,提升交互体验 。

Column() {
  Row({ space: 20 }) {
    Image($r("app.media.icon_up"))
      .width(20)
      .height(20)
      .onClick(() => {
        if (this.directionR !== 'down') {
          this.directionR = 'up';
        }
      })
      .width(50)
      .height(50)
      .borderRadius(25)
      .backgroundColor("#FFE4B5")

    Image($r("app.media.icon_down"))
      .width(20)
      .height(20)
      .onClick(() => {
        if (this.directionR !== 'up') {
          this.directionR = 'down';
        }
      })
      .width(50)
      .height(50)
      .borderRadius(25)
      .backgroundColor("#FFE4B5")
  }

  Row({ space: 20 }) {
    Image($r("app.media.icon_left"))
      .width(20)
      .height(20)
      .onClick(() => {
        if (this.directionR !== 'right') {
          this.directionR = 'left';
        }
      })
      .width(50)
      .height(50)
      .borderRadius(25)
      .backgroundColor("#FFE4B5")

    Image($r("app.media.icon_right"))
      .width(20)
      .height(20)
      .onClick(() => {
        if (this.directionR !== 'left') {
          this.directionR = 'right';
        }
      })
      .width(50)
      .height(50)
      .borderRadius(25)
      .backgroundColor("#FFE4B5")
  }
}
.margin({ top: 10, bottom: 10 })

(四)分数与重启功能

Row组件中展示分数信息,游戏结束时显示“重新开始”按钮,点击调用resetGame方法重置游戏。按钮设置了颜色、圆角等样式,方便玩家操作 。

Row() {
  Text(`得分: ${this.score}`)
    .fontSize(20)
    .fontColor("#2ECC71")
    .fontWeight(FontWeight.Bold)
  if (this.gameOver) {
    Text("🔄 重新开始")
      .fontSize(16)
      .fontColor("#FFFFFF")
      .fontWeight(FontWeight.Bold)
      .onClick(() => {
        this.resetGame();
      })
      .width(120)
      .height(40)
      .borderRadius(20)
      .backgroundColor("#3498DB")
      .padding({ left: 10, right: 10 })
  }
}
.justifyContent(FlexAlign.SpaceEvenly)
.margin({ top: 5, bottom: 15 })

四、附源文件

interface SnakeSegment {
  x: number;
  y: number;
}
@Preview
@Component
export struct play_4 {
  // 蛇的主体
  @State snake: Array<SnakeSegment> = [{x: 150, y: 150}];
  // 食物位置
  @State food: SnakeSegment = this.generateFood();
  // 方向控制 (up/down/left/right)
  @State directionR: string = 'right';
  // 游戏状态
  @State gameOver: boolean = false;
  // 分数
  @State score: number = 0;

  // 生成食物
  generateFood(): SnakeSegment {
    const x = Math.floor(Math.random() * 35) * 10; // 根据游戏区域为 350x350
    const y = Math.floor(Math.random() * 35) * 10;
    return {x, y};
  }

  updateSnakePosition(): void {
    // 计算新的蛇头位置
    let headX = this.snake[0].x;
    let headY = this.snake[0].y;

    switch (this.directionR) {
      case 'up':
        headY -= 10;
        break;
      case 'down':
        headY += 10;
        break;
      case 'left':
        headX -= 10;
        break;
      case 'right':
        headX += 10;
        break;
    }

    // 创建新的蛇头
    const newHead: SnakeSegment = {x: headX, y: headY};

    // 检查是否吃到食物
    if (headX === this.food.x && headY === this.food.y) {
      // 增加分数
      this.score += 10;
      // 生成新食物
      this.food = this.generateFood();
      // 蛇身增长
      this.snake = [newHead, ...this.snake];
    } else {
      // 蛇身移动
      this.snake = [newHead, ...this.snake.slice(0, -1)];
    }

    // 检查碰撞
    this.checkCollision(headX, headY);
  }

  checkCollision(headX: number, headY: number): void {
    // 检查是否撞墙
    if (headX < 0 || headX >= 350 || headY < 0 || headY >= 350) {
      this.gameOver = true;
    }

    // 检查是否撞到自己
    for (let i = 1; i < this.snake.length; i++) {
      if (this.snake[i].x === headX && this.snake[i].y === headY) {
        this.gameOver = true;
        break;
      }
    }
  }

  // 重置游戏状态
  resetGame(): void {
    this.snake = [{x: 150, y: 150}];
    this.food = this.generateFood();
    this.directionR = 'right';
    this.gameOver = false;
    this.score = 0;
    this.gameLoop();
  }
  build() {
    Column() {
      // 游戏标题和说明
      Text("贪吃蛇大冒险")
        .fontSize(28)
        .fontColor("#4A90E2")
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20 })
      if (this.gameOver) {
        Text("游戏结束!")
          .fontSize(24)
          .fontColor("#FF4757")
          .margin({ top: 10 })
      }

      // 游戏区域
      Stack() {
        // 蛇的身体
        ForEach(this.snake, (segment: SnakeSegment) => {
         Text("蛇")
            .width(10)
            .height(10)
            .position({ x: segment.x, y: segment.y })
            .zIndex(1) // 确保蛇在食物之上
        })

        // 食物
        Text('🍎')
          .width(10)
          .height(10)
          .position({ x: this.food.x, y: this.food.y })
          .zIndex(1) // 确保食物可见
      }
      .backgroundColor("#ffb0d2fc")
      .width('350')
      .height('350')
      .borderWidth(3)
      .borderColor("#6CDBD3")
      .borderStyle(BorderStyle.Solid)
      .onAppear(() => {
        // 开始游戏循环
        this.gameLoop();
      })

      // 控制面板
      Column() {
        // 方向按钮布局
        Row({ space: 20 }) {
          // 上按钮
            Image($r("app.media.icon_up"))
              .width(20)
              .height(20)

          .onClick(() => {
            if (this.directionR !== 'down') {
              this.directionR = 'up';
            }
          })
          .width(50)
          .height(50)
          .borderRadius(25)
          .backgroundColor("#FFE4B5")

          // 下按钮
            Image($r("app.media.icon_down"))
              .width(20)
              .height(20)
          .onClick(() => {
            if (this.directionR !== 'up') {
              this.directionR = 'down';
            }
          })
          .width(50)
          .height(50)
          .borderRadius(25)
          .backgroundColor("#FFE4B5")
        }

        // 左右按钮行
        Row({ space: 20 }) {
          // 左按钮
            Image($r("app.media.icon_left"))
              .width(20)
              .height(20)
          .onClick(() => {
            if (this.directionR !== 'right') {
              this.directionR = 'left';
            }
          })
          .width(50)
          .height(50)
          .borderRadius(25)
          .backgroundColor("#FFE4B5")

          // 右按钮
            Image($r("app.media.icon_right"))
              .width(20)
              .height(20)
          .onClick(() => {
            if (this.directionR !== 'left') {
              this.directionR = 'right';
            }
          })
          .width(50)
          .height(50)
          .borderRadius(25)
          .backgroundColor("#FFE4B5")
        }
      }
      .margin({ top: 10, bottom: 10 })

      // 分数显示和重新开始按钮
      Row() {
        // 得分显示
        Text(`得分: ${this.score}`)
          .fontSize(20)
          .fontColor("#2ECC71")
          .fontWeight(FontWeight.Bold)
        // 添加重新开始按钮
        if (this.gameOver) {
            Text("🔄 重新开始")
              .fontSize(16)
              .fontColor("#FFFFFF")
              .fontWeight(FontWeight.Bold)
          .onClick(() => {
            this.resetGame();
          })
          .width(120)
          .height(40)
          .borderRadius(20)
          .backgroundColor("#3498DB")
          .padding({ left: 10, right: 10 })
        }
      }
      .justifyContent(FlexAlign.SpaceEvenly)
      .margin({ top: 5, bottom: 15 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor("#F0F8FF")
    .padding({ left: 10, right: 10, top: 10, bottom: 20 })
  }

  gameLoop(): void {
    // 如果游戏未结束,继续游戏循环
    if (!this.gameOver) {
      // 更新蛇的位置
      this.updateSnakePosition();
      // 设置下一帧(使用setTimeout模拟setInterval)
      setTimeout(() => {
        this.gameLoop();
      }, 200); // 修改了游戏速度,从500毫秒调整为200毫秒以提高响应性
    }
  }
}