🐍 Flutter + HarmonyOS 实战:开发经典贪吃蛇游戏


在这里插入图片描述
在这里插入图片描述

📋 文章导读

章节 内容概要 预计阅读
贪吃蛇游戏规则与设计 3分钟
核心数据结构设计 5分钟
游戏循环与移动算法 10分钟
碰撞检测与游戏状态 6分钟
UI渲染与交互设计 8分钟
完整源码与运行 3分钟

💡 写在前面:贪吃蛇是一款承载了无数人童年回忆的经典游戏。从诺基亚手机到现代智能设备,它的魅力经久不衰。本文将带你用Flutter从零实现这款经典游戏,深入讲解游戏循环、碰撞检测、状态管理等核心技术点。


一、游戏规则与设计

1.1 贪吃蛇游戏规则

移动规则
蛇会持续向当前方向移动,玩家可以改变方向但不能反向
进食规则
吃到食物后蛇身变长,分数增加,新食物随机生成
死亡条件
撞到墙壁或撞到自己的身体,游戏结束
难度递增
随着分数增加,蛇的移动速度会逐渐加快

1.2 功能设计

贪吃蛇游戏

核心玩法

自动移动

方向控制

吃食物长大

碰撞检测

游戏状态

开始/暂停

游戏结束

分数统计

速度递增

交互方式

触屏滑动

键盘控制

方向按钮

视觉效果

蛇身渐变

食物光晕

网格背景

1.3 游戏参数设计

参数 说明
棋盘大小 20×20 400个格子
初始长度 3节 蛇的初始长度
初始速度 200ms 每次移动间隔
最快速度 100ms 速度上限
加速条件 每50分 速度减少20ms
食物分值 10分 每个食物得分

二、核心数据结构

2.1 坐标表示

使用 Point<int> 表示二维坐标:

import 'dart:math';

// 蛇身体:列表存储,头部在末尾
List<Point<int>> snake = [];

// 食物位置
Point<int>? food;

// 初始化蛇(3节,在中间位置)
snake = [
  Point(cols ~/ 2 - 2, rows ~/ 2),  // 尾部
  Point(cols ~/ 2 - 1, rows ~/ 2),  // 身体
  Point(cols ~/ 2, rows ~/ 2),      // 头部
];

2.2 方向枚举

enum Direction { up, down, left, right }

// 当前方向
Direction direction = Direction.right;

// 下一个方向(防止快速按键导致反向)
Direction nextDirection = Direction.right;

2.3 蛇的数据结构图解

蛇的移动过程(向右):

移动前:
  [0]    [1]    [2]
  尾部 → 身体 → 头部
  (3,5)  (4,5)  (5,5)

移动后(未吃食物):
  [0]    [1]    [2]
  尾部 → 身体 → 头部
  (4,5)  (5,5)  (6,5)  ← 新头部
  ↑
  原尾部被移除

移动后(吃到食物):
  [0]    [1]    [2]    [3]
  尾部 → 身体 → 身体 → 头部
  (3,5)  (4,5)  (5,5)  (6,5)  ← 新头部
  ↑
  尾部保留,蛇变长

三、游戏循环与移动算法

3.1 游戏循环

贪吃蛇的核心是一个定时器驱动的游戏循环:

游戏开始

启动定时器

每隔N毫秒

更新方向

计算新头部位置

检查碰撞?

游戏结束

添加新头部

吃到食物?

加分 + 生成新食物

移除尾部

检查是否加速

3.2 定时器实现

Timer? gameTimer;
int speed = 200;  // 毫秒

void _startGame() {
  gameTimer?.cancel();
  gameTimer = Timer.periodic(Duration(milliseconds: speed), (_) {
    _moveSnake();
  });
}

void _pauseGame() {
  gameTimer?.cancel();
}

3.3 移动算法

void _moveSnake() {
  if (!isPlaying) return;

  setState(() {
    // 1. 更新方向
    direction = nextDirection;

    // 2. 计算新头部位置
    final head = snake.last;
    Point<int> newHead;

    switch (direction) {
      case Direction.up:
        newHead = Point(head.x, head.y - 1);
        break;
      case Direction.down:
        newHead = Point(head.x, head.y + 1);
        break;
      case Direction.left:
        newHead = Point(head.x - 1, head.y);
        break;
      case Direction.right:
        newHead = Point(head.x + 1, head.y);
        break;
    }

    // 3. 检查碰撞
    if (_checkCollision(newHead)) {
      _endGame();
      return;
    }

    // 4. 添加新头部
    snake.add(newHead);

    // 5. 检查是否吃到食物
    if (newHead == food) {
      score += 10;
      _generateFood();
      // 不移除尾部,蛇变长
    } else {
      // 移除尾部,蛇长度不变
      snake.removeAt(0);
    }
  });
}

3.4 方向变化的坐标计算

方向 X变化 Y变化 说明
↑ 上 0 -1 Y坐标减小
↓ 下 0 +1 Y坐标增大
← 左 -1 0 X坐标减小
→ 右 +1 0 X坐标增大

3.5 防止反向移动

void _changeDirection(Direction newDirection) {
  // 防止反向移动(会导致立即撞到自己)
  if ((direction == Direction.up && newDirection == Direction.down) ||
      (direction == Direction.down && newDirection == Direction.up) ||
      (direction == Direction.left && newDirection == Direction.right) ||
      (direction == Direction.right && newDirection == Direction.left)) {
    return;  // 忽略反向操作
  }

  nextDirection = newDirection;
}

⚠️ 为什么用 nextDirection?

如果直接修改 direction,在两次移动之间快速按两次键可能导致反向。
使用 nextDirection 缓存,在下次移动时才生效,避免这个问题。


四、碰撞检测与游戏状态

4.1 碰撞检测

bool _checkCollision(Point<int> head) {
  // 1. 撞墙检测
  if (head.x < 0 || head.x >= cols || 
      head.y < 0 || head.y >= rows) {
    return true;
  }

  // 2. 撞自己检测(不包括尾部)
  // 为什么不包括尾部?因为移动时尾部会移走
  for (int i = 0; i < snake.length - 1; i++) {
    if (snake[i] == head) {
      return true;
    }
  }

  return false;
}

4.2 碰撞类型图解

撞墙:
┌────────────────────┐
│                    │
│        🐍→         │ ← 蛇头超出边界
│                    │
└────────────────────┘

撞自己:
    ┌──┐
    │  ↓
    │  🐍←──┐
    │       │
    └───────┘
    蛇头撞到身体

4.3 食物生成

void _generateFood() {
  // 收集所有空白位置
  List<Point<int>> emptySpaces = [];

  for (int x = 0; x < cols; x++) {
    for (int y = 0; y < rows; y++) {
      final point = Point(x, y);
      // 排除蛇身占据的位置
      if (!snake.contains(point)) {
        emptySpaces.add(point);
      }
    }
  }

  // 随机选择一个空白位置
  if (emptySpaces.isNotEmpty) {
    food = emptySpaces[random.nextInt(emptySpaces.length)];
  }
}

4.4 难度递增

// 每50分加速一次
if (score % 50 == 0 && speed > 100) {
  speed -= 20;  // 速度减少20ms
  
  // 重启定时器
  gameTimer?.cancel();
  gameTimer = Timer.periodic(Duration(milliseconds: speed), (_) {
    _moveSnake();
  });
}
分数 速度 难度
0-40 200ms
50-90 180ms ⭐⭐
100-140 160ms ⭐⭐⭐
150-190 140ms ⭐⭐⭐⭐
200-240 120ms ⭐⭐⭐⭐⭐
250+ 100ms 🔥极限

五、UI渲染与交互

5.1 整体布局

游戏区域

分数板

Body - Column

Scaffold

AppBar - 标题 + 重开

Body

分数板

游戏区域

控制按钮

当前分数

最高分

蛇长度

网格背景

蛇和食物 CustomPaint

状态遮罩

5.2 CustomPainter 绘制蛇和食物

class SnakePainter extends CustomPainter {
  final List<Point<int>> snake;
  final Point<int>? food;
  final int cols, rows;

  
  void paint(Canvas canvas, Size size) {
    final cellWidth = size.width / cols;
    final cellHeight = size.height / rows;

    // 绘制食物(带光晕效果)
    if (food != null) {
      final center = Offset(
        food!.x * cellWidth + cellWidth / 2,
        food!.y * cellHeight + cellHeight / 2,
      );
      
      // 光晕
      canvas.drawCircle(center, cellWidth * 0.45,
        Paint()..color = Colors.red.withOpacity(0.3));
      // 本体
      canvas.drawCircle(center, cellWidth * 0.35,
        Paint()..color = Colors.red);
    }

    // 绘制蛇(渐变色)
    for (int i = 0; i < snake.length; i++) {
      final segment = snake[i];
      final isHead = i == snake.length - 1;

      // 渐变色:尾部深,头部浅
      final color = isHead
          ? Colors.green.shade400
          : Color.lerp(
              Colors.green.shade700,
              Colors.green.shade400,
              i / snake.length,
            )!;

      final rect = RRect.fromRectAndRadius(
        Rect.fromLTWH(
          segment.x * cellWidth + 1,
          segment.y * cellHeight + 1,
          cellWidth - 2,
          cellHeight - 2,
        ),
        const Radius.circular(4),
      );

      canvas.drawRRect(rect, Paint()..color = color);

      // 蛇头眼睛
      if (isHead) {
        _drawEyes(canvas, segment, cellWidth, cellHeight);
      }
    }
  }
}

5.3 交互方式

方式 操作 说明
键盘 ↑↓←→ / WASD 方向控制
键盘 空格 开始/暂停
触屏 滑动 方向控制
按钮 点击方向键 方向控制

5.4 手势识别

GestureDetector(
  onVerticalDragUpdate: (details) {
    if (details.delta.dy < -5) {
      _changeDirection(Direction.up);
    } else if (details.delta.dy > 5) {
      _changeDirection(Direction.down);
    }
  },
  onHorizontalDragUpdate: (details) {
    if (details.delta.dx < -5) {
      _changeDirection(Direction.left);
    } else if (details.delta.dx > 5) {
      _changeDirection(Direction.right);
    }
  },
  child: // 游戏区域
)

六、完整源码与运行

6.1 项目结构

flutter_snake/
├── lib/
│   └── main.dart       # 贪吃蛇游戏代码(约400行)
├── ohos/               # 鸿蒙平台配置
├── pubspec.yaml        # 依赖配置
└── README.md           # 项目说明

6.2 运行命令

# 获取依赖
flutter pub get

# 运行游戏
flutter run

# 运行到鸿蒙设备
flutter run -d ohos

6.3 功能清单

功能 状态 说明
20×20游戏区域 400个格子
自动移动 定时器驱动
方向控制 键盘+触屏+按钮
吃食物长大 核心玩法
碰撞检测 撞墙+撞自己
分数统计 实时更新
最高分记录 本次游戏内
难度递增 每50分加速
开始/暂停 空格键控制
蛇身渐变 视觉效果
蛇头眼睛 可爱细节

七、扩展方向

7.1 功能扩展

贪吃蛇

穿墙模式

双人对战

道具系统

关卡模式

排行榜

从一边出另一边进

分屏对战

加速/减速/无敌

障碍物关卡

7.2 穿墙模式实现

// 修改碰撞检测,实现穿墙
Point<int> _wrapPosition(Point<int> head) {
  int x = head.x;
  int y = head.y;
  
  if (x < 0) x = cols - 1;
  if (x >= cols) x = 0;
  if (y < 0) y = rows - 1;
  if (y >= rows) y = 0;
  
  return Point(x, y);
}

7.3 道具系统设计

道具 效果 持续时间
🚀 加速 移动速度×2 5秒
🐢 减速 移动速度÷2 5秒
⭐ 无敌 不会撞死 3秒
💎 双倍分 得分×2 10秒
✂️ 缩短 蛇身减半 立即

八、常见问题

Q1: 为什么蛇头在列表末尾而不是开头?

这是为了优化性能:

  • 添加头部:list.add() 是 O(1) 操作
  • 移除尾部:list.removeAt(0) 是 O(n) 操作

虽然移除尾部是 O(n),但如果头部在开头,添加头部就变成 O(n) 了。
考虑到蛇通常不会太长,这个设计是合理的。

如果追求极致性能,可以使用双端队列 Queue

Q2: 为什么用 Timer 而不是 AnimationController?

两者都可以实现游戏循环:

  • Timer.periodic:简单直接,适合固定间隔的逻辑更新
  • AnimationController:更适合需要平滑动画的场景

贪吃蛇是离散移动(一格一格跳),用 Timer 更合适。
如果要实现平滑移动动画,可以结合 AnimationController。

Q3: 如何实现蛇的平滑移动动画?

可以使用插值动画:

// 记录每个蛇节的目标位置和当前位置
// 使用 AnimationController 驱动插值
// 每帧计算当前位置 = lerp(旧位置, 新位置, 动画进度)

这会增加不少复杂度,但视觉效果会更好。


九、总结

本文从零实现了经典的贪吃蛇游戏,核心技术点包括:

  1. 游戏循环:Timer.periodic 驱动定时更新
  2. 移动算法:头部添加 + 尾部移除
  3. 碰撞检测:边界检测 + 自身碰撞
  4. 方向控制:防反向 + 多种输入方式
  5. 难度递增:分数驱动速度变化
  6. 视觉效果:CustomPainter 绘制渐变蛇身

贪吃蛇虽然简单,但包含了游戏开发的核心要素。希望这篇文章能帮你理解游戏循环和状态管理的基本思想!


🐍 完整源码已上传,欢迎Star支持!

--- **欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net**
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐