1.项目描述
项目目的
使用姿态传感器完成体感游戏手柄,并设计完成一个游戏,例如俄罗斯方块、左右晃动手柄使方块左右移动。
计划使用M5StickC Plus设计一款重力感应的贪吃蛇游戏。
设计思路
利用重力感应去作为方向按键,控制蛇的上下左右移动,共计支持四个方向。
板卡正面按键作为开始按钮,当蛇死亡时,按此按键可以重新开始游戏。
游戏显示底为纯绿色,屏幕四周绘制红色圆形围栏,碰上后游戏结束。蛇本身采用纯黑色,食物采用蓝色,蛇、食物、围栏采用圆形图形。
2.硬件介绍
- 基于 ESP32开发,支持WiFi
- 内置3轴加速计与3轴陀螺仪
- 内置Red LED
- 集成红外发射管
- 内置RTC
- 集成麦克风
- 用户按键, LCD(1.14 寸), 电源/复位按键
- 120 mAh 锂电池
- 拓展接口
- 集成无源蜂鸣器
- 可穿戴 & 可固定
- 兼容多平台开发:
主控资源 | 参数 |
ESP32 | 240MHz dual core, 600 DMIPS, 520KB SRAM, Wi-Fi, dual mode Bluetooth |
Flash闪存 | 4MB Flash |
输入电压 | 5V @ 500mA |
接口 | TypeC x 1, GROVE(I2C+I/0+UART) x 1 |
LCD屏幕 | 1.14 inch, 135*240 Colorful TFT LCD, ST7789v2 |
麦克风 | SPM1423 |
按键 | 自定义按键 x 2 |
LED | 红色 LED x 1 |
RTC | BM8563 |
PMU | AXP192 |
蜂鸣器 | 板载蜂鸣器 |
IR | Infrared transmission |
MEMS | MPU6886 |
天线 | 2.4G 3D天线 |
外接引脚 | G0, G25/G26, G36, G32, G33 |
电池 | 120 mAh @ 3.7V, inside vb |
工作温度 | 32°F to 104°F ( 0°C to 40°C ) |
净重 | 16g |
毛重 | 21g |
产品尺寸 | 48.2*25.5*13.7mm |
包装尺寸 | 65*25*15mm |
外壳材质 | Plastic ( PC ) |
3.开发环境
M5StickC PLUS支持多种开发环境,官方建议使用UIFlow, MicroPython, Arduino三种环境中的一种。本次开发选用Arduino作为开发环境:
首先将设备连接至PC,打开设备管理器为设备安装 FTDI驱动 。
访问 Arduino 官网 ,选择对应自己操作系统的安装包进行下载。
打开 Arduino IDE,选择 文件->首选项->设置
复制下方的 M5Stack 板管理网址到 附加开发板管理器: 中
https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json
选择 工具->开发板:->开发板管理器...
在新弹出的对话框中,输入并搜索 M5Stack,点击安装(若出现搜索失败的情况,可以尝试重启Arduino程序)
选择 工具->开发板:->M5Stack Arduino, 根据我们所使用的设备(M5Stick-C-Plus)选择对应的开发板配置。相关库
不同的硬件设备,有着不同的案例程序库,根据所使用的设备选择下载。打开 Arduino IDE, 然后选择 项目->加载库->库管理...
搜索 M5StickCPlus 并安装,下载时请根据弹窗提示,安装相关依赖库。
4.软件运行流程图
5.主要代码片段及说明
初始化部分
清屏,绘制整体的绿底,设置蛇的初始出现位置,设置蛇的初始长度,设置第一颗食物刷新的位置,绘制四边围栏。
snake_body[0][0]=3;
snake_body[0][1]=1;
snake_body[1][0]=2;
snake_body[1][1]=1;
snake_body[2][0]=1;
snake_body[2][1]=1;
snake_lenght = 3;
food_coordinate[0] = 7;
food_coordinate[1] = 7;
//清屏
M5.Lcd.fillScreen(TFT_GREEN);
//画围栏
for(uint8_t i=0; i<=SNAKE_X_MAX; i++) {
M5.Lcd.fillCircle(TFT_X_MIN + DOT_RADIUS + i*DOT_RADIUS*2, TFT_Y_MIN + DOT_RADIUS, DOT_RADIUS, TFT_RED);
M5.Lcd.fillCircle(TFT_X_MIN + DOT_RADIUS + i*DOT_RADIUS*2, TFT_Y_MIN + DOT_RADIUS + SNAKE_Y_MAX*DOT_RADIUS*2, DOT_RADIUS, TFT_RED);
}
for(uint8_t i=0; i<=SNAKE_Y_MAX; i++) {
M5.Lcd.fillCircle(TFT_X_MIN + DOT_RADIUS, TFT_Y_MIN + DOT_RADIUS + i*DOT_RADIUS*2, DOT_RADIUS, TFT_RED);
M5.Lcd.fillCircle(TFT_X_MIN + DOT_RADIUS + SNAKE_X_MAX*DOT_RADIUS*2, TFT_Y_MIN + DOT_RADIUS + i*DOT_RADIUS*2, DOT_RADIUS, TFT_RED);
}
蛇移动部分
读取加速度值,通过比较各个方向的加速度值大小与正负,判断蛇应该前进的方向。
//移动蛇
M5.IMU.getAccelData(&accX, &accY, &accZ);
for(uint8_t i=snake_lenght; i>0; i--)
{
snake_body[i][0] = snake_body[i-1][0];
snake_body[i][1] = snake_body[i-1][1];
}
if(accX < 0 && abs(accX) >= abs(accY)) {
snake_body[0][1]--;
}
else if(accX > 0 && abs(accX) >= abs(accY)) {
snake_body[0][1]++;
}
else if(accY < 0 && abs(accY) >= abs(accX)) {
snake_body[0][0]--;
}
else// if(accY > 0 && abs(accY) >= abs(accX))
{
snake_body[0][0]++;
}
吃食物与碰撞检测部分
判断蛇头与墙、食物、蛇身的重叠,然后进行不同的处理,碰到食物时:增加蛇长,重新产生一个随机位置的食物;碰到墙或自身时:游戏结束。
//吃食物
if(snake_body[0][0] == food_coordinate[0] && snake_body[0][1] == food_coordinate[1]) {
snake_lenght ++;
//重新生成食物
food_coordinate[0] = random(1, SNAKE_X_MAX);
food_coordinate[1] = random(1, SNAKE_Y_MAX);
}
//碰墙
if(snake_body[0][0] == 0 || snake_body[0][0] == SNAKE_X_MAX || snake_body[0][1] == 0 || snake_body[0][1] == SNAKE_Y_MAX) {
goto GAME_OVER;
}
//碰自己
for(uint8_t i=1; i<snake_lenght; i++) {
if(snake_body[0][0] == snake_body[i][0] && snake_body[0][1] == snake_body[i][1]) {
goto GAME_OVER;
}
}
显示蛇、食物部分
刷新新的蛇头,食物,清除不需要显示的蛇部分(使用覆盖刷新的办法)。
//刷新蛇
M5.Lcd.fillCircle(TFT_X_MIN + DOT_RADIUS + snake_body[0][0]*DOT_RADIUS*2, TFT_Y_MIN + DOT_RADIUS + snake_body[0][1]*DOT_RADIUS*2, DOT_RADIUS, TFT_BLACK);
if(snake_body[snake_lenght][0]!=0 && snake_body[snake_lenght][1]!=0){
M5.Lcd.fillCircle(TFT_X_MIN + DOT_RADIUS + snake_body[snake_lenght][0]*DOT_RADIUS*2, TFT_Y_MIN + DOT_RADIUS + snake_body[snake_lenght][1]*DOT_RADIUS*2, DOT_RADIUS, TFT_GREEN);
}
//刷新食物
M5.Lcd.fillCircle(TFT_X_MIN + DOT_RADIUS + food_coordinate[0]*DOT_RADIUS*2, TFT_Y_MIN + DOT_RADIUS + food_coordinate[1]*DOT_RADIUS*2, DOT_RADIUS, TFT_BLUE);
delay(500);
游戏结束
显示 GAME OVER! ,等待按键按下,重新开始。
GAME_OVER:
M5.Lcd.setCursor(20, 40);
M5.Lcd.print("GAME OVER!");
while (1) {
if (digitalRead(M5_BUTTON_HOME) == LOW) {
while (digitalRead(M5_BUTTON_HOME) == LOW)
;
break;
}
}
6.功能演示
游戏进行中
游戏结束
碰墙结束
7.遇到的主要难题及解决方法
屏幕显示刷新感强,闪烁严重
官方提供的屏幕显示代码是直接刷新到屏幕,并不是先刷新到缓冲区,然后再统一刷新到屏幕上的方式。这种直接刷新的方式,会有肉眼可见的刷新感,能看到整个的刷新过程,显示观感很差。解决的话,可以采用类似脏矩形的机制去刷新,只去刷新那些有改变的地方。例如当蛇移动过去的时候,在需要消失的地方刷新一个与底颜色相同的圆。对于这个设计,我将其简化为底加一个个不同颜色不同位置的圆,圆可能出现的位置是固定的,当我需要让某一个圆消失的时候,就可以在此圆的位置上刷新一个与底相同的圆覆盖刷新,这种方式既优化了显示,减少了刷新感,又加快了计算速度,减少了计算压力,让整体看起来更加流畅自然。
倾斜移动有问题,有些方向识别不好
代码编写问题,判断方向时,一开始的代码是直接进行大小比较,导致部分方向判断不准确,修改代码,改为判断加速度值正负,然后使用abs函数处理加速度之后比较大小,进而判断方向。
8.未来的计划或建议
当前只是初步实现了贪吃蛇的功能,下一步计划增加计分跟关卡功能,每一关设置一个目标,当到达目标后就会通过当前关卡,到达下一关,每一关都设计不同的地图,在每一关里面随着蛇不断变长,蛇移动速度也在不断增加。
当前存在一个小的逻辑问题,当向蛇前进方向相反的方向倾斜设备,就会让蛇原地掉头,导致碰到自己的身体而游戏结束,暂时没有想到太好的解决办法,以后想到之后可以对其进行改进。