基于Arduino的贪吃蛇游戏开发:核心逻辑实现

本文是”基于Arduino的贪吃蛇游戏开发”系列的第二篇,将深入探讨游戏的核心逻辑实现。在上一篇中,我们完成了硬件搭建和基础显示功能,现在让我们让这个游戏真正”活”起来!

游戏核心架构

贪吃蛇游戏的核心逻辑可以分为四个主要部分:

  1. 蛇的移动与转向控制
  2. 食物生成与碰撞检测
  3. 游戏状态管理
  4. 用户输入处理

让我们逐一分析这些关键组件在代码中的实现。

1. 蛇的移动与转向控制

方向表示与移动逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 方向定义:
// 1 = 右, 2 = 下, 3 = 左, 4 = 上
volatile int direction = 1;
volatile int old_direction = 1;

void snack_next() {
// 根据当前方向移动蛇头
if (direction == 2) snack_head[0]++; // 向下
else if (direction == 4) snack_head[0]--; // 向上
else if (direction == 1) snack_head[1]++; // 向右
else if (direction == 3) snack_head[1]--; // 向左

// 检查是否吃到食物
if (snack_head[0] == food[0] && snack_head[1] == food[1]) {
score++;
snack_body[score - 1][0] = snack_head[0];
snack_body[score - 1][1] = snack_head[1];
food_summon(); // 生成新食物
} else {
// 移动蛇身(去除尾部,添加新头部)
for (int i = 0; i < score - 1; i++) {
snack_body[i][0] = snack_body[i + 1][0];
snack_body[i][1] = snack_body[i + 1][1];
}
snack_body[score - 1][0] = snack_head[0];
snack_body[score - 1][1] = snack_head[1];
}

// ...碰撞检测代码...
}

这段代码实现了贪吃蛇的核心移动逻辑:

  • 方向控制:使用简单的整数表示方向(1-右,2-下,3-左,4-上)
  • 吃到食物:当蛇头与食物位置重合时,蛇身长度增加
  • 正常移动:蛇身向前移动一节,尾部消失,头部新增一节

转向限制机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void control() {
// ...初始化代码...

while (millis() - time_ < 500) {
// ...按钮检测...

// 左转按钮处理
if (digitalRead(3)) {
if (button_ != 1 && s3) {
// 防止180度直接转向
if (old_direction == 1) { direction = 4; }
else { direction = old_direction - 1; }
}
button_ = 1;
}
// 右转按钮处理
else if (digitalRead(2)) {
if (button_ != -1 && s2) {
// 防止180度直接转向
if (old_direction == 4) { direction = 1; }
else { direction = old_direction + 1; }
}
button_ = -1;
}
}
}

这段代码实现了两个重要功能:

  1. 500ms时间窗口:限制玩家在500ms内只能进行一次转向操作
  2. 方向限制:防止蛇进行180度直接转向(如从右直接转左)

2. 食物生成与碰撞检测

智能食物生成算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void food_summon() {
int x, y;
bool m;
do {
m = false;
// 随机生成食物位置
x = random(1, w);
y = random(1, h);

// 检查是否与蛇头重叠
if (x == snack_head[1] && y == snack_head[0]) {
m = true;
continue;
}

// 检查是否与蛇身重叠
for (int i = 0; i < score - 1; i++) {
if (x == snack_body[i][1] && y == snack_body[i][0]) {
m = true;
break;
}
}
} while (m); // 循环直到找到有效位置

food[1] = x;
food[0] = y;
}

这个算法确保食物不会出现在蛇身或蛇头的位置:

  1. 使用do-while循环代替递归,避免栈溢出风险
  2. 随机生成位置后检查是否与蛇的任何部分重叠
  3. 如果重叠则重新生成,直到找到有效位置

碰撞检测系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void snack_next() {
// ...移动逻辑...

// 自碰撞检测
for (int i = 0; i < score - 1; i++) {
if (snack_head[0] == snack_body[i][0] &&
snack_head[1] == snack_body[i][1]) {
game_over = true; // 蛇头碰到蛇身
}
}

// 边界碰撞检测
if (snack_head[1] == 0 || snack_head[0] == 0 ||
snack_head[1] == w + 1 || snack_head[0] == h + 1) {
game_over = true; // 蛇头碰到边界
}
}

碰撞检测分为两部分:

  1. 自碰撞:遍历蛇身,检查蛇头是否与任何身体部分重合
  2. 边界碰撞:检查蛇头是否超出游戏区域边界(1-8)

3. 游戏状态管理

游戏结束处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void loop() {
// ...主游戏循环...

if (game_over) {
// 游戏结束动画(闪烁效果)
for (int i = 0; i < 5; i++) {
P.displayClear();
mx.clear();
delay(250);
show(); // 显示当前状态
mx.setBuffer(15, 8, game_over_bitmap); // 显示游戏结束图标
delay(250);
}

// 等待重启
while (true) {
delay(250);
if (digitalRead(3) || digitalRead(2)) {
reset(); // 重置游戏
break;
}
}
}
}

游戏结束流程:

  1. 显示闪烁动画(交替显示当前状态和游戏结束图标)
  2. 进入等待循环,检测按钮输入
  3. 当玩家按下任意按钮时,重置游戏

游戏重置功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void reset() {
// 重置蛇身数组
for (int i = 0; i < 64; i++) {
snack_body[i][0] = 0;
snack_body[i][1] = 0;
}

// 重置食物位置
food[0] = 4;
food[1] = 6;

// 重置蛇头位置
snack_head[0] = 4;
snack_head[1] = 3;

// 重置游戏状态
score = 3;
direction = 1;
old_direction = 1;
game_over = false;

// 初始化蛇身
snack_body[0][0] = 4; snack_body[0][1] = 1;
snack_body[1][0] = 4; snack_body[1][1] = 2;
snack_body[2][0] = 4; snack_body[2][1] = 3;
}

重置功能将所有游戏状态恢复为初始值,确保玩家可以重新开始游戏。

4. 用户输入处理

按钮检测机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void control() {
time_ = millis();
button_ = 0;
old_direction = direction;

bool s3 = false, s2 = false; // 按钮按下状态标志

// 500ms控制窗口
while (millis() - time_ < 500) {
// 检测按钮按下
if (!digitalRead(3)) s3 = true;
if (!digitalRead(2)) s2 = true;

// 检测按钮释放(左转按钮)
if (digitalRead(3)) {
if (button_ != 1 && s3) {
// 处理转向逻辑
}
button_ = 1;
}
// 检测按钮释放(右转按钮)
else if (digitalRead(2)) {
if (button_ != -1 && s2) {
// 处理转向逻辑
}
button_ = -1;
}
}
}

这段代码实现了一个巧妙的按钮检测机制:

  1. 按下检测:当按钮被按下时设置标志(s2/s3)
  2. 释放检测:当按钮被释放时执行转向操作
  3. 状态锁定:使用button_变量防止同一按钮多次触发

游戏循环流程

整个游戏的主循环流程如下:

流程图

优化与改进建议

虽然当前实现已经相当完整,但仍有优化空间:

  1. 使用枚举提升可读性

    1
    2
    enum Direction { RIGHT = 1, DOWN, LEFT, UP };
    volatile Direction direction = RIGHT;
  2. 增加游戏难度

    1
    2
    int speed = max(100, 500 - score * 20); // 随分数增加速度
    while (millis() - time_ < speed) { ... }
  3. 添加音效反馈

    1
    2
    3
    4
    5
    void playTone(int freq, int duration) {
    tone(speakerPin, freq, duration);
    }
    // 在吃到食物时调用
    playTone(523, 150); // C5音调

总结与下期预告

在本章中,我们深入探讨了贪吃蛇游戏的核心逻辑实现,包括:

  • 蛇的移动与转向控制
  • 食物生成与碰撞检测
  • 游戏状态管理与结束处理
  • 用户输入处理机制

这些核心组件共同构成了一个完整可玩的贪吃蛇游戏。在下一篇中,我们将:

  1. 优化游戏性能与显示效果
  2. 添加游戏难度分级
  3. 实现高分记录功能
  4. 解决现有实现中的边界情况问题

项目源码更新:本文涉及的优化代码已更新到GitHub仓库 Arduino-Snake-Game

挑战任务:尝试添加游戏暂停功能,当玩家同时按下两个按钮时暂停游戏,再次按下时继续。这将帮助你深入理解游戏状态管理!