栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Python

手把手教你使用ESP32+MicroPython制作贪吃蛇游戏

Python 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

手把手教你使用ESP32+MicroPython制作贪吃蛇游戏

手把手教你使用ESP32+MicroPython制作贪吃蛇游戏 实现目标

在ESP32开发板上使用MicroPython编程实现一个贪吃蛇小游戏,游戏可以在ssd1306 OLED屏幕上游玩,使用四个按钮开关控制蛇的上下左右移动。

既然是手把手,就是让你不了解相关知识也能跟着流程运行起我们的项目,通过在线的仿真原件在线试玩。

项目所用工具介绍
  • ESP32开发板:上海乐鑫出品的MCU,自带wifi和蓝牙,功能和配置可以说非常良心。如果你有开发板的话,相信你对如何在上面跑程序非常了解了。如果没有开发板那我们也可以使用在线仿真网站Wokwi运行项目。效果和使用开发板是相同的。(接下来的示例就是在仿真网站运行的)
  • MicroPython:为微控制器设计的运行语言,语法和PC端运行的CPython几乎完全一致,不过库的功能可能有所削减。MicroPython让微控制器可以直接运行python这样的解释型语言,避免了使用C/C++等编译型语言开发时的编译、链接和上传步骤。我们只需要把要用的脚本放到目录下,将主程序所在的脚本命名为main.py就可以自动运行程序了。
  • SSD1306 OLED显示屏,用来显示游戏界面。
  • 按键开关4个:用来控制上下左右四个方向,控制蛇的移动。
操作步骤(使用在线仿真网站Wokwi演示)
  1. 进入Wokwi网站,在开发板处选择MicroPython with ESP32,进入项目开发页面:

  2. (不想动手连线的直接跳到第4步)在右侧的模拟器中添加我们使用的元器件,点击“+”,选择1个“SSD1306 OLED display”和4个“PushButton”:

  3. 添加元件后,按照下图连接引脚,连接时使用鼠标点击对应的引脚和想要连接的引脚就可以连线,点击导线还可以改变导线的颜色:

  4. 如果不想自己连线或怕连线出错,可以直接复制下面的代码,粘贴到左侧编辑窗口上方的diagram.json文件中替换原来的内容(使用该方法就不必执行2,3步骤了):

    {
      "version": 1,
      "author": "Besharp",
      "editor": "wokwi",
      "parts": [
        { "type": "wokwi-esp32-devkit-v1", "id": "esp", "top": -194.67, "left": -74, "attrs": {} },
        {
          "type": "wokwi-pushbutton",
          "id": "btn1",
          "top": 170.61,
          "left": -52.71,
          "attrs": { "color": "green" }
        },
        {
          "type": "wokwi-pushbutton",
          "id": "btn2",
          "top": 303.75,
          "left": -56.56,
          "attrs": { "color": "green" }
        },
        {
          "type": "wokwi-pushbutton",
          "id": "btn3",
          "top": 232.46,
          "left": -139.65,
          "attrs": { "color": "red" }
        },
        {
          "type": "wokwi-pushbutton",
          "id": "btn4",
          "top": 236.22,
          "left": 39.83,
          "attrs": { "color": "red" }
        },
        { "type": "board-ssd1306", "id": "oled1", "top": 55.97, "left": -73.5, "attrs": {} }
      ],
      "connections": [
        [ "esp:TX0", "$serialMonitor:RX", "", [] ],
        [ "esp:RX0", "$serialMonitor:TX", "", [] ],
        [ "oled1:GND", "esp:GND.1", "black", [ "v0" ] ],
        [ "oled1:VCC", "esp:3V3", "red", [ "v0" ] ],
        [ "oled1:SCL", "esp:D18", "gold", [ "v0" ] ],
        [ "oled1:SDA", "esp:D19", "gold", [ "v0" ] ],
        [ "btn1:1.r", "esp:D15", "green", [ "v-0.84", "h32.39", "v-232.09" ] ],
        [ "btn2:1.r", "esp:D2", "green", [ "v0.62", "h133.1", "v-381.78" ] ],
        [ "btn3:1.r", "esp:D4", "green", [ "v-74.01", "h125.62", "v3.77" ] ],
        [ "btn4:1.r", "esp:D5", "green", [ "v0" ] ],
        [ "btn1:2.l", "esp:GND.2", "black", [ "h-51.79", "v-249.86" ] ],
        [ "btn4:2.l", "esp:GND.2", "black", [ "h-0.3", "v18.51", "h-207.56", "v-335.24" ] ],
        [ "btn3:2.l", "esp:GND.2", "black", [ "h-10.8", "v-41.22" ] ],
        [ "btn2:2.l", "esp:GND.2", "black", [ "h-100.39", "v-18.02" ] ]
      ]
    }
    


    粘贴后,右侧模拟器窗口就会自动显示连好线的电路图。

  5. 添加ssd1306的库依赖:
    和python程序一样MicroPython为各种硬件提供了方便用户使用的库,用户直接调用其中的方法就可以完成对硬件的操作,而不必去研究复杂的底层硬件操作。
    这里我们需要为ssd1306 OLED显示器添加它的库文件,点击编辑栏上方的下拉按钮,选择“new file”创建一个新文件,命名为ssd1306.py:

    然后将以下内容复制粘贴到文件中:

    # MicroPython SSD1306 OLED driver, I2C and SPI interfaces
    
    from micropython import const
    import framebuf
    
    
    # register definitions
    SET_CONTRAST = const(0x81)
    SET_ENTIRE_ON = const(0xA4)
    SET_NORM_INV = const(0xA6)
    SET_DISP = const(0xAE)
    SET_MEM_ADDR = const(0x20)
    SET_COL_ADDR = const(0x21)
    SET_PAGE_ADDR = const(0x22)
    SET_DISP_START_LINE = const(0x40)
    SET_SEG_REMAP = const(0xA0)
    SET_MUX_RATIO = const(0xA8)
    SET_IREF_SELECt = const(0xAD)
    SET_COM_OUT_DIR = const(0xC0)
    SET_DISP_OFFSET = const(0xD3)
    SET_COM_PIN_CFG = const(0xDA)
    SET_DISP_CLK_DIV = const(0xD5)
    SET_PRECHARGE = const(0xD9)
    SET_VCOM_DESEL = const(0xDB)
    SET_CHARGE_PUMP = const(0x8D)
    
    # Subclassing FrameBuffer provides support for graphics primitives
    # http://docs.micropython.org/en/latest/pyboard/library/framebuf.html
    class SSD1306(framebuf.FrameBuffer):
        def __init__(self, width, height, external_vcc):
            self.width = width
            self.height = height
            self.external_vcc = external_vcc
            self.pages = self.height // 8
            self.buffer = bytearray(self.pages * self.width)
            super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
            self.init_display()
    
        def init_display(self):
            for cmd in (
                    SET_DISP,  # display off
                    # address setting
                    SET_MEM_ADDR,
                    0x00,  # horizontal
                    # resolution and layout
                    SET_DISP_START_LINE,  # start at line 0
                    SET_SEG_REMAP | 0x01,  # column addr 127 mapped to SEG0
                    SET_MUX_RATIO,
                    self.height - 1,
                    SET_COM_OUT_DIR | 0x08,  # scan from COM[N] to COM0
                    SET_DISP_OFFSET,
                    0x00,
                    SET_COM_PIN_CFG,
                    0x02 if self.width > 2 * self.height else 0x12,
                    # timing and driving scheme
                    SET_DISP_CLK_DIV,
                    0x80,
                    SET_PRECHARGE,
                    0x22 if self.external_vcc else 0xF1,
                    SET_VCOM_DESEL,
                    0x30,  # 0.83*Vcc
                    # display
                    SET_CONTRAST,
                    0xFF,  # maximum
                    SET_ENTIRE_ON,  # output follows RAM contents
                    SET_NORM_INV,  # not inverted
                    SET_IREF_SELECt,
                    0x30,  # enable internal IREF during display on
                    # charge pump
                    SET_CHARGE_PUMP,
                    0x10 if self.external_vcc else 0x14,
                    SET_DISP | 0x01,  # display on
            ):  # on
                self.write_cmd(cmd)
            self.fill(0)
            self.show()
    
        def poweroff(self):
            self.write_cmd(SET_DISP)
    
        def poweron(self):
            self.write_cmd(SET_DISP | 0x01)
    
        def contrast(self, contrast):
            self.write_cmd(SET_CONTRAST)
            self.write_cmd(contrast)
    
        def invert(self, invert):
            self.write_cmd(SET_NORM_INV | (invert & 1))
    
        def rotate(self, rotate):
            self.write_cmd(SET_COM_OUT_DIR | ((rotate & 1) << 3))
            self.write_cmd(SET_SEG_REMAP | (rotate & 1))
    
        def show(self):
            x0 = 0
            x1 = self.width - 1
            if self.width != 128:
                # narrow displays use centred columns
                col_offset = (128 - self.width) // 2
                x0 += col_offset
                x1 += col_offset
            self.write_cmd(SET_COL_ADDR)
            self.write_cmd(x0)
            self.write_cmd(x1)
            self.write_cmd(SET_PAGE_ADDR)
            self.write_cmd(0)
            self.write_cmd(self.pages - 1)
            self.write_data(self.buffer)
    
    
    class SSD1306_I2C(SSD1306):
        def __init__(self, width, height, i2c, addr=0x3C, external_vcc=False):
            self.i2c = i2c
            self.addr = addr
            self.temp = bytearray(2)
            self.write_list = [b"x40", None]  # Co=0, D/C#=1
            super().__init__(width, height, external_vcc)
    
        def write_cmd(self, cmd):
            self.temp[0] = 0x80  # Co=1, D/C#=0
            self.temp[1] = cmd
            self.i2c.writeto(self.addr, self.temp)
    
        def write_data(self, buf):
            self.write_list[1] = buf
            self.i2c.writevto(self.addr, self.write_list)
    
    
    class SSD1306_SPI(SSD1306):
        def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
            self.rate = 10 * 1024 * 1024
            dc.init(dc.OUT, value=0)
            res.init(res.OUT, value=0)
            cs.init(cs.OUT, value=1)
            self.spi = spi
            self.dc = dc
            self.res = res
            self.cs = cs
            import time
    
            self.res(1)
            time.sleep_ms(1)
            self.res(0)
            time.sleep_ms(10)
            self.res(1)
            super().__init__(width, height, external_vcc)
    
        def write_cmd(self, cmd):
            self.spi.init(baudrate=self.rate, polarity=0, phase=0)
            self.cs(1)
            self.dc(0)
            self.cs(0)
            self.spi.write(bytearray([cmd]))
            self.cs(1)
    
        def write_data(self, buf):
            self.spi.init(baudrate=self.rate, polarity=0, phase=0)
            self.cs(1)
            self.dc(1)
            self.cs(0)
            self.spi.write(buf)
            self.cs(1)
    
    

  6. 最后,添加我们的主程序:
    在编辑栏中,选择main.py文件,填入我们的主程序:

    """
    显示在ssd1306上的贪吃蛇游戏
    运行后,可以通过按任意按钮启动游戏。
    默认情况下,蛇初始时从左到右移动。游戏的目的是收集尽可能多的水果,水果将随机放置。
    随着每吃一个水果,蛇就会变得更长。
    当蛇撞到墙上或它自己时,游戏结束,显示Gameover。
    此时可通过按任意按钮,将游戏还原为起始值,然后触摸按钮即可再次启动游戏。
    """
    import random
    import time
    
    from machine import Pin, I2C
    
    import ssd1306
    
    SCREEN_WIDTH = 128
    SCREEN_HEIGHT = 64
    
    # 上下左右引脚, 通过上拉电阻设为高电平
    UP_PIN = Pin(15, Pin.IN, Pin.PULL_UP)
    DOWN_PIN = Pin(2, Pin.IN, Pin.PULL_UP)
    LEFT_PIN = Pin(4, Pin.IN, Pin.PULL_UP)
    RIGHT_PIN = Pin(5, Pin.IN, Pin.PULL_UP)
    
    # snake config
    SNAKE_PIECE_SIZE = 3  # 蛇的每一格占用3*3个像素
    MAX_SNAKE_LENGTH = 150  # 蛇的最长长度
    MAP_SIZE_X = 20  # 活动范围
    MAP_SIZE_Y = 20
    START_SNAKE_SIZE = 5  # 初始长度
    SNAKE_MOVE_DELAY = 30  # 移动延时
    
    
    # game config
    class State(object):
        START = 0
        RUNNING = 1
        GAMEOVER = 2
    
        @classmethod
        def setter(cls, state):
            if state == cls.START:
                return cls.START
            elif state == cls.RUNNING:
                return cls.RUNNING
            elif state == cls.GAMEOVER:
                return cls.GAMEOVER
    
    
    class Direction(object):
        # 注意顺序
        UP = 0
        LEFT = 1
        DOWN = 2
        RIGHT = 3
    
        @classmethod
        def setter(cls, dirc):
            if dirc == cls.UP:
                return cls.UP
            elif dirc == cls.DOWN:
                return cls.DOWN
            elif dirc == cls.LEFT:
                return cls.LEFT
            elif dirc == cls.RIGHT:
                return cls.RIGHT
    
    
    i2c = I2C(0)
    screen = ssd1306.SSD1306_I2C(SCREEN_WIDTH, SCREEN_HEIGHT, I2C(0))
    
    
    ################ Snake 功能实现 ###################
    class Snake(object):
        def __init__(self):
            self.snake = []  # 初始位置[(x1,y1),(x2,y2),...]一个元组列表
            self.fruit = []  # 水果,[x,y]
            self.snake_length = START_SNAKE_SIZE
            self.direction = Direction.RIGHT  # 当前前进方向
            self.new_direction = Direction.RIGHT  # 用户按键后的前进方向
            self.game_state = None
            self.display = screen
            self.setup_game()
    
        def setup_game(self):
            """初始化游戏"""
            self.game_state = State.START
            direction = Direction.RIGHT
            new_direction = Direction.RIGHT
            self.reset_snake()
            self.generate_fruit()
            self.display.fill(0)
            self.draw_map()
            self.show_score()
            self.show_press_to_start()
            self.display.show()
    
        def reset_snake(self):
            """重设蛇的位置"""
            self.snake = []  # 重置
            self.snake_length = START_SNAKE_SIZE
            for i in range(self.snake_length):
                self.snake.append((MAP_SIZE_X // 2 - i, MAP_SIZE_Y // 2))
    
        def check_fruit(self):
            """检测蛇是否吃到水果,能否继续吃水果"""
            if self.snake[0][0] == self.fruit[0] and self.snake[0][1] == self.fruit[1]:
                if self.snake_length + 1 < MAX_SNAKE_LENGTH:
                    self.snake_length += 1
                    # 吃到水果后,将蛇增加一格
                    self.snake.insert(0, (self.fruit[0], self.fruit[1]))
                self.generate_fruit()
    
        def generate_fruit(self):
            """随机生成水果位置,注意不能生成在蛇身上"""
            while True:
                self.fruit = [random.randint(1, MAP_SIZE_X - 1), random.randint(1, MAP_SIZE_Y - 1)]
                fruit = tuple(self.fruit)
                if fruit in self.snake:
                    # 生成在蛇身上
                    continue
                else:
                    print('fruit: ', self.fruit)
                    break
    
        @staticmethod
        def button_press():
            """是否有按键按下"""
            for pin in UP_PIN, DOWN_PIN, LEFT_PIN, RIGHT_PIN:
                if pin.value() == 0:  # 低电平表示按下
                    return True
            return False
    
        def read_direction(self):
            """读取新的按键方向,不能与当前方向相反"""
            for direction, pin in enumerate((UP_PIN, LEFT_PIN, DOWN_PIN, RIGHT_PIN)):
                if pin.value() == 0 and not (direction == (self.direction + 2) % 4):
                    self.new_direction = Direction.setter(direction)
                    return
    
        def collection_check(self, x, y):
            """检查蛇社否撞到墙或者(x,y)位置"""
            for i in self.snake:
                if x == i[0] and y == i[1]:
                    return True
            if x < 0 or y < 0 or x >= MAP_SIZE_X or y >= MAP_SIZE_Y:
                return True
            return False
    
        def move_snake(self):
            """按照方向键移动蛇,返回能否继续移动的布尔值"""
            x, y = self.snake[0]
            new_x, new_y = x, y
    
            if self.direction == Direction.UP:
                new_y -= 1
            elif self.direction == Direction.DOWN:
                new_y += 1
            elif self.direction == Direction.LEFT:
                new_x -= 1
            elif self.direction == Direction.RIGHT:
                new_x += 1
    
            if self.collection_check(new_x, new_y):  # 不能继续移动
                return False
    
            self.snake.pop()  # 去除最后一个位置
            self.snake.insert(0, (new_x, new_y))  # 在开头添加新位置
            return True  # 能继续移动
    
        def draw_map(self):
            """绘制地图区域: 蛇、水果、边界"""
            offset_map_x = SCREEN_WIDTH - SNAKE_PIECE_SIZE * MAP_SIZE_X - 2
            offset_map_y = 2
    
            # 绘制水果
            self.display.rect(self.fruit[0] * SNAKE_PIECE_SIZE + offset_map_x,
                              self.fruit[1] * SNAKE_PIECE_SIZE + offset_map_y,
                              SNAKE_PIECE_SIZE, SNAKE_PIECE_SIZE, 1)
            # 绘制地图边界, 边界占一个像素,但是绘制时在内侧留一个像素,当蛇头部到达内部一个像素时,即判定为碰撞
            self.display.rect(offset_map_x - 2,
                              0,
                              SNAKE_PIECE_SIZE * MAP_SIZE_X + 4,
                              SNAKE_PIECE_SIZE * MAP_SIZE_Y + 4, 1)
            # 绘制蛇
            for x, y in self.snake:
                self.display.fill_rect(x * SNAKE_PIECE_SIZE + offset_map_x,
                                       y * SNAKE_PIECE_SIZE + offset_map_y,
                                       SNAKE_PIECE_SIZE,
                                       SNAKE_PIECE_SIZE, 1)
    
        def show_score(self):
            """显示得分"""
            score = self.snake_length - START_SNAKE_SIZE
            self.display.text('Score:%d' % score, 0, 2, 1)
    
        def show_press_to_start(self):
            """提示按任意键开始游戏"""
            self.display.text('Press', 0, 16, 1)
            self.display.text('button', 0, 26, 1)
            self.display.text('start!', 0, 36, 1)
    
        def show_game_over(self):
            """显示游戏结束"""
            self.display.text('Game', 0, 30, 1)
            self.display.text('Over!', 0, 40, 1)
    
    
    #################  循环运行程序  ##################
    if __name__ == '__main__':
        # print('******** Start ********')
        snake = Snake()
        move_time = 0
        while True:
            if snake.game_state == State.START:
                if Snake.button_press():
                    snake.game_state = State.RUNNING
    
            elif snake.game_state == State.RUNNING:
                move_time += 1
                snake.read_direction()
                if move_time >= SNAKE_MOVE_DELAY:
                    snake.direction = snake.new_direction
                    snake.display.fill(0)
                    if not snake.move_snake():
                        snake.game_state = State.GAMEOVER
                        snake.show_game_over()
                        time.sleep(1)
                    snake.draw_map()
                    snake.show_score()
                    snake.display.show()
                    snake.check_fruit()
                    move_time = 0
    
            elif snake.game_state == State.GAMEOVER:
                if Snake.button_press():
                    time.sleep_ms(500)
                    snake.setup_game()
                    print('******** new game ********')
                    snake.game_state = State.START
    
            time.sleep_ms(20)
    
    


    如果想了解代码的详细内容,可以自己阅读,代码都添加了注释,内容不算复杂,有Python基础的同学应该都可以看懂。如果有什么问题,欢迎下方评论区交流。

  7. OK,现在我们的项目就准备完成了,接下来,点击上方的“save”按钮保存程序,然后点击右侧模拟器的运行按钮,就可以运行我们的贪吃蛇小游戏了!


动图演示:

目前本人最高分76分(这是得有多无聊才能玩这么久),欢迎来挑战!!!

Enjoy IT!!!

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/886515.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号