1、硬件介绍:
M5StickC PLUS 是M5StickC的大屏幕版本。由电子森林“2022暑期在家一起练”活动推出的一个开发板。主控采用ESP32-PICO-D4模组,具备蓝牙4.2与WIFI功能,小巧的机身内部集成了丰富的硬件资源,如红外、RTC、麦克风、LED、IMU、按键、蜂鸣器、PMU等,在保留原有M5StickC功能的基础上加入了无源蜂鸣器,同时屏幕尺寸升级到1.14寸、135*240分辨率的TFT屏幕,相较之前的0.96寸屏幕增加18.7%的显示面积,电池容量达到120mAh,接口同样支持HAT与Unit系列产品。开发平台支持 UIFlow, MicroPython, Arduino。
2、设计思路:
思路来源于一个弹珠迷宫的游戏。小时候上学,同桌拿来的尺子上带着这样的游戏。就是一个小铁弹珠,尺子中间是镂空的,通过控制尺子的角度移动弹珠通过迷宫。到现在当时老师课上讲的什么,全忘了,这个游戏还历历在目。M5StickC PLUS 中集成了IMU,正好可以用了检测水平角度,通过重力控制小球通过迷宫。编程环境就选用Arduino。
3、设计实现:
既然是个迷宫游戏,就要先有个迷宫。大概了解了一下,生成迷宫的算法主要有三种思路,其中最小生成树算法又可以分为选点法(prim)和选边法(kruskal):随机深度优先算法。递归分割算法(TODO)。随机prim最小生成树算法。*kruskal最小生成树算法(使用并查集实现)。生成的迷宫需要在屏幕上显示,M5StickC PLUS 屏幕135*240像素的。我这里使用15*15的矩形块作为迷宫的通道和障碍物。则迷宫的规模就是15*27。使用随机深度优先算法来生成迷宫。深度优先算法过程核心是随机选择遍历上下左右四个方向的顺序,然后开始搜索。将整个迷宫看做一个【15*27】的矩阵,每个节点使用一位来存储,每一行就使用两个byte来存储。迷宫就使用一个无符号整型的数组来表示,长度为27。先使用python来生成一个迷宫。
import numpy as np import time import random import copy class Maze(object): def __init__(self, width=11, height=11): # 迷宫最小长宽为5 assert width >= 5 and height >= 5, "Length of width or height must be larger than 5." # 确保迷宫的长和宽均为奇数 self.width = (width // 2) * 2 + 1 self.height = (height // 2) * 2 + 1 self.start = [1, 0] self.destination = [self.height - 2, self.width - 1] self.matrix = None def print_matrix(self): for i in range(self.height): rowval=0 for j in range(self.width): if self.matrix[i][j] == -1: rowval=(rowval<<1)+1 elif self.matrix[i][j] == 0: rowval = (rowval<<1) + 0 print(rowval,end=",") # print('') def generate_matrix_dfs(self): # 地图初始化,并将出口和入口处的值设置为0 self.matrix = -np.ones((self.height, self.width)) self.matrix[self.start[0], self.start[1]] = 0 self.matrix[self.destination[0], self.destination[1]] = 0 visit_flag = [[0 for i in range(self.width)] for j in range(self.height)] def check(row, col, row_, col_): temp_sum = 0 for d in [[0, 1], [0, -1], [1, 0], [-1, 0]]: temp_sum += self.matrix[row_ + d[0]][col_ + d[1]] return temp_sum <= -3 def dfs(row, col): visit_flag[row][col] = 1 self.matrix[row][col] = 0 if row == self.start[0] and col == self.start[1] + 1: return directions = [[0, 2], [0, -2], [2, 0], [-2, 0]] random.shuffle(directions) for d in directions: row_, col_ = row + d[0], col + d[1] if row_ > 0 and row_ < self.height - 1 and col_ > 0 and col_ < self.width - 1 and visit_flag[row_][ col_] == 0 and check(row, col, row_, col_): if row == row_: visit_flag[row][min(col, col_) + 1] = 1 self.matrix[row][min(col, col_) + 1] = 0 else: visit_flag[min(row, row_) + 1][col] = 1 self.matrix[min(row, row_) + 1][col] = 0 dfs(row_, col_) dfs(self.destination[0], self.destination[1] - 1) self.matrix[self.start[0], self.start[1] + 1] = 0 # 这里的长和宽设置的是50,但是实际生成的迷宫长宽会是51 maze = Maze(15, 27) maze.generate_matrix_dfs() maze.print_matrix()
初始化各个组件:这里需要用到屏幕——用了展示、IMU ——用来感知开发板与地面的夹角、串口——用来调试程序。M5StickC Plus提供了详细的说明文档,还提供了Arduino相关的例程。参考着例程对系统进行初始化。
void setup() { M5.begin(); M5.Lcd.fillScreen(TFT_DARKCYAN); Serial.begin(115200); M5.Imu.Init(); // Init IMU. 初始化IMU drawMaze(); drawBall(ballpos[0], ballpos[1]); // delay(1000); // dispSuccess(); }
初始化后,立即绘制迷宫图案,整个迷宫仅仅需要绘制障碍物部分,在游戏过程中,障碍物时不会被覆盖,所以只需要绘制一次即可。小球要求能够在通道中顺滑地滚动,这里使用一个4像素的圆的图案作为移动的小球。
//绘制迷宫 void drawMaze() { bool board; for (int i = 0; i < MAZEHIGHT; i++) { //迷宫每一个数组 都是迷宫每一行的信息,有16位信息 for (int j = 0; j < MAZEWIDE; j++) { board = (mazelist[i] >> j) & 1; // Serial.print(board); // Serial.print(" "); if (board) { //绘制障碍物 drawBlock(j, i); } else { //无处理 } } Serial.println(); } }
这个游戏的输入为重力。通过手控制M5StickC Plus开发板的水平角度,来控制小球移动。小球可以在水平方向上移动,移动方向就有2个x、y。所以只需要读取IMU的accX和accY的值即可。
M5StickC Plus中IMU使用的是MPU6886。accX和accY读取到的值就是重力在水平面上的分量。通过三角函数可以计算出开发板当前的倾斜角度。这里为了简化模型,直接使用读取到的accX和accY放大到整数,作为控制小球移动的力量。将移动小球的速度控制在一个合理的区间内,对x、y方向的力的大小由accX和accY的值做范围限制,限制在【-3,3】之间。当偏转角度较小时(为0时),循环读取IMU的数据,对屏幕不做处理。
void loop() { int movex = 0, movey = 0; short newposx, newposy; if (!isSuccess) { while (1) { M5.IMU.getAccelData(&accX, &accY, &accZ); movex = accX * IMUZOOM; movey = accY * IMUZOOM; if (movex != 0 || movey != 0) { break; } } //控制移动上限 if (movex > MAXSPEED) { movex = MAXSPEED; } if (movex < -MAXSPEED) { movex = -MAXSPEED; } if (movey > MAXSPEED) { movey = MAXSPEED; } if (movey < -MAXSPEED) { movey = -MAXSPEED; } Serial.printf("%dt%dn", movex, movey); moveBall(movey, -movex); delay(15); } else { dispSuccess(); delay(5000); } }
当感知到M5StickC Plus水平面有偏转了,即需要小球移动时,首先检查是否在终点,在终点则游戏结束。不在终点就分别检查X,Y两个方向上是否可以移动。优先X方向。若两个方向均可移动,先在X方向移动1格,再在Y方向移动一格。若只是单方向可以移动,则仅仅处理单方向的移动。小球每次移动1个格子,在屏幕上的新位置绘制小球,然后在原来的位置使用背景色绘制一遍小球,就实现了小球移动动画的绘制。这样每次就只需要重新绘制小球所在新旧位置的圆即可,大大提升了绘制的速度,使得界面流畅。不同的移动力量对应着小球最大能移动的步数。最大力量对应着最多能移动3格。
//绘制小球 void drawBall(uint x, uint y) { int posx = x * BLOKESIZE ; int posy = y * BLOKESIZE ; M5.Lcd.fillCircle(posy + BALLRADIUS, posx + BALLRADIUS, BALLRADIUS, TFT_YELLOW); } //清除小球 void clearBall(uint x, uint y) { int posx = x * BLOKESIZE ; int posy = y * BLOKESIZE ; M5.Lcd.fillCircle(posy + BALLRADIUS, posx + BALLRADIUS, BALLRADIUS, TFT_DARKCYAN); } //判断新位置是否有障碍物,如果没有则返回false 有则true bool isBlock(uint x, uint y) { uint mazecol = mazelist[x]; if (x < 0 || x >= MAZEHIGHT) return true; if (y < 0 || y >= MAZEWIDE) return true; return (mazecol >> y) & 1; } //判断新位置是否为初始值或者是结束值。初始值:-1 结束值:1 其它:0 short isSpacePos(uint x, uint y) { if (x == 1 && y == MAZEWIDE - 1) return -1; if (x == MAZEHIGHT - 1 - 1 && y == 0) return 1; return 0; } //移动小球的执行动作 void dealMoveBall(short newx, short newy) { //判断新的坐标是否 到达特殊地址? short echo = isSpacePos(newx, newy); if(newx==ballpos[0] && newy==ballpos[1]) return; Serial.printf("oldpos:%d,%dttnewpos=%d,%dt%dnn", ballpos[0], ballpos[1], newx, newy, echo); if ( echo == 1) { //到达终点 isSuccess = true; drawBall(newx, newy); clearBall(ballpos[0], ballpos[1]); ballpos[0] = newx; ballpos[1] = newy; } else if ( echo == 0) { //无障碍物 drawBall(newx, newy); clearBall(ballpos[0], ballpos[1]); ballpos[0] = newx; ballpos[1] = newy; } } //移动小球 x ,y 代表x方向和y方向上的移动,每次仅移动1 void moveBall(short movex, short movey) { bool xflag = false, yflag = false; //通过与水平夹角决定移动小球 short moveround = abs(movex) > abs(movey) ? abs(movex) : abs(movey); short newx = ballpos[0], newy = ballpos[1]; // Serial.printf("%dn",moveround); if ( isSuccess ) return; //如果已经完成了,则退出 for (int i = 0; i < moveround; i++) { //每次移动一位 x 轴 if (movex > 0) { //右移 newx = ballpos[0] + 1; movex = movex - 1; xflag = true; } else if (movex < 0) { //左移 newx = ballpos[0] - 1; movex = movex + 1; xflag = true; } if (movey > 0) { //上移 newy = ballpos[1] + 1; movey = movey - 1; yflag = true; } else if (movey < 0) { //下移 newy = ballpos[1] - 1; movey = movey + 1; yflag = true; } // Serial.printf("count new pos:%d,%d newxy:%d,%dn",ballpos[0], ballpos[1],newx, newy); //三种可能 xy都移动 if (!isBlock(newx, newy)) { // Serial.printf("move xy:%d,%dn",newx, newy); dealMoveBall(newx, newy); } else if (xflag && (!isBlock(newx, ballpos[1]))) { //只移动X轴 // Serial.printf("move x:%d,%dn",newx, ballpos[1]); dealMoveBall(newx, ballpos[1]); } else if (yflag && (!isBlock(ballpos[0], newy))) { //只移动y轴 // Serial.printf("move y:%d,%dn",ballpos[0],newy); dealMoveBall(ballpos[0], newy); } } }
游戏开局,小球在左上方的入口,通过控制M5StickC Plus的水平角度,慢慢滴让小球移动到右下角的出口。实现了小时游戏的感觉。