简介
在完成了 Tab5 上的 IMU 实时数据可视化之后,我终于可以开始最初的目标了:基于 IMU 操作的贪吃蛇游戏。和传统按键或触摸不同,这次我希望整个游戏完全通过设备的倾斜来控制方向,也就是说:向右倾斜 → 蛇向右、向左倾斜 → 蛇向左、向前 / 向后倾斜 → 上下移动。这样既能充分利用 Tab5 内置的 IMU,又能让游戏的交互方式更直观。 整个项目仍然基于 Arduino 环境 + M5Unified + M5GFX,不依赖额外外设。这次的代码稍微比较复杂了一点。因为我借助了微软的Copilot来完成了这次的游戏编写。整个编写和调试一共花了一个晚上。
视频效果如下
游戏整体设计思路
为了简化逻辑,我将屏幕划分为固定大小的网格:
const int GRID_SIZE = 10; WIDTH = M5.Display.width() / GRID_SIZE; HEIGHT = M5.Display.height() / GRID_SIZE;
蛇本身使用一个非常直观的结构体表示:
struct Snake
{
vector<pair<int, int>> body;
int dir; // 0: up, 1: right, 2: down, 3: left
};这里使用了一个向量(数组),数组内保存的是一对数据。即蛇身体的坐标。蛇头始终位于 vector 的第一个元素。
在 initGame() 中完成所有状态的重置:蛇从屏幕中央开始、初始方向为向右、随机生成食物、清空分数和 Game Over 状态
snake.body.push_back({WIDTH / 2, HEIGHT / 2});
snake.dir = 1;食物生成与碰撞判断:食物的位置是随机生成的,但必须确保食物不能生成在蛇身上
因此在生成时加入了判断:
do
{
food.first = random(0, WIDTH);
food.second = random(0, HEIGHT);
} while (isSnake(food.first, food.second));游戏绘制逻辑
为了逻辑简单,采用的是整屏重绘:清屏、绘制蛇、绘制食物、绘制分数
M5.Display.clear(TFT_BLACK);
蛇身和食物都直接用 fillRect() 绘制,颜色区分明确 :蛇:绿色 食物:红色
Game Over 提示当游戏结束时,屏幕中央会显示:Game Over! Tilt to restart 提示玩家通过倾斜设备重新开始游戏。

核心逻辑:蛇的移动与碰撞
每一次更新都会执行以下步骤:
1-根据方向计算新的蛇头位置
2-检查是否撞墙
3-检查是否撞到自己
4-插入新蛇头
5-判断是否吃到食物
如果没吃到食物,就移除蛇尾,保持长度不变。
void updateGame()
{
if (gameOver)
return;
// Move snake
pair<int, int> head = snake.body.front();
switch (snake.dir)
{
case 0:
head.second--;
break; // up
case 1:
head.first++;
break; // right
case 2:
head.second++;
break; // down
case 3:
head.first--;
break; // left
}
// Check wall collision
if (head.first < 0 || head.first >= WIDTH || head.second < 0 || head.second >= HEIGHT)
{
gameOver = true;
return;
}
// Check self collision
if (isSnake(head.first, head.second))
{
gameOver = true;
return;
}
snake.body.insert(snake.body.begin(), head);
// Check food
if (head == food)
{
score++;
generateFood();
}
else
{
snake.body.pop_back();
}
}游戏的主绘图函数如下所示
void drawGame()
{
M5.Display.clear(TFT_BLACK);
// Draw snake
for (auto &p : snake.body)
{
M5.Display.fillRect(p.first * GRID_SIZE, p.second * GRID_SIZE, GRID_SIZE, GRID_SIZE, TFT_GREEN);
}
// Draw food
M5.Display.fillRect(food.first * GRID_SIZE, food.second * GRID_SIZE, GRID_SIZE, GRID_SIZE, TFT_RED);
// Draw score
M5.Display.setFont(&fonts::FreeMonoBold12pt7b);
M5.Display.setTextColor(TFT_WHITE);
M5.Display.setCursor(10, 10);
M5.Display.printf("Score: %d", score);
if (gameOver)
{
M5.Display.setCursor(WIDTH * GRID_SIZE / 2 - 50, HEIGHT * GRID_SIZE / 2);
M5.Display.println("Game Over!");
M5.Display.setCursor(WIDTH * GRID_SIZE / 2 - 70, HEIGHT * GRID_SIZE / 2 + 20);
M5.Display.println("Tilt to restart");
}
}主程序循环
void loop()
{
M5.update();
M5.Imu.update();
imuData = M5.Imu.getImuData();
// Control direction based on IMU tilt
const float threshold = 0.3;
if (imuData.accel.x > threshold)
{
if (snake.dir != 3)
snake.dir = 1; // right
}
else if (imuData.accel.x < -threshold)
{
if (snake.dir != 1)
snake.dir = 3; // left
}
if (imuData.accel.y > threshold)
{
if (snake.dir != 0)
snake.dir = 2; // down
}
else if (imuData.accel.y < -threshold)
{
if (snake.dir != 2)
snake.dir = 0; // up
}
static unsigned long lastUpdate = 0;
if (millis() - lastUpdate > 200)
{ // Update every 200ms
updateGame();
drawGame();
lastUpdate = millis();
}
if (gameOver)
{
// Check if tilted to restart
if (abs(imuData.accel.x) > threshold || abs(imuData.accel.y) > threshold)
{
initGame();
}
}
}主循环每次都获取IMU的数据,然后来更新游戏状态和绘制图形。并且判断是否游戏结束。 将上述的所有代码都组合一起,便成了这次的贪吃蛇的Demo
完整代码如下
我要赚赏金
