纯血Harmony NETX 5小游戏实践:贪吃蛇(附源文件)
一、项目缘起:对鸿蒙应用开发的探索
在鸿蒙系统生态逐渐丰富的当下,我一直想尝试开发一款简单又有趣的应用,以此深入了解鸿蒙应用开发的流程与特性。贪吃蛇作为经典游戏,规则易懂、逻辑清晰,非常适合用来实践。于是,基于鸿蒙的ArkTS语言,开启了这款“贪吃蛇大冒险”小游戏的开发之旅。
二、核心逻辑拆解:构建游戏的“大脑”
(一)数据结构与初始状态
首先定义SnakeSegment
接口,用于描述蛇的身体片段和食物的坐标信息,包含x
和y
两个数值属性。游戏初始时,蛇的主体snake
是一个包含单个片段(初始位置{x: 150, y: 150}
)的数组;食物food
通过generateFood
方法随机生成,其坐标范围基于350x350的游戏区域,确保在合理范围内;方向控制directionR
初始为right
,游戏状态gameOver
为false
,分数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();
})
(三)控制面板与交互
用Column
和Row
组件搭建方向控制面板,四个方向按钮(上、下、左、右 )通过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; // 根据游戏区域为 350x350const 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毫秒以提高响应性}}
}