2021暑假一起练:树莓派PICO扩展功能板上的贪吃蛇小游戏
基于2021暑假练平台2:树莓派PICO的扩展功能板(含pico模块),选用MicroPython为编程语言,实现了对经典贪吃蛇小游戏的复刻。
标签
嵌入式系统
MPU
树梅派
Puff-shroom
更新2021-09-07
1070

一、项目背景

硬禾课堂开展了“2021暑假练活动”,完成项目就能免费获得板卡,这让更多的同学们参与进来,让同学们学有成效,真正将所学知识用于实践。

二、项目要求

  1. 设计或移植一款经典的游戏,通过LCD屏显示,通过按键和旋转编码器控制
  2. 在游戏中要通过蜂鸣器播放背景音乐

三、项目功能

   1.运行程序,按任意键游戏开始。

   2.游戏时播放背景音乐。

   3.根据得分调整贪吃蛇移动速率,调整游戏难度。

   4.实时显示当前分数和历史最高分。

   5.根据游戏结束后将游戏数据保存,待死亡动画结束后按任意键可重新开始游戏。

四、项目实现

拿到pico拓展功能板的第一件事肯定就是了解设备的具体信息,熟悉电路和引脚位置。我这里放上几张原理图,方便大家查阅。FpHeq4UpR1jWO1us9vul2yQAnCp_Fg9iiiNvXcsA0u6SfchQzU4fPDLRFny7hzNScXCOrdM37MhPUinMFjcs

PICO官方支持MicroPython和C/C++ 两种编程语言。考虑到为了使用现有的ST7789驱动显示屏,作者选择了使用MicroPython进行编程。

关于IDE的配置,MicroPython拥有很多不错的IDE,在这里给大家推荐Thonny,由爱沙尼亚的 Tartu 大学开发,轻量化的同时,兼具了很多必要的功能。具体的安装方法可以参见https://www.eetree.cn/project/detail/257

为了往PICO中拷贝程序,我们还需要下载一个UF2文件,链接https://pico.org.cn/。将文件复制到PICO中之后,我们就可以开始对它进行编程了,准备好开始了吗?

为了实现项目要求,我一共编写了两个py文件,分别是game.py和snake.py,前者为游戏的主程序,后者涉及生命判断,图像显示等功能函数。

(一)图像显示模块

ST7789库提供了pixel,line,fill等强大的功能,本项目首先初期采用单纯的pixel函数,得到的贪吃蛇太过于细长,将单个像素点扩充为一个边长为9的正方形,然后采用pixel函数对正方形中的每一个点进行绘制,增加了蛇的宽度。蛇身移动的基本逻辑为添头+去尾(将原有的区域重新填涂为黑色),具体代码如下。(其中苹果采用相同的方法绘制,颜色选用红色)

def multiple(a):
    l=[]
    for i in range(a[0]-4,a[0]+5):
        for j in range(a[1]-4,a[1]+5):
            l.append([i,j])
    return l
def snake_oled(display,apple,snake,l2):
    
    for i in multiple(apple):
        display.pixel(i[0],i[1],st7789.RED)
    
    for n in multiple(snake[0]):
        display.pixel(n[0],n[1],st7789.WHITE)
    
    for c in multiple(l2):
        display.pixel(c[0],c[1],st7789.BLACK)

(二)生死判断与得分判断

贪吃蛇一共有两种死亡方式,第一种为撞墙,第二种是吃到了自己的尾巴。检测方法非常简单,第一种表现为蛇头坐标超出范围,第二种表现为蛇头坐标与某一蛇身坐标相同。

得分判断方法相似,代码如下:

def juggl(self):
    head=self.snake[0]
    if head in self.snake[1:]:
        return False
    if head[0]<10 or head[0]>230 or head[1]<70 or head[1]>230:
        return False
    return True
def jugga(self,snakes):
    if self.apple in snakes:
        self.randapple()
        return True
    else:
        return False

(三)方向控制

由于PICO拓展版只给了两个按键,我才用了一种有些别扭的操作方式,即Key1对应逆时针转弯(面向前进方向的左转),Key2对应顺时针旋转(面向前进方向的右转)。具体的实现方式为将上、右、下、左四个前进方向映射为0、1、2、3四个数字。当Key1按下时,数字减一,如从2变化到1,即实现了一次逆时针旋转。代码如下:

def do(self,types=1,q=1):
    ne=[]
    if types==0:
        m=0
        n=-10
    elif types==1:
        m=10
        n=0
    elif types==2:
        m=0
        n=10
    elif types==3:
        m=-10
        n=0
    
    l=self.snake[0]
    ne=[l[0]+m,l[1]+n]
    if self.juggl():
        if self.jugga(self.snake):
            q=[ne,]+self.snake
            self.snake=q
        else:
            q=[ne,]+self.snake
            self.snake=q
            self.snake.pop()                
        return True
    else:
        return False

(四)音频播放

关于音频播放的模块,我参考了别人写的一个库,具体可参考https://www.eetree.cn/project/detail/272

先将部分代码以buzzer_music.py为文件名保存在pico中。然后再在程序主循环中加入乐符发声的语句就可以了,因为为我们的游戏主程序也是一个循环。大家也可以根据自己的喜好选择合适的音乐进行播放,或者使用耳机来输出。代码如下

"""
Micropython (Raspberry Pi Pico)

Plays music written on onlinesequencer.net through a monophonic passive piezo buzzer.
Uses fast arpeggios to simulate polyphony

https://github.com/james1236/buzzer_music
"""

from machine import Pin, PWM
from math import ceil

tones = {
    'C0':16,
    'C#0':17,
    'D0':18,
    'D#0':19,
    'E0':21,
    'F0':22,
    'F#0':23,
    'G0':24,
    'G#0':26,
    'A0':28,
    'A#0':29,
    'B0':31,
    'C1':33,
    'C#1':35,
    'D1':37,
    'D#1':39,
    'E1':41,
    'F1':44,
    'F#1':46,
    'G1':49,
    'G#1':52,
    'A1':55,
    'A#1':58,
    'B1':62,
    'C2':65,
    'C#2':69,
    'D2':73,
    'D#2':78,
    'E2':82,
    'F2':87,
    'F#2':92,
    'G2':98,
    'G#2':104,
    'A2':110,
    'A#2':117,
    'B2':123,
    'C3':131,
    'C#3':139,
    'D3':147,
    'D#3':156,
    'E3':165,
    'F3':175,
    'F#3':185,
    'G3':196,
    'G#3':208,
    'A3':220,
    'A#3':233,
    'B3':247,
    'C4':262,
    'C#4':277,
    'D4':294,
    'D#4':311,
    'E4':330,
    'F4':349,
    'F#4':370,
    'G4':392,
    'G#4':415,
    'A4':440,
    'A#4':466,
    'B4':494,
    'C5':523,
    'C#5':554,
    'D5':587,
    'D#5':622,
    'E5':659,
    'F5':698,
    'F#5':740,
    'G5':784,
    'G#5':831,
    'A5':880,
    'A#5':932,
    'B5':988,
    'C6':1047,
    'C#6':1109,
    'D6':1175,
    'D#6':1245,
    'E6':1319,
    'F6':1397,
    'F#6':1480,
    'G6':1568,
    'G#6':1661,
    'A6':1760,
    'A#6':1865,
    'B6':1976,
    'C7':2093,
    'C#7':2217,
    'D7':2349,
    'D#7':2489,
    'E7':2637,
    'F7':2794,
    'F#7':2960,
    'G7':3136,
    'G#7':3322,
    'A7':3520,
    'A#7':3729,
    'B7':3951,
    'C8':4186,
    'C#8':4435,
    'D8':4699,
    'D#8':4978,
    'E8':5274,
    'F8':5588,
    'F#8':5920,
    'G8':6272,
    'G#8':6645,
    'A8':7040,
    'A#8':7459,
    'B8':7902,
    'C9':8372,
    'C#9':8870,
    'D9':9397,
    'D#9':9956,
    'E9':10548,
    'F9':11175,
    'F#9':11840,
    'G9':12544,
    'G#9':13290,
    'A9':14080,
    'A#9':14917,
    'B9':15804
}

#Time, Note, Duration, Instrument (onlinesequencer.net schematic format)
#0 D4 8 0;0 D5 8 0;0 G4 8 0;8 C5 2 0;10 B4 2 0;12 G4 2 0;14 F4 1 0;15 G4 17 0;16 D4 8 0;24 C4 8 0

class music:
    def __init__(self, songString, looping=True, tempo=3, duty=2512):
        self.tempo = tempo
        self.song = songString
        self.looping = looping
        self.duty = duty
        
        self.stopped = False
        
        self.timer = -1
        self.beat = -1
        self.arpnote = 0
        
        self.pwm = PWM(Pin(16))
        
        self.notes = []

        self.playingNotes = []
        self.playingDurations = []


        #Find the end of the song
        self.end = 0
        splitSong = self.song.split(";")
        for note in splitSong:
            snote = note.split(" ")
            testEnd = round(float(snote[0])) + ceil(float(snote[2]))
            if (testEnd > self.end):
                self.end = testEnd
                
        #Create empty song structure
        while (self.end > len(self.notes)):
            self.notes.append(0)

        #Populate song structure with the notes
        for note in splitSong:
            snote = note.split(" ")
            beat = round(float(snote[0]));
            
            if (self.notes[beat] == 0):
                self.notes[beat] = []
            self.notes[beat].append([snote[1],ceil(float(snote[2]))]) #Note, Duration


        #Round up end of song to nearest bar
        self.end = ceil(self.end / 8) * 8
    
    def stop(self):
        self.pwm.deinit()
        self.stopped = True
        
    def tick(self):
        if (not self.stopped):
            self.timer = self.timer + 1
            
            #Loop
            if (self.timer % (self.tempo * self.end) == 0 and (not (self.timer == 0))):
                if (not self.looping):
                    self.stop()
                    return False
                self.beat = -1
                self.timer = 0
            
            #On Beat
            if (self.timer % self.tempo == 0):
                self.beat = self.beat + 1

                #Remove expired notes from playing list
                i = 0
                while (i < len(self.playingDurations)):
                    self.playingDurations[i] = self.playingDurations[i] - 1
                    if (self.playingDurations[i] <= 0):
                        self.playingNotes.pop(i)
                        self.playingDurations.pop(i)
                    else:
                        i = i + 1
                        
                #Add new notes and their durations to the playing list
                
                """
                #Old method runs for every note, slow to process on every beat and causes noticeable delay
                ssong = song.split(";")
                for note in ssong:
                    snote = note.split(" ")
                    if int(snote[0]) == beat:
                        playingNotes.append(snote[1])
                        playingDurations.append(int(snote[2]))
                """
                
                if (self.beat < len(self.notes)):
                    if (self.notes[self.beat] != 0):
                        for note in self.notes[self.beat]:
                            self.playingNotes.append(note[0])
                            self.playingDurations.append(note[1])
                
                #Only need to run these checks on beats
                if ((len(self.playingNotes) == 0)):
                    self.pwm.duty_u16(0)
                elif (len(self.playingNotes) == 1):
                    #Play note
                    self.pwm.duty_u16(self.duty)
                    self.pwm.freq(tones[self.playingNotes[0]])
            

            #Play arp of all playing notes
            if (len(self.playingNotes) > 1):
                self.pwm.duty_u16(self.duty)
                if (self.arpnote >= len(self.playingNotes)):
                    self.arpnote = 0
                self.pwm.freq(tones[self.playingNotes[self.arpnote]])
                self.arpnote = self.arpnote + 1
                
            return True
        else:
            return False

五、项目总结

本次活动是我第一次尝试单片机编程,勇敢的迈出了第一步,由于本人刚刚大一,受限于专业知识水平,和实践经验的不足。对于很多东西,尤其是硬件方面的逻辑还不甚了解,这是下一步要学习的内容。经验的话就是不要对一些自己不太了解的东西感到害怕,学会在探索中学习。再一点就是模块化编程很重要。

本项目的不足与改进:第一是,由于游戏主程序比较长,在运行时偶尔会出现按键检测失灵的情况,需要玩家进行一定的适应。第二是编程风格的问题,由于自己不太记得对程序进行注释,导致程序可读性的下降。

最后感谢电子森林、硬禾学堂提供这次学习交流的机会。

大家有任何问题的话可以通过B站私信我。

 

附件下载
program.zip
团队介绍
国防科技大学电子科学学院
团队成员
张晟锴
电子科学与技术专业大二学生
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号