一、项目缘起:对鸿蒙应用开发的探索
在鸿蒙系统生态逐渐丰富的当下,我一直想尝试开发一款简单又有趣的应用,以此深入了解鸿蒙应用开发的流程与特性。贪吃蛇作为经典游戏,规则易懂、逻辑清晰,非常适合用来实践。于是,基于鸿蒙的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; // 根据游戏区域为 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毫秒以提高响应性
}
}
}