MicroPython-On-ESP8266——8x8LED点阵模块(5)自制贪吃蛇游戏
1. 背景知识
连续折腾了一段时间的8x8点阵屏模块,从基本原理到驱动它显示滚动图案效果,常用的功能都使用到了。系列如下:
MicroPython-On-ESP8266——8x8LED点阵模块(1)驱动原理
MicroPython-On-ESP8266——8x8LED点阵模块(2)使用74HC595驱动
MicroPython-On-ESP8266——8x8LED点阵模块(3)使用MAX7219驱动
MicroPython-On-ESP8266——8x8LED点阵模块(4)基于MAX7219滚动显示字符/图案
由于我手上只有这么一块屏,没有做多屏串接显示的效果。那下一步咱们来继续折腾点啥。就基于MAX7219模块做个贪吃蛇游戏吧(掌机粉、诺基亚粉才懂为什么做这个)
2. 贪吃蛇原始分析
8x8点阵屏有64个led灯珠,从长度2开始,理论上可以贪吃成一条长度为63的小蛇。所以还是有一点可玩性的。
2.1. 游戏规则
- 地图(点阵屏)上初始有一条长度为2的小蛇,间隔一定时间保持惯性向蛇头方向移动
- 通过四个方向键可以控制小蛇的移动方向
- 初始在蛇身之外的地方随机有一个食物,蛇头碰到食物会把食物吃掉,小蛇长度加1,同时食物再随机产生一个
- 蛇头碰到边界或者自身,游戏结束
2.2. 程序逻辑
先构思一下程序需要实现的基础功能模块:
- 初始化
- 惯性移动
- 创建食物
- 判断吃到食物
- 判断撞墙或自杀
- 小蛇长身体
再根据模块组装一下程序流程:
2.3. 程序模块分析
基础铺垫:
A.我们基于屏幕坐标的方式来全局进行位置判断和屏幕绘制
B.用坐标表示食物,用两个数组来表示小蛇的身体
我们用数组来表示小蛇的身体,且把数组的第一个元素定义为蛇头的位置。基于此,
如果我们要判断吃到食物则用 snake_x[0] == food_x and snake_y[0] == food_y
如果我们判断小蛇撞墙则用snake_x[0] < 0 or snake_x[0] > 7 or snake_y[0] < 0 or snake_y[0] > 7
如果小蛇要长身体则用snake_x.append(...); snake_y.append(...)
这样大体逻辑就出来了。
模块原理拆解
模块 | 原理解释 |
---|---|
初始化 | 给小蛇固定一个初始长度(=2)和中间靠左一点点的初始位置 (1,3) 、(2,3) |
惯性移动 | 由外部按钮确定方向,初始向右,如果外部按键无操作,就保持当前方向且固定时间间隔地向该方向移动。移动的方法就是从尾巴开始遍历小蛇身体数组,让当前值等于数组的上一个值;而蛇头呢则要根据需要移动的方向去取下个位置的值。 |
创建食物 | 随机在屏幕范围内找个位置 (x食物, y食物),但不能与小蛇的身体重叠 |
判断吃到食物 | 这个上面讲到过了,蛇头坐标与食物坐标重合则吃到了 |
判断撞墙或自杀 | 撞墙是判断蛇头的坐标有没有越界屏幕坐标范围,自杀则是判断蛇头有没有跟任一个身体节点的坐标重合 |
小蛇长身体 | 先缓存下来蛇尾巴,当小蛇移动一次以后,再把缓存的坐标补到蛇尾巴后面 |
绘制图案 | 这个会有点绕。每次点亮屏幕前,先把所有位置都当作黑的,再依次把小蛇和食物对应的位置坐标转换为max7219驱动位数据。思路就是这么个思路,具体还是看后面代码慢慢理解吧。 |
用定时器或者固定间隔的循环来移动小蛇
3. 硬件及接线连接
程序需要不断扫描需要4个按键来确定上下左右四个方向,并保持最后一个按下的方向不变。再有就是使用MAX7219模块来驱动点阵屏。
接线示意图:
实物连接图:
按键我直接借用的一个焊废的板子上的4个触点按钮,板子斜过来用就是上下左右的布局。
4. 程序代码
上面已经解析了原理,这里直接整篇代码放上来吧
from machine import Pin
import time
from random import getrandbitsclass Button(object):'四个按钮,用简化接线方式,按钮线与地线进行判断'# def __init__(self, gpio_up=0, gpio_down=5, gpio_left=2, gpio_right=4):def __init__(self, gpio_up=0, gpio_down=4, gpio_left=5, gpio_right=2):self.btn_up = Pin(gpio_up, Pin.IN, pull=Pin.PULL_UP)self.btn_down = Pin(gpio_down, Pin.IN, pull=Pin.PULL_UP)self.btn_left = Pin(gpio_left, Pin.IN, pull=Pin.PULL_UP)self.btn_right = Pin(gpio_right, Pin.IN, pull=Pin.PULL_UP)self.last_press = 'right'def _check(self, _btn):if _btn.value() == 0:time.sleep_ms(20)if _btn.value() == 0:return Truereturn Falsedef press(self):if self._check(self.btn_up): self.last_press = 'up'if self._check(self.btn_down): self.last_press = 'down'if self._check(self.btn_left): self.last_press = 'left'if self._check(self.btn_right): self.last_press = 'right'return self.last_pressclass Matrix(object):'8x8LED点阵屏,MAX7219驱动'def __init__(self, gpio_din=13, gpio_clk=14, gpio_cs=15):'初始化'# 准备数据引脚self.pin_clk = Pin(gpio_clk, Pin.OUT, value=1) #D5,时钟,上升跳变时数据位移锁存self.pin_cs = Pin(gpio_cs, Pin.OUT, value=1) #D8,上升跳变时,数据全部推入锁存self.pin_din = Pin(gpio_din, Pin.OUT, value=1) #D7,待移入的数据self.model_init()def write_byte(self, data):"向芯片移入一个字节"for i in range(8):self.pin_clk.off()self.pin_din.value(1 if ((data << i) & 0x80) else 0) # 从高位开始送数据self.pin_clk.on()def write_data(self, addr, data):"写入地址与值"self.pin_cs.off()self.write_byte(addr)self.write_byte(data)time.sleep_us(5)self.pin_cs.on()def model_init(self):"初始化模块"self.write_data(0x0c, 0x00) #关断处于关闭状态 self.write_data(0x0f, 0x00) #不测试self.write_data(0x0b, 0x07) #扫描所有位码self.write_data(0x0a, 0x0F) #亮度0x07,半亮self.write_data(0x09, 0x00) #不译码self.write_data(0x0c, 0x01) #关断处于显示状态 def show(self, col_data):"亮屏控制,col_data需要为长度为8的数组"for line in range(8):self.write_data(line+1, col_data[line])class Snake(object):'贪吃蛇'def __init__(self):'''初始状态.........................00..... ->................................'''self.direct = 'right' # 初始移动方向self.x = [2, 1] #范围[0,7],第一个元素是蛇头x,蛇身加长时直接append(self.x[-1])self.y = [3, 3] #范围[0,7],第一个元素是蛇头y,蛇身加长时直接append(self.y[-1])self.long = 2self.foodx, self.foody = 0, 0self.food_create()def move(self):'移动'tmp_x, tmp_y = self.x[-1], self.y[-1]# 蛇身向蛇首路过的方向移动for i in range(self.long, 1, -1):self.x[i-1] = self.x[i-2]self.y[i-1] = self.y[i-2]# 处理蛇首if self.direct=='up':self.y[0] = self.y[0]-1elif self.direct=='down':self.y[0] = self.y[0]+1elif self.direct=='left':self.x[0] = self.x[0]-1else:self.x[0] = self.x[0]+1# 吃到食物if self.food_eat():self.x.append(tmp_x)self.y.append(tmp_y)self.long += 1self.food_create()def is_dead(self):'判断是否撞墙或自杀'if self.x[0]<0 or self.x[0]>7:return Trueif self.y[0]<0 or self.y[0]>7:return Truefor i in range(1, self.long):if self.x[0] == self.x[i] and self.y[0] == self.y[i]:return Truereturn Falsedef food_create(self):'创建食物'while True:self.foodx = getrandbits(3)self.foody = getrandbits(3)bad_food = Falsefor i in range(self.long):if self.x[i] == self.foodx and self.y[i]==self.foody:bad_food = Truebreakif not bad_food:breakdef food_eat(self):'判断吃到食物'return self.x[0]==self.foodx and self.y[0]==self.foodydef drawdata(self):'创建绘制图形数据'data = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] # 待绘制数据# 画蛇for i in range(self.long):data[self.y[i]] |= (1 << (7-self.x[i]))# 画食物data[self.foody] |= (1<<(7-self.foodx))return data# 初始各模块
btn = Button()
led = Matrix()
snake = Snake()led.show(snake.drawdata())
step = 0
while True:time.sleep_ms(10)step+= 1last_press_direct = btn.press()if step > 50:if last_press_direct != snake.direct:snake.direct = last_press_direct # 方向有变化时才转向snake.move()if snake.is_dead():breakelse:led.show(snake.drawdata())step = 0
5. 实验效果
8x8LED点阵屏制作贪吃蛇游戏
目前存在的问题与改进方向:
- 小蛇的移动是的循环里面判断次数达到就移动一次,间隔不是精准的。可以使用micropython的定时器来改进;
- 吃到食物长身体时,那个间隔内小蛇没有移动,只是长了一个节点,可以改进一下;
- 撞墙或自杀后程序就卡死了,因为小蛇身体已经越界,使用绘制屏幕去亮屏时报错了,这里也可以改进;
- 后续可以增加启动、结束的闪屏效果;
- 小蛇的移动速度是固定的,可以改进为随着身体越来越长,移动速度也逐步加快;
就这些吧,然后这些改进我就不费时做了,需要的同(主)学(要)自(是)行(我)研(人)究(懒)。