目录
简介
效果展示
源代码
main.py
ssd1306.py
实现思路
血量值
分数
恐龙
障碍物
得分与血量值的计算
简介
使用合宙esp32c3模块,基于micropython平台开发的一款oled小游戏,恐龙快跑,所有代码已经给出,将两个py文件放进esp32c3里即可运行,使用的是硬件i2c,这个ssd1306.py文件是我优化过的,许多用法可查看源码即可推敲,只支持128*64的I2C oled一定要用我提供的ssd1306驱动。
效果展示
esp32 micropython oled恐龙快跑
源代码
main.py
from ssd1306 import SSD1306_I2C
from machine import Pin,I2C,ADC
import time
import randomi2c=I2C(0,scl=Pin(5),sda=Pin(4))
oled=SSD1306_I2C(i2c)ps2x=ADC(Pin(2))
ps2x.atten(ADC.ATTN_11DB)
#生命值的绘制
live_list=[0x30,0x4C,0x42,0x21,0x21,0x42,0x4C,0x30]
#绘制恐龙
dinosaur=[0x03,0x01,0x00,0x00,0x00,0x00,0x01,0x03,0x07,0x7F,0xFF,0x8F,0xAF,0x8D,0xF9,0x78,
0xC0,0xE0,0xF0,0x78,0x7F,0xFD,0xF0,0xFC,0xFF,0xF9,0xF0,0xE0,0xC0,0x00,0x80,0x00]
#初始化屏幕
oled.fill(0)
oled.p8(live_list,0,0)
oled.p8(live_list,9,0)
oled.p8(live_list,18,0)
oled.text('0',120,0)
oled.p16(dinosaur,0,48)
oled.rect(117,53,10,10,True)
oled.show()
#设定游戏参数,一次跳跃用12帧数据处理
jump = 0
jump_num = [48,34,23,15,10,8]#根据恐龙的跳跃位置和自由落体运动规律得到的每帧绘制恐龙的位置
fall = False#降落标志
score = 0#分数
x = 0
live=3#生命值为3
#障碍物随机坐标
box1 = random.randint(40,120)
box2 = random.randint(40,120) + box1
speed = 3#初始游戏速度while True:#如果还有生命值if live:#当摇杆推动且恐龙在地平线上才触发跳动if ps2x.read()>2500 and jump == 0:jump = 1#一直升高直到最高点if jump != 0:jump += 1 if not fall else -1#降落到地面if jump == 0:fall = Falsescore += 1if jump == 5:fall = True#跳到最高点下落x += speedspeed = 3 + score//10#速度随积分增加#刷新屏幕oled.fill(0)#显示血量值for i in range(live):oled.p8(live_list,i*9,0)#更新分数oled.text(str(score),120,0)#画小恐龙的外形oled.p16(dinosaur,0,jump_num[jump])#画障碍物oled.rect(box1-x,55,4,8,True)oled.rect(box2-x,55,4,8,True)#判断当前障碍物过了屏幕,就把它变最后一个if box1+4-x <= 0:box1 = box2 box2 = box1 + random.randint(40,120)#判断是否碰到障碍物if 15 > box1-x > 12 and 48-jump_num[jump] < 14:live-=1score-=1time.sleep_ms(20)elif 0<=box1-x<4 and 48-jump_num[jump] < 14:live-=1#生命值为0,恐龙挂掉了else:oled.text('GAME_OVER',29,30)oled.show()
ssd1306.py
import framebuf
# 寄存器定义
SET_CONTRAST = const(0x81)
SET_ENTIRE_ON = const(0xa4)
SET_NORM_INV = const(0xa6)
SET_MEM_ADDR = const(0x20)
SET_COL_ADDR = const(0x21)
SET_PAGE_ADDR = const(0x22)
SET_DISP_START_LINE = const(0x40)
SET_DISP = const(0xae)
SET_SEG_REMAP = const(0xa0)
SET_MUX_RATIO = const(0xa8)
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)class SSD1306:def __init__(self,external_vcc):self.width = 128self.height = 64self.external_vcc = external_vccself.pages = 8self.init_display()def init_display(self):for cmd in (SET_DISP | 0x00, #熄屏SET_MEM_ADDR, 0x00, #水平寻址SET_DISP_START_LINE | 0x00,#显示起始行地址SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0SET_MUX_RATIO, 63,SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0SET_DISP_OFFSET, 0x00,SET_COM_PIN_CFG, 0x12,# timing and driving schemeSET_DISP_CLK_DIV, 0x80,SET_PRECHARGE, 0x22 if self.external_vcc else 0xf1,SET_VCOM_DESEL, 0x30, # 0.83*Vcc# displaySET_CONTRAST, 0xff, # maximumSET_ENTIRE_ON, # output follows RAM contentsSET_NORM_INV, # not inverted# charge pumpSET_CHARGE_PUMP, 0x10 if self.external_vcc else 0x14,0x2e, # 禁止滚动0xae | 0x01): #开屏self.write_cmd(cmd)self.fill(0)self.show()def v_scroll(self, d=1): self.write_cmd(0x2e) # 关闭滚动if d:self.write_cmd(0x26) # 向左self.write_cmd(0x00)self.write_cmd(0x07) # 起始页self.write_cmd(0x00) # 滚动帧率self.write_cmd(0x00) # 结束页else:self.write_cmd(0x27) # 向左self.write_cmd(0x00)self.write_cmd(0x00) # 起始页self.write_cmd(0x00) # 滚动帧率self.write_cmd(0x07) # 结束页self.write_cmd(0x00)self.write_cmd(0xff)self.write_cmd(0x2f) # 开启滚动def poweroff(self):self.write_cmd(const(0xae) | 0x00)#熄屏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 show(self):self.write_cmd(SET_COL_ADDR)self.write_cmd(0)self.write_cmd(127)self.write_cmd(SET_PAGE_ADDR)self.write_cmd(0)self.write_cmd(63)self.write_framebuf()def fill(self, c):self.framebuf.fill(c)def pixel(self, x, y, c):self.framebuf.pixel(x, y, c)def text(self, string, x, y, c=1):self.framebuf.text(string, x, y, c)def hline(self,x,y,w,c=1):self.framebuf.hline(x,y,w,c)def vline(self,x,y,h,c=1):self.framebuf.vline(x,y,h,c)def line(self,x1,y1,x2,y2,c=1):self.framebuf.line(x1,y1,x2,y2,c)def rect(self,x,y,w,h,c=1,f=False):self.framebuf.rect(x,y,w,h,c,f)def ellipse(self,x,y,xr,yr,c,f=False,m=15):self.framebuf.ellipse(x,y,xr,yr,c,f,m)def cube(self,x,y,l):self.rect(x,y,l,l)self.rect(x+int(0.5*l),int(y-0.5*l),l,l)self.line(x,y,int(x+0.5*l),int(y-0.5*l),1)self.line(x+l,y,int(x+1.5*l),int(y-0.5*l),1)self.line(x,y+l,int(x+0.5*l),int(y+0.5*l),1)self.line(x+l,y+l,int(x+1.5*l),int(y+0.5*l),1)def p8(self,page,x,y):for e in range(8):byte=bin(page[e]).replace('0b','')while len(byte)<8:byte='0'+bytefor i in range(8):if byte[i]=='1':self.pixel(x+e,y+i,int(byte[i]))def p16(self,page,x,y):for e in range(32):byte=bin(page[e]).replace('0b','')while len(byte)<8:byte='0'+bytefor i in range(8):if byte[i] and e<16:self.pixel(x+e,y+i,int(byte[i]))elif byte[i] and e>=16:self.pixel(x-16+e,y+8+i,int(byte[i]))def p32(self,page,x,y):for e in range(128):byte=bin(page[e]).replace('0b','')while len(byte)<8:byte='0'+bytefor i in range(8):if byte[i] and e<32:self.pixel(x+e,y+i,int(byte[i]))elif byte[i] and 32<=e<64:self.pixel(x+e-32,y+8+i,int(byte[i]))elif byte[i] and 64<=e<96:self.pixel(x+e-64,y+16+i,int(byte[i]))elif byte[i] and 96<=e<128:self.pixel(x+e-96,y+24+i,int(byte[i]))class SSD1306_I2C(SSD1306):def __init__(self,i2c, addr=0x3c, external_vcc=False):self.i2c = i2cself.addr = addrself.temp = bytearray(2)# buffer需要8 * 128的显示字节加1字节命令self.buffer = bytearray(8 * 128 + 1)self.buffer[0] = 0x40 # Co=0, D/C=1self.framebuf = framebuf.FrameBuffer1(memoryview(self.buffer)[1:], 128, 64)super().__init__(external_vcc)def write_cmd(self, cmd):self.temp[0] = 0x80 # Co=1, D/C#=0self.temp[1] = cmdself.i2c.writeto(self.addr, self.temp)def write_framebuf(self):self.i2c.writeto(self.addr, self.buffer)
实现思路
在python框架下开发游戏,思路很重要
血量值
用了8*8的矩形取模,是一个小爱心,代表血量值,在oled的左上角展示
分数
设置了游戏奖励分数,位于oled右上角
恐龙
拥有跳跃功能,符合自由落体运动规律,采用16*16取模方式构建,在oled左下角。恐龙跳起来分为6帧,落地也分为6帧,就是恐龙从地面跳到最高点时,刷新6次,用6个图像展示动态过程。根据自由落体运动规律,6帧平均每帧时间相同,由于恐龙只在y轴上运动,帧与帧之间的距离为1:3:5:7:9,用一个列表来记录这个规律,计算每一帧恐龙y坐标的位置,从最高点向下运动也是一样的。
障碍物
用4*8的矩形表示,设置两个并排的矩形,分别用随机数将其位置初始化,由于矩形只在x轴上移动,只需不停的移动矩形,改变矩形的x坐标即可,由于在恐龙的处理设置了12帧,不妨将障碍物一起嵌入这12帧中,在每一帧减小矩形的x坐标,每一帧减小的数量关系到矩形的移动速度。为了设置难度,把矩形移动速度与积分挂钩,积分越多,移动速度越大,难度越大,游戏越有意思,当然也可以用随机数处理,忽快忽慢,也很有意思。定义了两个矩形,(也可以根据个人感觉多定义几个),分别编号1,2,接近恐龙的为1号,可以实时计算矩形的坐标来检查矩形是否出界。分别用随机数表示两个矩形出现的位置,并把2号矩形加在1号矩形的后面,以随机数为它们之间的距离,由于不断减小矩形的坐标,并刷新,视觉上就看到了向左滚动的矩形,当第一个矩形出界后,就把2号矩形转为1号,同时新定义1个2号矩形,以一个随机数为距离加在1号矩形的后面,以此类推,就形成了一个迭代效果,源源不断的矩形滚动。
得分与血量值的计算
我把得分加在落地的瞬间,只要落地就加1分,产生了一种很奇妙的效果,玩家为了多得分,就要多跳,又不能碰到障碍物,无形中加了难度和趣味。
由于恐龙只在y轴上移动,矩形只在x轴移动,它们交叉的区域就是碰撞区域,可以计算损失血量,但是程序是在一个循环里的,esp32c3运行速度很快,如果把0-15的区域都设为碰撞区,会导致一次碰撞损失很多血量,我想要一次碰撞损失1滴血,就把碰撞区域缩小到两边,对应跳起和降落碰到障碍物两种情况。同时加了一定的延时使游戏感更好。
对于碰撞的判断,就相当于车祸双方,双方有交叉区域才能判定发生碰撞,当障碍物处于交叉区域,恐龙的跳跃高度没有跳出障碍区时,就判断发生碰撞,血量值减1。
当血量值为0时,游戏结束。
如果你有更好的想法,大家一起交流