基于M5StickC Plus制作的儿时小游戏——弹珠迷宫
2022暑期在家一起练 M5StickC Plus esp32 Arduino 游戏 迷宫 随机深度优先算法 弹珠迷宫 IMU
标签
嵌入式系统
MPU
测试
显示
aramy
更新2022-09-02
777

硬件介绍: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。

任务选择:我选择了任务3:使用姿态传感器完成体感游戏手柄,并设计完成一个游戏。我设计的游戏是一个弹珠迷宫的游戏。来源于小时候上学,同桌拿来的尺子上带着这样的游戏。就是一个小铁弹珠,尺子中间是镂空的,通过控制尺子的角度移动弹珠通过迷宫。到现在当时老师课上讲的什么,全忘了,这个游戏还历历在目。
M5StickC PLUS 中集成了IMU,正好可以用了检测水平角度,通过重力控制小球通过迷宫。编程环境就选用Arduino。

任务实现FlZy6kNgcuFGbii8sZHs9_3fJjDn

既然是个迷宫游戏,就要先有个迷宫。大概了解了一下,生成迷宫的算法主要有三种思路,其中最小生成树算法又可以分为选点法(prim)和选边法(kruskal):随机深度优先算法。递归分割算法(TODO)。随机prim最小生成树算法。*kruskal最小生成树算法(使用并查集实现)。生成的迷宫需要在屏幕上显示,M5StickC PLUS 屏幕135*240像素的。我这里使用15*15的矩形块作为迷宫的通道和障碍物。则迷宫的规模就是15*27。使用随机深度优先算法来生成迷宫。深度优先算法过程核心是随机选择遍历上下左右四个方向的顺序,然后开始搜索。将整个迷宫看做一个【15*27】的矩阵,每个节点使用一位来存储,每一行就使用两个byte来存储。迷宫就使用一个无符号整型的数组来表示,长度为27。

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("%d\t%d\n", 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,%d\t\tnewpos=%d,%d\t%d\n\n", 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("%d\n",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,%d\n",ballpos[0], ballpos[1],newx, newy);
    //三种可能  xy都移动
    if (!isBlock(newx, newy)) {
      //      Serial.printf("move xy:%d,%d\n",newx, newy);
      dealMoveBall(newx, newy);
    } else   if (xflag && (!isBlock(newx, ballpos[1]))) {    //只移动X轴
      //      Serial.printf("move x:%d,%d\n",newx, ballpos[1]);
      dealMoveBall(newx, ballpos[1]);
    } else  if (yflag && (!isBlock(ballpos[0], newy))) {     //只移动y轴
      //      Serial.printf("move y:%d,%d\n",ballpos[0],newy);
      dealMoveBall(ballpos[0], newy);
    }
  }
}

游戏开局,小球在左上方的入口,通过控制M5StickC Plus的水平角度,慢慢滴让小球移动到右下角的出口。实现了小时游戏的感觉。

Fp6R735udpF1OqsGEKO0VRoOkPmrFpz5B0lqc4wSJFqpw7leqY4-AgUWFqJwA993x69YXQrGRrZaFevNDCX9

心得体会:非常感谢硬禾学堂和电子森林,M5StickC Plus功能非常强大,Arduino用来开发方便快捷,但总觉得还有少许遗憾。跟着群里的各位老师学习着esp-idf的编程,以后可以尝试着在esp-idf下玩出更多的精彩。

附件下载
mazz.ino
maza_prim.py
团队介绍
DIY折腾小能手
团队成员
aramy
单片机业余爱好者,瞎捣鼓小能手。
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号