在ESP32开发板上使用MicroPython编程实现一个贪吃蛇小游戏,游戏可以在ssd1306 OLED屏幕上游玩,使用四个按钮开关控制蛇的上下左右移动。
既然是手把手,就是让你不了解相关知识也能跟着流程运行起我们的项目,通过在线的仿真原件在线试玩。
项目所用工具介绍- ESP32开发板:上海乐鑫出品的MCU,自带wifi和蓝牙,功能和配置可以说非常良心。如果你有开发板的话,相信你对如何在上面跑程序非常了解了。如果没有开发板那我们也可以使用在线仿真网站Wokwi运行项目。效果和使用开发板是相同的。(接下来的示例就是在仿真网站运行的)
- MicroPython:为微控制器设计的运行语言,语法和PC端运行的CPython几乎完全一致,不过库的功能可能有所削减。MicroPython让微控制器可以直接运行python这样的解释型语言,避免了使用C/C++等编译型语言开发时的编译、链接和上传步骤。我们只需要把要用的脚本放到目录下,将主程序所在的脚本命名为main.py就可以自动运行程序了。
- SSD1306 OLED显示屏,用来显示游戏界面。
- 按键开关4个:用来控制上下左右四个方向,控制蛇的移动。
-
进入Wokwi网站,在开发板处选择MicroPython with ESP32,进入项目开发页面:
-
(不想动手连线的直接跳到第4步)在右侧的模拟器中添加我们使用的元器件,点击“+”,选择1个“SSD1306 OLED display”和4个“PushButton”:
-
添加元件后,按照下图连接引脚,连接时使用鼠标点击对应的引脚和想要连接的引脚就可以连线,点击导线还可以改变导线的颜色:
-
如果不想自己连线或怕连线出错,可以直接复制下面的代码,粘贴到左侧编辑窗口上方的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" ] ] ] }
粘贴后,右侧模拟器窗口就会自动显示连好线的电路图。 -
添加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) -
最后,添加我们的主程序:
在编辑栏中,选择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基础的同学应该都可以看懂。如果有什么问题,欢迎下方评论区交流。 -
OK,现在我们的项目就准备完成了,接下来,点击上方的“save”按钮保存程序,然后点击右侧模拟器的运行按钮,就可以运行我们的贪吃蛇小游戏了!
动图演示:
目前本人最高分76分(这是得有多无聊才能玩这么久),欢迎来挑战!!!
Enjoy IT!!!



