通过网盘分享的文件:game_flutter_openharmony.zip
链接: https://pan.baidu.com/s/1ryUS1A0zcvXGrDaStu530w 提取码: tqip
前言
上一篇讲了贪吃蛇的蛇头蛇身,这篇来聊聊食物的生成和吃食物的逻辑。
食物是贪吃蛇的目标,吃到食物蛇会变长,分数会增加。食物要随机出现在空地上,不能出现在蛇身上。
这个逻辑看起来简单,但有一些细节需要注意:随机位置可能在蛇身上,需要重新生成;蛇很长时可能需要多次尝试才能找到空位。

食物数据
dart
Point<int> food = const Point(10, 10);
食物也用Point表示,一个坐标点。
初始位置是(10, 10),在棋盘中间。这个位置是安全的,因为初始蛇在(5, 10)附近,不会重叠。
生成食物
dart
void _spawnFood() {
final random = Random();
do {
food = Point(random.nextInt(gridSize), random.nextInt(gridSize));
} while (snake.contains(food));
}
这个方法生成一个新的食物位置,确保不在蛇身上。
Random对象
dart
final random = Random();
创建一个随机数生成器。Random是dart:math包提供的类。
每次调用_spawnFood都创建新的Random对象,这样随机性更好。也可以把random作为成员变量复用。
随机位置
dart
food = Point(random.nextInt(gridSize), random.nextInt(gridSize));
random.nextInt(gridSize)生成0到gridSize-1的随机整数。
gridSize是20,所以食物坐标在0-19范围内,正好覆盖整个棋盘。
避开蛇身
dart
do {
food = Point(...);
} while (snake.contains(food));
用do-while循环,如果随机位置在蛇身上,就重新生成。
直到找到一个不在蛇身上的位置。
snake.contains(food)检查food是否在snake列表中。Point类重写了==运算符,所以可以正确比较。
为什么用do-while
do-while至少执行一次循环体,然后检查条件。
这里需要先生成一个位置,再检查是否合法,所以用do-while。
如果用while,需要先初始化food为一个在蛇身上的值,比较麻烦:
dart
// 用while的写法,比较啰嗦
food = snake.first; // 先设为蛇头,肯定在蛇身上
while (snake.contains(food)) {
food = Point(random.nextInt(gridSize), random.nextInt(gridSize));
}
do-while更简洁。
食物的渲染
dart
bool isFood = food == p;
...
color: ... isFood ? Colors.red : Colors.grey[850],
borderRadius: BorderRadius.circular(isFood ? 8 : 2),
食物的渲染在GridView.builder的itemBuilder里,和蛇身一起处理。
判断是否是食物
dart
bool isFood = food == p;
比较当前格子坐标p和食物坐标food是否相等。Point类重写了==运算符,可以直接比较。
红色
食物用红色,和绿色的蛇形成对比,很醒目。
红色在游戏中通常表示"目标"或"奖励",玩家一眼就能看到食物在哪里。
大圆角
dart
borderRadius: BorderRadius.circular(isFood ? 8 : 2),
食物用8像素圆角,看起来像个圆形(苹果?)。
蛇身用2像素圆角,是方块。
这样视觉上能区分食物和蛇。圆形的食物更像是可以"吃"的东西。
吃食物
dart
snake.insert(0, newHead);
if (newHead == food) {
score += 10;
_spawnFood();
} else {
snake.removeLast();
}
这段代码在_update方法里,处理蛇移动后的逻辑。
先插入新蛇头
dart
snake.insert(0, newHead);
不管有没有吃到食物,都先把新蛇头插入列表头部。
判断吃到
dart
if (newHead == food) {
新蛇头位置和食物位置相同,就是吃到了。
Point类重写了==运算符,可以直接比较两个Point是否相等。
加分
dart
score += 10;
每吃一个食物加10分。可以根据游戏难度调整分数。
生成新食物
dart
_spawnFood();
吃掉后立即生成新食物,游戏继续。
蛇变长
注意这里没有 snake.removeLast()。
正常移动时,头部增加尾部删除,蛇长度不变。
吃到食物时,头部增加但不删除尾部,蛇就变长了一节。
这是贪吃蛇变长的核心逻辑,非常简洁。
没吃到时删除尾部
dart
} else {
snake.removeLast();
}
没吃到食物,删除尾部,蛇长度不变。这就是正常的移动。
分数显示
dart
Padding(padding: const EdgeInsets.all(8), child: Text('分数: $score', style: const TextStyle(fontSize: 20))),
顶部显示当前分数,让玩家知道自己的成绩。
字符串插值
dart
'分数: $score'
Dart的字符串插值,$score会被替换成score变量的值。
样式
dart
style: const TextStyle(fontSize: 20)
20号字体,大小适中,不会太抢眼但也能看清。
游戏速度
dart
timer = Timer.periodic(const Duration(milliseconds: 200), (_) => _update());
每200毫秒移动一次,也就是每秒5次。
难度递增
可以根据分数加快速度:
dart
int speed = 200 - (score ~/ 50) * 20; // 每50分加速20毫秒
speed = speed.clamp(100, 200); // 最快100毫秒
timer = Timer.periodic(Duration(milliseconds: speed), (_) => _update());
每50分加速20毫秒,从200毫秒最快到100毫秒。
clamp(100, 200)确保速度在100-200范围内,不会太快或太慢。
但当前实现简化了,速度固定。如果要实现难度递增,需要在吃到食物后重新创建定时器。
边界处理
dart
Point<int> newHead = Point((snake.first.x + direction.x) % gridSize, (snake.first.y + direction.y) % gridSize);
% gridSize实现穿墙效果:
- x超过19变成0(从右边出去,左边进来)
- x小于0变成19(从左边出去,右边进来)
- y同理
这样蛇不会撞墙死亡,只会撞到自己死亡。
取模运算的妙用
取模运算%可以实现循环:
- 20 % 20 = 0
- 21 % 20 = 1
- -1 % 20 = 19(在Dart中)
所以不管坐标怎么变化,结果都在0-19范围内。
如果想要撞墙死亡
如果想要撞墙死亡,改成:
dart
int nx = snake.first.x + direction.x;
int ny = snake.first.y + direction.y;
if (nx < 0 || nx >= gridSize || ny < 0 || ny >= gridSize) {
gameOver = true;
timer?.cancel();
return;
}
Point<int> newHead = Point(nx, ny);
先检查是否出界,出界就游戏结束。
食物生成的边界情况
蛇很长时
当蛇占据了大部分格子,随机生成食物可能需要很多次尝试。
极端情况下,蛇占满整个棋盘(20x20=400格),就没地方放食物了。
可以加个检查:
dart
void _spawnFood() {
if (snake.length >= gridSize * gridSize) {
// 蛇占满了,游戏胜利
gameOver = true;
timer?.cancel();
return;
}
// ... 正常生成逻辑
}
但实际上很难玩到这个程度。400格的蛇需要吃397个食物(初始3格),按每秒5次移动,至少需要几分钟不出错。
更高效的方法
如果追求效率,可以维护一个空位列表:
dart
List<Point<int>> emptySpaces = [];
for (int x = 0; x < gridSize; x++) {
for (int y = 0; y < gridSize; y++) {
Point<int> p = Point(x, y);
if (!snake.contains(p)) emptySpaces.add(p);
}
}
food = emptySpaces[random.nextInt(emptySpaces.length)];
这样只需要一次随机,不需要循环重试。
但对于20x20的棋盘,do-while方法已经够快了。蛇长度通常不会超过100,空位还有300多个,平均一两次就能找到。
初始化时生成食物
dart
void _initGame() {
snake = [const Point(5, 10), const Point(4, 10), const Point(3, 10)];
direction = const Point(1, 0);
score = 0;
gameOver = false;
_spawnFood();
timer?.cancel();
timer = Timer.periodic(const Duration(milliseconds: 200), (_) => _update());
}
游戏开始时调用_spawnFood(),确保食物不在初始蛇身上。
初始蛇在(3,10)、(4,10)、(5,10),食物随机生成后肯定不在这三个位置。
特殊食物
可以加入不同类型的食物:
dart
enum FoodType { normal, bonus, speed }
class Food {
final Point<int> position;
final FoodType type;
Food(this.position, this.type);
}
Food food = Food(const Point(10, 10), FoodType.normal);
void _spawnFood() {
final random = Random();
Point<int> pos;
do {
pos = Point(random.nextInt(gridSize), random.nextInt(gridSize));
} while (snake.contains(pos));
// 10%概率生成奖励食物,5%概率生成加速食物
FoodType type;
double chance = random.nextDouble();
if (chance < 0.05) {
type = FoodType.speed;
} else if (chance < 0.15) {
type = FoodType.bonus;
} else {
type = FoodType.normal;
}
food = Food(pos, type);
}
不同食物有不同效果:
- normal: 普通食物,+10分
- bonus: 奖励食物,+50分
- speed: 加速食物,暂时加快速度
食物颜色
dart
Color getFoodColor(FoodType type) {
switch (type) {
case FoodType.normal: return Colors.red;
case FoodType.bonus: return Colors.yellow;
case FoodType.speed: return Colors.purple;
}
}
不同类型用不同颜色,让玩家一眼就能区分。
当前实现简化了,只有普通食物。
食物动画
可以让食物有呼吸效果:
dart
class _FoodWidget extends StatefulWidget {
@override
_FoodWidgetState createState() => _FoodWidgetState();
}
class _FoodWidgetState extends State<_FoodWidget> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
)..repeat(reverse: true);
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: 0.8 + _controller.value * 0.2,
child: Container(
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
);
},
);
}
}
食物会在0.8到1.0之间缩放,有一个"呼吸"的效果,更吸引注意力。
当前实现简化了,食物是静态的。
音效
吃到食物时可以播放音效:
dart
import 'package:audioplayers/audioplayers.dart';
final AudioPlayer _player = AudioPlayer();
void _playEatSound() {
_player.play(AssetSource('sounds/eat.mp3'));
}
在吃到食物的逻辑里调用_playEatSound()。
需要添加audioplayers依赖和音效文件。当前实现简化了。
障碍物
可以加入障碍物增加难度:
dart
List<Point<int>> obstacles = [];
void _generateObstacles() {
final random = Random();
obstacles.clear();
// 生成5个障碍物
for (int i = 0; i < 5; i++) {
Point<int> pos;
do {
pos = Point(random.nextInt(gridSize), random.nextInt(gridSize));
} while (snake.contains(pos) || pos == food || obstacles.contains(pos));
obstacles.add(pos);
}
}
碰撞检测时也要检查障碍物:
dart
if (snake.skip(1).contains(newHead) || obstacles.contains(newHead)) {
gameOver = true;
timer?.cancel();
return;
}
障碍物用灰色或黑色显示,和蛇、食物区分开。
当前实现简化了,没有障碍物。
小结
这篇讲了贪吃蛇的食物生成,核心知识点:
- Random.nextInt: 生成随机整数,范围是0到参数-1
- do-while循环: 确保食物不在蛇身上,至少执行一次
- Point比较: ==运算符判断是否吃到,Point类已重写
- 蛇变长: 吃到食物时不删除尾部,简洁的实现
- 红色圆形: 食物的视觉设计,和蛇形成对比
- 穿墙效果: %运算符实现边界循环
- 分数显示: 字符串插值显示当前分数
- 特殊食物: 不同类型不同效果,增加趣味性
- 食物动画: 呼吸效果吸引注意力
- 障碍物: 增加难度的可选功能
食物是贪吃蛇的核心元素,生成和吃的逻辑都很简单,但组合起来就是完整的游戏。好的游戏设计往往是简单规则的巧妙组合。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net