2022寒假在家练——基于树莓派RP2040实现推箱子小游戏
使用Thonny IDE连接RP2040 Game Kit,基于MicroPython语言实现了一款经典的推箱子小游戏,将地图上所有箱子推到指定的标记点处即为游戏成功。
标签
嵌入式系统
RP2040
2022寒假在家练
王雪怡
更新2022-03-02
中央民族大学
1029
  • 项目介绍:使用Thonny IDE连接RP2040 Game Kit,基于MicroPython语言实现了一款经典的推箱子小游戏,将地图上所有箱子推到指定的标记点处即为游戏成功。
  • 硬件介绍:本项目使用树莓派Pico核心芯片RP2040。板卡正面由一个240*240分辨率的彩色IPS LCD、一个四向摇杆、两个轻触按键构成。此外,板卡上还有SPI接口、一个三轴姿态传感器MMA7660用做输入控制、一个红外接收管 + 一个红外发射管、双排16Pin连接器,有SPI、I2C以及2路模拟信号输入。片内温度传感器、并支持外部4路模拟信号输入,内部ADC采样率高达500Ksps、12位精度,支持MicroPython、C、C++编程。
  • 项目设计思路:
    • 文字说明: 
      • 程序开始运行,出现初始界面,显示项目名称及游戏方法,并提示用户按下A键触发游戏。
      • 用户按下A键开始游戏,出现初始地图。初始地图中静态部分有板墙、通道及标记点位置,动态部分有箱子和小人。
      • 用户使用摇杆控制小人方向。这里以“上”为例,其它三个方向(下、左、右)同理。(设目前所处位置坐标为(x,y))
        • 若所处位置上方(x,y-1)为板墙:则返回,小人停留在目前的位置不变。
        • 若所处位置上方(x,y-1)为箱子:
          • 若所处位置上方两格(x,y-2)为板墙或另一个箱子:返回,小人停留在目前的位置不变
          • 否则,小人纵坐标-1(变为(x,y-1)),小人正上方的箱子纵坐标-1(变为(x,y-2)),当前位置(x,y)变为初始地图静态部分规定的样子(通道或标记点)。
        • 若以上两种情况都不是,则直接将小人纵坐标位置-1,当前位置(x,y)变为初始地图静态部分规定的样子(通道或标记点)。
      • 判断两个箱子所在的位置是否与标记点位置一致
        • 一致:游戏成功,显示成功字样
        • 不一致:继续游戏,用户继续使用摇杆控制
      • 若用户需要游戏中断重来:按B键,则游戏界面回复初始地图样式。
    • 流程图说明
  • 代码片段说明: 
    • 设置引脚,A键和B键均为输入端口,会使电平升高。
      xAxis = ADC(Pin(28))
      yAxis = ADC(Pin(29))
      buttonB = Pin(5,Pin.IN, Pin.PULL_UP) 
      buttonA = Pin(6,Pin.IN, Pin.PULL_UP)
      led = Pin(4, Pin.OUT)​
    • 背景音乐数据,使用贪吃蛇背景音乐中的一个。
      song = '0 A5 2 26 0.6299212574958801;2 B5 2 26 0.6299212574958801;4 C6 6 26 0.6299212574958801;10 B5 2 26 0.6299212574958801;12 C6 4 26 0.6299212574958801;16 E6 4 26 0.6299212574958801;20 B5 8 26 0.6299212574958801;32 E5 4 26 0.6299212574958801;36 A5 6 26 0.6299212574958801;42 G5 2 26 0.6299212574958801;44 A5 4 26 0.6299212574958801;48 C6 4 26 0.6299212574958801;52 G5 8 26 0.6299212574958801;64 F5 2 26 0.6299212574958801;66 E5 2 26 0.6299212574958801;68 F5 6 26 0.6299212574958801;74 E5 2 26 0.6299212574958801;76 F5 4 26 0.6299212574958801;80 C6 4 26 0.6299212574958801;84 E5 8 26 0.6299212574958801;94 C6 2 26 0.6299212574958801;96 C6 2 26 0.6299212574958801;98 C6 2 26 0.6299212574958801;100 B5 6 26 0.6299212574958801;106 F#5 2 26 0.6299212574958801;108 F#5 4 26 0.6299212574958801;112 B5 4 26 0.6299212574958801;116 B5 8 26 0.6299212574958801;128 A5 2 26 0.6299212574958801;130 B5 2 26 0.6299212574958801;132 C6 6 26 0.6299212574958801;138 B5 2 26 0.6299212574958801;140 C6 4 26 0.6299212574958801;144 E6 4 26 0.6299212574958801;148 B5 8 26 0.6299212574958801;160 E5 2 26 0.6299212574958801;162 E5 2 26 0.6299212574958801;164 A5 6 26 0.6299212574958801;170 G5 2 26 0.6299212574958801;172 A5 4 26 0.6299212574958801;176 C6 4 26 0.6299212574958801;180 G5 8 26 0.6299212574958801;192 E5 4 26 0.6299212574958801;196 F5 4 26 0.6299212574958801;200 C6 2 26 0.6299212574958801;202 B5 2 26 0.6299212574958801;204 B5 4 26 0.6299212574958801;208 C6 4 26 0.6299212574958801;212 D6 4 26 0.6299212574958801;216 E6 2 26 0.6299212574958801;218 C6 2 26 0.6299212574958801;220 C6 8 26 0.6299212574958801;228 C6 2 26 0.6299212574958801;230 B5 2 26 0.6299212574958801;232 A5 4 26 0.6299212574958801;236 B5 4 26 0.6299212574958801;240 G#5 4 26 0.6299212574958801;244 A5 11 26 0.6299212574958801;256 C6 2 26 0.6299212574958801;258 D6 2 26 0.6299212574958801;260 E6 6 26 0.6299212574958801;266 D6 2 26 0.6299212574958801;268 E6 4 26 0.6299212574958801;272 G6 4 26 0.6299212574958801;276 D6 8 26 0.6299212574958801;288 G5 2 26 0.6299212574958801;290 G5 2 26 0.6299212574958801;292 C6 6 26 0.6299212574958801;298 B5 2 26 0.6299212574958801;300 C6 4 26 0.6299212574958801;304 E6 4 26 0.6299212574958801;308 E6 11 26 0.6299212574958801;324 A5 2 26 0.6299212574958801;326 B5 2 26 0.6299212574958801;328 C6 4 26 0.6299212574958801;332 B5 2 26 0.6299212574958801;334 C6 2 26 0.6299212574958801;336 D6 4 26 0.6299212574958801;340 C6 6 26 0.6299212574958801;346 G5 2 26 0.6299212574958801;348 G5 8 26 0.6299212574958801;356 F6 4 26 0.6299212574958801;360 E6 4 26 0.6299212574958801;364 D6 4 26 0.6299212574958801;368 C6 4 26 0.6299212574958801;372 E6 15 26 0.6299212574958801;388 E6 11 26 0.6299212574958801;400 E6 4 26 0.6299212574958801;404 A6 8 26 0.6299212574958801;412 G6 8 26 0.6299212574958801;420 E6 4 26 0.6299212574958801;424 D6 2 26 0.6299212574958801;426 C6 2 26 0.6299212574958801;428 C6 8 26 0.6299212574958801;436 D6 4 26 0.6299212574958801;440 C6 2 26 0.6299212574958801;442 D6 2 26 0.6299212574958801;444 D6 4 26 0.6299212574958801;448 G6 4 26 0.6299212574958801;452 E6 11 26 0.6299212574958801;464 E6 4 26 0.6299212574958801;468 A6 8 26 0.6299212574958801;476 G6 8 26 0.6299212574958801;484 E6 4 26 0.6299212574958801;488 D6 2 26 0.6299212574958801;490 C6 2 26 0.6299212574958801;492 C6 8 26 0.6299212574958801;500 D6 4 26 0.6299212574958801;504 C6 2 26 0.6299212574958801;506 D6 2 26 0.6299212574958801;508 D6 4 26 0.6299212574958801;512 B5 4 26 0.6299212574958801;516 A5 11 26 0.6299212574958801;528 A5 2 26 0.6299212574958801;530 B5 2 26 0.6299212574958801;532 C6 6 26 0.6299212574958801;538 B5 2 26 0.6299212574958801;540 C6 4 26 0.6299212574958801;544 E6 4 26 0.6299212574958801;548 B5 8 26 0.6299212574958801;560 E5 4 26 0.6299212574958801;564 A5 6 26 0.6299212574958801;570 G5 2 26 0.6299212574958801;572 A5 4 26 0.6299212574958801;576 C6 4 26 0.6299212574958801;580 G5 8 26 0.6299212574958801;592 F5 2 26 0.6299212574958801;594 E5 2 26 0.6299212574958801;596 F5 6 26 0.6299212574958801;602 E5 2 26 0.6299212574958801;604 F5 4 26 0.6299212574958801;608 C6 4 26 0.6299212574958801;612 E5 8 26 0.6299212574958801;622 C6 2 26 0.6299212574958801;624 C6 2 26 0.6299212574958801;626 C6 2 26 0.6299212574958801;628 B5 6 26 0.6299212574958801;634 F#5 2 26 0.6299212574958801;636 F#5 4 26 0.6299212574958801;640 B5 4 26 0.6299212574958801;644 B5 8 26 0.6299212574958801;656 A5 2 26 0.6299212574958801;658 B5 2 26 0.6299212574958801;660 C6 6 26 0.6299212574958801;666 B5 2 26 0.6299212574958801;668 C6 4 26 0.6299212574958801;672 E6 4 26 0.6299212574958801;676 B5 8 26 0.6299212574958801;688 E5 2 26 0.6299212574958801;690 E5 2 26 0.6299212574958801;692 A5 6 26 0.6299212574958801;698 G5 2 26 0.6299212574958801;700 A5 4 26 0.6299212574958801;704 C6 4 26 0.6299212574958801;708 G5 8 26 0.6299212574958801;720 E5 4 26 0.6299212574958801;724 F5 4 26 0.6299212574958801;728 C6 2 26 0.6299212574958801;730 B5 2 26 0.6299212574958801;732 B5 4 26 0.6299212574958801;736 C6 4 26 0.6299212574958801;740 D6 4 26 0.6299212574958801;744 E6 2 26 0.6299212574958801;746 C6 2 26 0.6299212574958801;748 C6 8 26 0.6299212574958801;756 C6 2 26 0.6299212574958801;758 B5 2 26 0.6299212574958801;760 A5 4 26 0.6299212574958801;764 B5 4 26 0.6299212574958801;768 G#5 4 26 0.6299212574958801;772 A5 11 26 0.6299212574958801'
    • 作图颜色样式设定。板墙设置为白色,小人设置为浅蓝色,箱子设置为蓝色(其上有黄色图案),标记点设置为红色,整个游戏的背景设置为紫色。
      class color_cfg: #颜色设定
          wall = st7789.WHITE
          box = st7789.YELLOW
          blue = st7789.BLUE
          man = st7789.CYAN
          sym = st7789.RED
          background = st7789.color565(128, 0, 128)
      
      def outtextxy(x, y, c, color): #作图
          global pen_color
          hardware.tft.text(font3, c, x*16, y*16, pen_color, color)
      
      def setcolor(c): #设置作图颜色
          global pen_color
          pen_color = c​
    • 初始化屏幕驱动
      • 初始化
        class hardware(): #初始化屏幕驱动
            def init():
                # screen
                st7789_res = 0
                st7789_dc  = 1
                spi_sck=machine.Pin(2)
                spi_tx=machine.Pin(3)
                spi = machine.SPI(0,baudrate=4000000, phase=1, polarity=1, sck=spi_sck, mosi=spi_tx)
                tft = st7789.ST7789(
                    spi,
                    240,
                    240,
                    reset=machine.Pin(st7789_res, machine.Pin.OUT),
                    dc=machine.Pin(st7789_dc, machine.Pin.OUT),
                    #xstart=0,
                    #ystart=0,
                    rotation=0)
        
                tft.fill(color_cfg.background)
                tft.fill_rect(0, 232, 240, 8, color_cfg.blue)
        
                hardware.tft = tft​
      • 静态地图设置。静态地图即不随用户输入电平而改变的部分,在本游戏中为板墙、通道和标记点。用户每操作一次摇杆,地图都需按照此静态地图的初始设置重新绘制。
        #将地图绘制为一个凸字形
        m = [
            0,0,0,0,-1,-1,-1,-1,
            0,1,3,0,0,0,0,0,
            0,1,1,1,1,1,1,0,
            0,1,1,1,1,1,1,0,
            0,1,1,3,1,1,1,0,
            0,1,1,1,1,1,1,0,
            0,1,1,0,0,0,0,0,
            0,0,0,0,-1,-1,-1,-1,]​
    • 游戏主体部分
      • 绘制地图。此函数用于绘制初始地图,且用户每操作一次摇杆,地图都需依赖此函数重新绘制。
        class game():
            ma = []
            d = 0
            x = 1
            y = 1
            mySong = music(song, pins=[Pin(23)])
            
            def draw(self): #根据上述设置绘制地图
                for i in range(8):
                    for n in range(8):
                        if self.ma[n*8+i] == 0:  #绘制板墙
                            setcolor(color_cfg.wall)
                            outtextxy(i+1, n+1, '0',color_cfg.wall)
                        if self.ma[n*8+i] == 1:  #绘制通道
                            setcolor(color_cfg.background)
                            outtextxy(i+1, n+1, '0',color_cfg.background)
                        if self.ma[n*8+i] == 2:  #绘制箱子,放到m中设置的初始位置
                            setcolor(color_cfg.box)
                            outtextxy(i+1, n+1, 'b',color_cfg.blue)
                        if self.ma[n*8+i] == 3:  #绘制终点
                            setcolor(color_cfg.sym)
                            outtextxy(i+1, n+1, 'o',color_cfg.background)
                        if self.ma[n*8+i] == 4:  #绘制人,初始位置为(1,1)
                            setcolor(color_cfg.background)
                            outtextxy(i+1, n+1, 'm',color_cfg.man)
                            ​
      • 初始界面设置。第一行使用头文件中设置好的font2从屏幕的第2列起显示游戏名称,第3-5行从第1列起显示游戏的玩法指南,第6行从第2列起提示用户游戏的触发机制。
            def title(self): #初始界面
                hardware.tft.text(font2, 'BOXMAN', 2*16, 1*32, color_cfg.wall, color_cfg.background) #标题
                hardware.tft.text(font1, 'Note:', 1*16, 3*32, color_cfg.wall, color_cfg.background) #游戏说明
                hardware.tft.text(font1, 'Push boxes to the red flag,', 1*16, 4*32, color_cfg.wall, color_cfg.background)
                hardware.tft.text(font1, 'and then you win the game.', 1*16, 5*32, color_cfg.wall, color_cfg.background)
                hardware.tft.text(font1, 'START:buttonA on the right', 2*16, 6*32, color_cfg.wall, color_cfg.background)
        ​
      • 初始化游戏数据。相比于上面设置好的静态地图,此处增加了动态部分——箱子、小人。箱子的位置根据矩阵设置,小人的初始位置设定为地图的左上角。此处的地图才是游戏开始时在界面上真正显示的地图。
            def init_run(self): #初始化游戏数据
                self.ma = [
                0,0,0,0,-1,-1,-1,-1,
                0,1,3,0,0,0,0,0,
                0,1,1,1,1,1,1,0,
                0,1,2,1,1,0,1,0,
                0,1,1,3,1,0,1,0,
                0,1,1,1,1,2,1,0,
                0,1,1,0,0,0,0,0,
                0,0,0,0,-1,-1,-1,-1,]
                self.d = 0 #移动方向标记,上1下3,左2右4
                 #人所在初始位置为(1,1)
                self.x = 1
                self.y = 1
                self.ma[self.y*8+self.x] = 4
                self.draw() #显示游戏画面
        ​
      • 游戏主进程。首先设置通过按键A触发游戏开始的机制,然后进入游戏的主循环。这是一个死循环,保障游戏一直运行。主循环中调用了判断行动方向、根据行动方向修改地图数据、根据新的地图数据重新绘制地图、判断游戏胜利四个主要函数。
            async def process(self): #游戏主进程
                bgm = asyncio.create_task(self.bgm_process()) #播放音乐(此处使用实例贪吃蛇的音乐)
                self.title() #显示标题界面
                while True:
                    buttonValueA = buttonA.value() 
                    if buttonValueA == 0: #按下A键时进入游戏
                        hardware.init()
                        self.init_run()
                        print('go')
                        
                        while True: #游戏主循环
                            self.d = 0
                            self.dir_select() #判断小人行动方向
                            self.run() #根据移动修改地图数据
                            self.draw() #重绘地图
                            self.judge() #判断游戏胜利
                            await self.blink() #led灯闪烁
            
        ​
      • 判断行动方向。通过x和y方向数值的感知判断用户操作的方向,并将每个行动方向标记为一个数字,便于之后程序的编写。
            def dir_select(self):
                xValue = xAxis.read_u16()
                yValue = yAxis.read_u16()
                buttonValueB = buttonB.value()
                
                #摇杆方向感知
                if xValue <1000: #上,d标记为1
                    self.d = 1
                elif xValue >40000: #下,d标记为3
                    self.d = 3
            
                if yValue <1000: #左,d标记为4
                    self.d = 4
                elif yValue >40000: #右,d标记为2
                    self.d = 2
                if buttonValueB == 0: #按B键,游戏重新开始
                    self.init_run()
                    ​
      • 根据行动方向修改地图数据,具体算法与上述“项目设计思路”中所写相同。分别按照上述算法写好四个方向的代码即可。
            def run(self):
                if self.d == 1: #上
                    if self.ma[(self.y-1)*8 + self.x] == 0:
                        return
                    elif self.ma[(self.y-1)*8 + self.x] == 2:
                        if self.ma[(self.y-2)*8 + self.x] == 0 or self.ma[(self.y-2)*8 + self.x] == 2:
                            return
                        else:
                            self.ma[(self.y-2)*8 + self.x] = 2
                            self.ma[(self.y-1)*8 + self.x] = 4
                            self.ma[self.y*8 + self.x] = m[self.y*8 + self.x]
                            self.y = self.y - 1
                    else:
                        self.ma[(self.y-1)*8 + self.x] = 4
                        self.ma[self.y*8 + self.x] = m[self.y*8 + self.x]
                        self.y = self.y - 1
                elif self.d == 3: #下
                    if self.ma[(self.y+1)*8 + self.x] == 0:
                        return
                    elif self.ma[(self.y+1)*8 + self.x] == 2:
                        if self.ma[(self.y+2)*8 + self.x] == 0 or self.ma[(self.y+2)*8 + self.x] == 2:
                            return
                        else:
                            self.ma[(self.y+2)*8 + self.x] = 2
                            self.ma[(self.y+1)*8 + self.x] = 4
                            self.ma[self.y*8 + self.x] = m[self.y*8 + self.x]
                            self.y = self.y + 1
                    else:
                        self.ma[(self.y+1)*8 + self.x] = 4
                        self.ma[self.y*8 + self.x] = m[self.y*8 + self.x]
                        self.y = self.y + 1
                elif self.d == 4: #左
                    if self.ma[self.y*8 + self.x-1] == 0:
                        return
                    elif self.ma[self.y*8 + self.x-1] == 2:
                        if self.ma[self.y*8 + self.x-2] == 0 or self.ma[self.y*8 + self.x-2] == 2:
                            return
                        else:
                            self.ma[self.y*8 + self.x-2] = 2
                            self.ma[self.y*8 + self.x-1] = 4
                            self.ma[self.y*8 + self.x] = m[self.y*8 + self.x]
                            self.x = self.x - 1
                    else:
                        self.ma[self.y*8 + self.x-1] = 4
                        self.ma[self.y*8 + self.x] = m[self.y*8 + self.x]
                        self.x = self.x - 1
                elif self.d == 2: #右
                    if self.ma[self.y*8 + self.x+1] == 0:
                        return
                    elif self.ma[self.y*8 + self.x+1] == 2:
                        if self.ma[self.y*8 + self.x+2] == 0 or self.ma[self.y*8 + self.x+2] == 2:
                            return
                        else:
                            self.ma[self.y*8 + self.x+2] = 2
                            self.ma[self.y*8 + self.x+1] = 4
                            self.ma[self.y*8 + self.x] = m[self.y*8 + self.x]
                            self.x = self.x + 1
                    else:
                        self.ma[self.y*8 + self.x+1] = 4
                        self.ma[self.y*8 + self.x] = m[self.y*8 + self.x]
                        self.x = self.x + 1
                
            ​
      • 判断是否胜利
            def judge(self):
                if self.ma[1*8+2] == 2 and self.ma[4*8+3] == 2:  #根据终点(sym)所在位置计算游戏是否胜利
                    self.win()​
      • 胜利后出现文字与灯的设定
            def win(self):
                hardware.tft.text(font1, 'Win!Press B to RESTART!', 2*16, 6*32, color_cfg.wall, color_cfg.background)
                
            async def blink(self):
                led.value(1)
                await asyncio.sleep_ms(50)
                led.value(0)
                await asyncio.sleep_ms(50)
                
        ​
      • 播放背景音乐
            async def bgm_process(self):
                while True:
                    self.mySong.tick()
                    await asyncio.sleep(0.04)
                    
        ​
    • 项目主函数调用上面所写函数,运行游戏。
      def main():
          hardware.init()
          s = game()
          asyncio.run(s.process())
                      
      main()
      ​
  • 实现的功能及成果展示
    • 初始界面初始界面
      • 最上面一行为游戏名称,而后为游戏指南,最后为触发游戏机制提醒。
    • 初始地图初始地图
      • 动态+静态。
    • 游戏成功游戏成功
      • 两个箱子均被移动到了标记点位置上。
    • 成功后重置游戏重置游戏
      • 游戏返回初始地图位置。
  • 主要难题及解决方法
    • 问题:起初想要使用python库直接使用python实现游戏编程,遇到树莓派默认关闭SSH的问题,尝试打开SSL但没有成功。

      解决:选择直接使用电子森林中已有的库文件进行编程

    • 问题:地图过大产生溢出

      解决:缩小地图,使用8×8大小即可

    • 问题:给出的贪吃蛇程序中,st7789c程序没有给出,导致贪吃蛇程序无法正常运行,继而无法根据贪吃蛇程序进行修改

      解决:不使用st7789c,直接使用文件st7789中的函数进行初始化即可

    • 问题:课程中使用Online Sequencer网站生成音乐序列,在实际操作过程中,网站的加载速度非常慢,无法生成音乐序列

      解决:直接使用贪吃蛇程序中已经给出的音乐序列即可

附件下载
WangXueyi.zip
团队介绍
王雪怡
团队成员
王雪怡
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号