Flutter for OpenHarmony游戏集合App实战之贪吃蛇食物生成

通过网盘分享的文件: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

相关推荐
不会写代码0002 小时前
Flutter 框架跨平台鸿蒙开发 - 全国景区门票查询应用开发教程
flutter·华为·harmonyos
Lhuu(重开版2 小时前
JS:正则表达式和作用域
开发语言·javascript·正则表达式
仙俊红3 小时前
Java Map 家族核心解析
java·开发语言
浅念-3 小时前
C语言小知识——指针(3)
c语言·开发语言·c++·经验分享·笔记·学习·算法
kirk_wang3 小时前
Flutter艺术探索-Riverpod深度解析:新一代状态管理方案
flutter·移动开发·flutter教程·移动开发教程
猛扇赵四那边好嘴.3 小时前
Flutter 框架跨平台鸿蒙开发 - 旅行规划助手应用开发教程
flutter·华为·harmonyos
紫雾凌寒4 小时前
【 HarmonyOS 面试题】2026 最新 ArkTS 语言基础面试题
华为·面试·程序员·华为云·职场发展·harmonyos·arkts
code_li4 小时前
聊聊支付宝架构
java·开发语言·架构
2501_937145414 小时前
神马影视8.8版2026最新版:核心技术升级与多场景适配解析
android·源码·电视盒子·源代码管理