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 getrandbits
class 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 True
return False
def 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_press
class 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 = 2
self.foodx, self.foody = 0, 0
self.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]-1
elif self.direct=='down':
self.y[0] = self.y[0]+1
elif self.direct=='left':
self.x[0] = self.x[0]-1
else:
self.x[0] = self.x[0]+1
# 吃到食物
if self.food_eat():
self.x.append(tmp_x)
self.y.append(tmp_y)
self.long += 1
self.food_create()
def is_dead(self):
'判断是否撞墙或自杀'
if self.x[0]<0 or self.x[0]>7:
return True
if self.y[0]<0 or self.y[0]>7:
return True
for i in range(1, self.long):
if self.x[0] == self.x[i] and self.y[0] == self.y[i]:
return True
return False
def food_create(self):
'创建食物'
while True:
self.foodx = getrandbits(3)
self.foody = getrandbits(3)
bad_food = False
for i in range(self.long):
if self.x[i] == self.foodx and self.y[i]==self.foody:
bad_food = True
break
if not bad_food:
break
def food_eat(self):
'判断吃到食物'
return self.x[0]==self.foodx and self.y[0]==self.foody
def 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+= 1
last_press_direct = btn.press()
if step > 50:
if last_press_direct != snake.direct:
snake.direct = last_press_direct # 方向有变化时才转向
snake.move()
if snake.is_dead():
break
else:
led.show(snake.drawdata())
step = 0
5. 实验效果
8x8LED点阵屏制作贪吃蛇游戏
目前存在的问题与改进方向:
- 小蛇的移动是的循环里面判断次数达到就移动一次,间隔不是精准的。可以使用micropython的定时器来改进;
- 吃到食物长身体时,那个间隔内小蛇没有移动,只是长了一个节点,可以改进一下;
- 撞墙或自杀后程序就卡死了,因为小蛇身体已经越界,使用绘制屏幕去亮屏时报错了,这里也可以改进;
- 后续可以增加启动、结束的闪屏效果;
- 小蛇的移动速度是固定的,可以改进为随着身体越来越长,移动速度也逐步加快;
就这些吧,然后这些改进我就不费时做了,需要的同(主)学(要)自(是)行(我)研(人)究(懒)。