这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 活动中心 » 板卡试用 » 【M5STACKTAB5W/OBATTERY】【五】IMU贪吃蛇游戏

共1条 1/1 1 跳转至

【M5STACKTAB5W/OBATTERY】【五】IMU贪吃蛇游戏

工程师
2026-01-23 03:04:32     打赏

简介

在完成了 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 提示玩家通过倾斜设备重新开始游戏。

f6f98d4a17f202f8be22e66ab26b69ba.jpg


核心逻辑:蛇的移动与碰撞

每一次更新都会执行以下步骤:

    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


完整代码如下

tanchishe.zip




关键词: M5STACKTAB5W     IMU     贪吃蛇    

共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]