初入Python(一) Pygame贪吃蛇游戏的编写与改进

贪吃蛇游戏是一款简单耐玩的休闲益智类游戏,利用pygame可以实现轻松编写,不需要辅佐图片等等元素,可以直接利用涂色方块表示,吃果子变长的原理也很容易实现,将body分为一块一块,每块有自己的位置属性,从而可以轻松表示出来。通过对贪吃蛇游戏的编写和改进可以在pygame模块的应用以及面向对象编程的编程方面得到不错的练习效果。
作为一个初学者,我也打算利用这种方式对学习进行一个有趣的总结,maybe效果不错哦!

初步编写——借鉴的艺术

初步编写的代码来自于简书上的一个程序,看起来清晰好懂。

https://gitee.com/codetimer/Snake/blob/master/main.py

前期定义

这部分主要导入库以及定义游戏窗口大小

import pygame
import sys
import random

# 全局定义
SCREEN_X = 600
SCREEN_Y = 600

蛇类的定义

蛇类拥有方向和身体块两个属性,蛇块的增加、减少和移动都可以利用列表的增删轻松实现,方向的判断处要注意左右、上下方向不能被直接逆向改变。死亡判断处,直接用头部方块的位置做if判断即可。

# 蛇类 点以25为单位
class Snake(object):
    # 初始化各种需要的属性 [开始时默认向右/身体块x5]
    def __init__(self):
        self.dirction = pygame.K_RIGHT
        self.body = []
        for x in range(5):
            self.addnode()
        
    # 无论何时 都在前端增加蛇块
    def addnode(self):
        left,top = (0,0)
        if self.body:
            left,top = (self.body[0].left,self.body[0].top)
        node = pygame.Rect(left,top,25,25)
        if self.dirction == pygame.K_LEFT:
            node.left -= 25
        elif self.dirction == pygame.K_RIGHT:
            node.left += 25
        elif self.dirction == pygame.K_UP:
            node.top -= 25
        elif self.dirction == pygame.K_DOWN:
            node.top += 25
        self.body.insert(0,node)
        
    # 删除最后一个块
    def delnode(self):
        self.body.pop()
        
    # 死亡判断
    def isdead(self):
        # 撞墙
        if self.body[0].x  not in range(SCREEN_X):
            return True
        if self.body[0].y  not in range(SCREEN_Y):
            return True
        # 撞自己
        if self.body[0] in self.body[1:]:
            return True
        return False
        
    # 移动!
    def move(self):
        self.addnode()
        self.delnode()
        
    # 改变方向 但是左右、上下不能被逆向改变
    def changedirection(self,curkey):
        LR = [pygame.K_LEFT,pygame.K_RIGHT]
        UD = [pygame.K_UP,pygame.K_DOWN]
        if curkey in LR+UD:
            if (curkey in LR) and (self.dirction in LR):
                return
            if (curkey in UD) and (self.dirction in UD):
                return
            self.dirction = curkey

食物类的定义

食物类主要包含一个方块,remove和set方法都是对食物的坐标进行判断,这里需要注意pygame中rect方法的原理。

# 食物类 点以25为单位
class Food:
    def __init__(self):
        self.rect = pygame.Rect(-25,0,25,25)
        
    def remove(self):
        self.rect.x=-25
    
    def set(self):
        if self.rect.x == -25:
            allpos = []
            # 不靠墙太近 25 ~ SCREEN_X-25 之间
            for pos in range(25,SCREEN_X-25,25):
                allpos.append(pos)
            self.rect.left = random.choice(allpos)
            self.rect.top  = random.choice(allpos)
            print(self.rect)

pygame.Rect()
通过Rect可以创造一个矩形区域,可以由LEFT,TOP,WIDTH,HEIGHT这四个值创建。
初入Python(一) Pygame贪吃蛇游戏的编写与改进_第1张图片
也可以用来裁剪图片
eg:
加载图片img = pygame.image.load(r’D:\Python\images\xxx.png’)
剪切图片rect = pygame.Rect(LEFT,TOP,WIDTH,HEIGHT)

显示与主函数

show_text没有什么可说的,对应好参数就OK。main函数的逻辑基本可以分解为——初始化、进入循环、检测事件、运动并更新蛇身、判断是否吃到并完成食物投递、判断死亡、显示分数。蛇运动的速度由clock.tick()中的参数决定,它决定了游戏绘制的帧率,参数越大,运动速度越大。

def show_text(screen, pos, text, color, font_bold = False, font_size = 60, font_italic = False):   
    #获取系统字体,并设置文字大小  
    cur_font = pygame.font.SysFont("宋体", font_size)  
    #设置是否加粗属性  
    cur_font.set_bold(font_bold)  
    #设置是否斜体属性  
    cur_font.set_italic(font_italic)  
    #设置文字内容  
    text_fmt = cur_font.render(text, 1, color)  
    #绘制文字  
    screen.blit(text_fmt, pos)

     
def main():
    pygame.init()
    screen_size = (SCREEN_X,SCREEN_Y)
    screen = pygame.display.set_mode(screen_size)
    pygame.display.set_caption('Snake')
    clock = pygame.time.Clock()
    scores = 0
    isdead = False
    
    # 蛇/食物
    snake = Snake()
    food = Food()
    
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
            if event.type == pygame.KEYDOWN:
                snake.changedirection(event.key)
                # 死后按space重新
                if event.key == pygame.K_SPACE and isdead:
                    return main()
                
            
        screen.fill((255,255,255))
        
        # 画蛇身 / 每一步+1分
        if not isdead:
            scores+=1
            snake.move()
        for rect in snake.body:
            pygame.draw.rect(screen,(20,220,39),rect,0)
            
        # 显示死亡文字
        isdead = snake.isdead()
        if isdead:
            show_text(screen,(100,200),'YOU DEAD!',(227,29,18),False,100)
            show_text(screen,(150,260),'press space to try again...',(0,0,22),False,30)
            
        # 食物处理 / 吃到+50分
        # 当食物rect与蛇头重合,吃掉 -> Snake增加一个Node
        if food.rect == snake.body[0]:
            scores+=50
            food.remove()
            snake.addnode()
        
        # 食物投递
        food.set()
        pygame.draw.rect(screen,(136,0,21),food.rect,0)
        
        # 显示分数文字
        show_text(screen,(50,500),'Scores: '+str(scores),(223,223,223))
        
        pygame.display.update()
        clock.tick(10)
    
    
if __name__ == '__main__':
    main()

初步效果

死亡后按空格重新开始。

优化过程

有了上面的成果,一个贪吃蛇游戏也就基本完成了,有意思么,还行吧。不过可能是个人对于游戏的痴迷让我觉得这个游戏总是少了点什么,或者说,太不完善了,只是单纯有它该有的功能罢了,什么游戏UI,闯关的感觉,都是没有的。所以,让我们来开动脑筋,试着去改造它吧,我将其与平常玩的游戏做了点小对比,提出了一点点优化目标,其中有些比较简单,有些也会有点困难,需要我们推翻现在的代码重新进行架构。

优化目标

1.cfg和函数的封装;
2.开始ui和结束ui;
3.难度设置(选择蛇的运行速度);
*4.障碍物的设置\特殊方块的设置(高得分方块、暂时减速方块);
*5.对于帧数的优化(帧数固定,即每次运动之间进行多次检测和输出);
*6.分数排行榜(玩之前输入用户名,这需要涉及到文件读写的操作)。
目前大概就这么多,打星的还有待考虑中。

cfg文件和函数的封装

cfg文件的封装我觉得对于开发来说至关重要,就像csgo中的config文件,玩家也可以改来改去,添加一些想要的参数(指插入二刺螈图片做背景),增加了游戏的可玩性。虽然这个贪吃蛇游戏没有太多的参数需要设置,但是我觉得养成一个好习惯是不错的。

import pygame


# 方块大小
cube = 25
pygame.init()
# 屏幕大小
SCREENSIZE_X = 600
SCREENSIZE_Y = 600

# highest scores的text
pos1 = (50, 500)
text1 = 'Highest Scores: '
color1 = (223, 223, 223)
font_size1 = 30
# curscores 的text
pos2 = (50, 550)
text2 = 'Scores: '
color2 = (223, 223, 223)
font_size2 = 30

# UI字体
UIfont_size_big = 60
UIfont_size_small = 30
UIfont1 = pygame.font.SysFont("宋体", UIfont_size_big)
UIfont2 = pygame.font.SysFont("宋体", UIfont_size_small)
UIcolor1 = (255, 151, 23)
UIcolor2 = (255, 151, 23)

cfg文件代码如上,其实有些参数我并没有设置过来,因为config中也不是需要包含所有参数的,有的参数如果改起来可行性不高我觉得也没必要全部放在这里,反而会很乱。
部分后面要用的函数封装如下(当然所有优化指标还没完全实现,因此必然还会改变):

'''other_function.py'''
import pygame
import sys


def show_scores(screen, scores, pos, text, color, font_size=30):
	pass
def StartUI(screen, cfg):
	pass
def EndUI(screen, cfg):
	pass

当然,蛇类和食物类也要封装起来,就直接封装就好。

'''Food.py'''
import pygame
import random

class Food(object):

    def __init__(self):
        self.rect = pygame.Rect(-25, 0, 25, 25)

    def remove(self):
        self.rect.x = -25

    def set(self, SCREENSIZE_X=600):
        if self.rect.x == -25:
            allpos = []
            # 不靠近墙
            for pos in range(25, SCREENSIZE_X - 25, 25):
                allpos.append(pos)
            self.rect.left = random.choice(allpos)
            self.rect.top = random.choice(allpos)
            print(self.rect)
'''Snake.py'''
import pygame


class Snake(object):
    def __init__(self):
        # 设定初始方向为向右 初始身体为空
        self.direction = pygame.K_RIGHT
        self.body = []

        # 初始化5个身体块
        for x in range(5):
            self.addnote()

    def addnote(self):
        # left top为身体块的定位位置
        left, top = (0, 0)
        if self.body:
            left, top = (self.body[0].left, self.body[0].top)
        node = pygame.Rect(left, top, 25, 25)
        if self.direction == pygame.K_LEFT:
            node.left -= 25
        elif self.direction == pygame.K_RIGHT:
            node.left += 25
        elif self.direction == pygame.K_UP:
            node.top -= 25
        elif self.direction == pygame.K_DOWN:
            node.top += 25
        self.body.insert(0, node)

    # 删除身体块
    def delnote(self):
        self.body.pop()

    def isdead(self, SCREENSIZE_X=600, SCREENSIZE_Y=600):
        # 撞墙
        if self.body[0].x not in range(SCREENSIZE_X):
            return True
        if self.body[0].y not in range(SCREENSIZE_Y):
            return True
        # 撞自己
        if self.body[0] in self.body[1:]:
            return True
        return False

    def move(self):
        # 在前进方向上头部增加一个方块 尾部减少一个方块
        self.addnote()
        self.delnote()

    def changedirection(self, curkey):
        LR = [pygame.K_LEFT, pygame.K_RIGHT]
        UD = [pygame.K_UP, pygame.K_DOWN]
        if curkey in LR + UD:
            if (curkey in LR) and (self.direction in LR):
                return
            if (curkey in UD) and (self.direction in UD):
                return
            self.direction = curkey

需要注意的点有:
1.利用cfg文件分装后,文件存放目录要进行改变,像下图一样:
初入Python(一) Pygame贪吃蛇游戏的编写与改进_第2张图片
同时main函数中的库的导入也应该改变(面向对象的基础~),比如:

import snake_cfg
from functions.Snake import *
from functions.Food import *
from functions.other_function import *

2.调用cfg中的参数时,要加前缀“cfg.”当然这取决于你导入的方式。
3.每个函数的参数都需要重写。同时为了让主函数循环运行,采用了如下的结构:

if __name__ == '__main__':
    while True:
        if not main(snake_cfg):
            break

main函数的返回值是由结束UI决定的,这部分需要结合后面的结束部分自己去理解。

def main(cfg):
    pygame.init()
    #省略......
    ...
    while not isdead:
    ...
    ...
    ...
        # 设置时间间隔
        clock.tick(difficulty)
    # 如果死亡 返回输出UI
    return EndUI(screen, cfg)

开始UI和结束UI

开始UI和结束UI是什么,其实经常玩游戏的人都很清楚,一个背景,几个选项,不同的选项导向不同的结果,比如开始游戏、继续游戏,选择难度、退出游戏等等。这里我们就实行一个简单的UI,开始UI包括两个难度的选择level1、level2(当然你想写10个都行),这与下一部分难度设置相重合,结束UI包括restart和quit.
UI的设计也是主要依托pygame中的显示功能,前面初步编写时也并没有谈到pygame中显示方式的使用,比如pygame.display.set_mode、screen.fill、pygame.draw.rect、font.render、surface.blit,因为这部分的内容实在是太繁琐,需要自己去查找与学习。
简单来说,开始UI分为几个部分:
文字的定义与显示、方块的创建与按钮的绑定、事件的选择。

def StartUI(screen, cfg):
    # 欢迎为大号字体
    cur_font1 = cfg.UIfont1
    text_fmt1 = cur_font1.render('Welcome!', False, cfg.UIcolor1)
    # 选项为小号字体 选项1为level1 选项2为level2
    cur_font2 = cfg.UIfont2
    text_fmt2 = cur_font2.render('level1', False, cfg.UIcolor2)
    text_fmt3 = cur_font2.render('level2', False, cfg.UIcolor2)
    # alpha通道起到滤镜效果 填充一层模糊界面
    surface = screen.convert_alpha()
    surface.fill((127, 255, 212, 2))
    # 定义文字位置并创建按钮
    text_rect1 = text_fmt1.get_rect()
    text_rect1.centerx, text_rect1.centery = cfg.SCREENSIZE_X / 2, cfg.SCREENSIZE_Y / 2 - 50
    # 显示文字1 即欢迎选项
    surface.blit(text_fmt1, text_rect1)
    # 定义按钮的位置 以屏幕中心位置开始进行微调(先随便给个数再调也行)
    button_width, button_height = 100, 40
    button_start_x_left = cfg.SCREENSIZE_X / 2 - button_width - 20
    button_start_x_right = cfg.SCREENSIZE_X / 2 + 20
    button_start_y = cfg.SCREENSIZE_Y / 2 - button_height / 2 + 20
    pygame.draw.rect(surface, (0, 255, 255), (button_start_x_left, button_start_y, button_width, button_height))
    # 定义按钮1的矩形
    text_level1_rect = text_fmt2.get_rect()
    text_level1_rect.centerx, text_level1_rect.centery = button_start_x_left + button_width / 2, button_start_y + button_height / 2
    surface.blit(text_fmt2, text_level1_rect)
    # 定义按钮2的矩形
    pygame.draw.rect(surface, (0, 255, 255), (button_start_x_right, button_start_y, button_width, button_height))
    text_level2_rect = text_fmt3.get_rect()
    text_level2_rect.centerx, text_level2_rect.centery = button_start_x_right + button_width / 2, button_start_y + button_height / 2
    surface.blit(text_fmt3, text_level2_rect)
    while True:
        screen.blit(surface, (0, 0))
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == pygame.MOUSEBUTTONDOWN and event.button:
                # 鼠标选择level1 返回死亡为False 速度为10
                if text_level1_rect.collidepoint(pygame.mouse.get_pos()):
                    return False, 10
                # 鼠标选择level2 返回死亡为False 速度为15
                if text_level2_rect.collidepoint(pygame.mouse.get_pos()):
                    return False, 15
        # 更新显示状态
        pygame.display.update()

你问我字体的参数和操作?查百度。你问我矩形和按钮怎么创建绑定?查百度。你问我键盘输入输出key,鼠标点击判断是怎么回事?查百度。
一些资料:

pygame.key 键值说明:
https://blog.csdn.net/stoneyyhit/article/details/52259993
pygame 字体设置:
https://blog.csdn.net/zengxiantao1994/article/details/58590594
pygame 屏幕显示
https://zhuanlan.zhihu.com/p/99450316
RGB查询:
https://www.fontke.com/tool/rgb/ffffff/

结束UI和开始UI几乎完全一致,只需要改变下文本和按钮的绑定即可:

def EndUI(screen, cfg):
    # 欢迎为大号字体
    cur_font1 = cfg.UIfont1
    text_fmt1 = cur_font1.render('Game Over!', False, cfg.UIcolor1)
    # 选项为小号字体 选项1为level1 选项2为level2
    cur_font2 = cfg.UIfont2
    text_fmt2 = cur_font2.render('restart', False, cfg.UIcolor2)
    text_fmt3 = cur_font2.render('quit', False, cfg.UIcolor2)
    # alpha通道起到滤镜效果 填充一层模糊界面
    surface = screen.convert_alpha()
    surface.fill((127, 255, 212, 2))
    # 定义文字位置并创建按钮
    text_rect1 = text_fmt1.get_rect()
    text_rect1.centerx, text_rect1.centery = cfg.SCREENSIZE_X / 2, cfg.SCREENSIZE_Y / 2 - 50
    # 显示文字1 即欢迎选项
    surface.blit(text_fmt1, text_rect1)
    # 定义按钮的位置 屏幕中心位置
    button_width, button_height = 100, 40
    button_start_x_left = cfg.SCREENSIZE_X / 2 - button_width - 20
    button_start_x_right = cfg.SCREENSIZE_X / 2 + 20
    button_start_y = cfg.SCREENSIZE_Y / 2 - button_height / 2 + 20
    pygame.draw.rect(surface, (0, 255, 255), (button_start_x_left, button_start_y, button_width, button_height))
    # 定义按钮1的矩形
    text_restart_rect = text_fmt2.get_rect()
    text_restart_rect.centerx, text_restart_rect.centery = button_start_x_left + button_width / 2, button_start_y + button_height / 2
    surface.blit(text_fmt2, text_restart_rect)
    # 定义按钮2的矩形
    pygame.draw.rect(surface, (0, 255, 255), (button_start_x_right, button_start_y, button_width, button_height))
    text_quit_rect = text_fmt3.get_rect()
    text_quit_rect.centerx, text_quit_rect.centery = button_start_x_right + button_width / 2, button_start_y + button_height / 2
    surface.blit(text_fmt3, text_quit_rect)
    while True:
        screen.blit(surface, (0, 0))
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == pygame.MOUSEBUTTONDOWN and event.button:
                # 鼠标选择restart 返回死亡为False
                if text_restart_rect.collidepoint(pygame.mouse.get_pos()):
                    return True
                # 鼠标选择quit 返回死亡为True
                if text_quit_rect.collidepoint(pygame.mouse.get_pos()):
                    return False
        pygame.display.update()

难度设置

开始UI的编写和难度设置是同时进行的,观察前面的开始UI可以发现开始UI中的选项level1和level2分别对应了两组不同的return值,在main函数中,在循环正式开始前,会有这么一行代码:

isdead, difficulty = StartUI(screen, cfg)

其中,isdead是while循环继续的判断条件,也是游戏继续进行的“钥匙”,而difficulty则是开始UI的第二个返回值,它代表了难度,我在这里简化(偷懒)了一点,返回值为10或者15,还记得前面说过的clock.tick么,它代表了游戏的帧数,也决定了蛇运动的速度,将diffculty直接作为参数,那么就会直接控制蛇的速度,从而实现难度的区分。
这样的区分好像确实有点太简单了,所以我才提出了后面的优化——添加障碍物,不同难度会对应不同大小、数量的障碍物,说实话,带障碍物的贪吃蛇我还真没怎么玩过,不过应该会很有意思。
当然,为了增加游戏性,你也可以发动脑筋,想出不同的策略为游戏添砖加瓦,比如食物有几率变成幸运方块,得分会增加或者可以让你的蛇暂时慢下来等等,听起来就很有意思的样子。

分数排行榜

要实现分数排行榜是有一定难度的,正如我前面提出指标时想的一样,需要用户在游玩之前输入用户名(字符串形式),在游玩时实时保存分数,并实时进行文件的读写,展示最高的一个或几个分数,当玩家的分数升高时,也可以看到自己的分数在排行榜上一步步前进(玩游戏最吸引人的就是成就感不是么),这也设计到了一个排序的问题,而且不要忘了,我们是有不同关卡选择的,不同关卡应该有着自己不同的排行榜。
当然,有了上面的需求,你可以轻易想到用字典来存储信息和更新信息,那我们该怎么读写文件呢,与字典联系最密切的当属json格式的文件,json文件正是由字典构成的,多层字典嵌套起来,也可以解决我们不同关卡不同排行榜的问题。

对于分数的读写展示可以分为以下几个步骤:

        # 记录当前分数
        save_score(level, username, curscores)
        # 展示当前分数
        show_scores(screen, curscores, cfg.pos2, cfg.text2, cfg.color2, font_size=30)
        # 读出当前游戏等级的排行榜数据
        Level_data = read_score(level)
        # 展示最高的三个分数
        show_highest_score(screen, Level_data, cfg.pos3, cfg.pos4, cfg.pos5, cfg.color2, cfg.font_size3)

当然这就需要对这四个步骤分别定义上述四个函数:
首先是保存分数函数,主要用到json的基本操作,json.load和json.dump.

def save_score(level, username, score):
    file = open('score_record.txt', 'r')
    data = json.load(file)
    file.close()
    file = open('score_record.txt', 'w')
    # 写入对应等级的该用户名的分数
    data['Level' + str(level)][username] = score
    json.dump(data, file)
    file.close()

然后是分数读取函数,因为我们很难对字典内部进行排序,所以选择用sorted函数,利用字典的items方法,将其作为元组的形式读出并进行排序,得到的当然也就是按照分数大小排序好的元组,具体方式可以百度,比如sorted函数中key的定义。

菜鸟教程
https://www.runoob.com/python3/python-sort-dictionaries-by-key-or-value.html

def read_score(level):
    # 生成代表等级的字符串
    Level = 'Level' + str(level)
    file = open('score_record.txt', 'r')
    data = json.load(file)

    # 得到排序后的该等级的分数--元组形式 (姓名,分数)
    Level_data = sorted(data[Level].items(), key=lambda x: x[1], reverse=True)
    file.close()
    # 读取前三个数据即前三高分
    if len(Level_data) < 3:
        return Level_data[0:]
    else:
        return Level_data[0:3]

展示最高分函数,这部分其实也是在前面的show_scores函数的基础上的改动,考虑到读取的元组数据中可能含有0-3组数据,因此要用if条件语句进行判断,从而当最高分不够三个时,用–和0代替输出。

def show_highest_score(screen, Level_data, pos1, pos2, pos3, color, font_size=20):
    # 三个位置显示三个最高分 其中排第一的字体大小为其他的二倍
    # 当最高分不够三个时,用--和0代替
    cur_font1 = pygame.font.SysFont("宋体", 2 * font_size)
    cur_font2 = pygame.font.SysFont("宋体", font_size)
    text_fmt1 = cur_font1.render('1  ' + '----    0', False, color)
    text_fmt2 = cur_font2.render('2  ' + '----    0', False, color)
    text_fmt3 = cur_font2.render('3  ' + '----    0', False, color)
    if len(Level_data) >= 1:
        text_fmt1 = cur_font1.render('1  ' + Level_data[0][0] + '    ' + str(Level_data[0][1]), False, color)
        if len(Level_data) >= 2:
            text_fmt2 = cur_font2.render('2  ' + Level_data[1][0] + '    ' + str(Level_data[1][1]), False, color)
            if len(Level_data) >= 3:
                text_fmt3 = cur_font2.render('3  ' + Level_data[2][0] + '    ' + str(Level_data[2][1]), False, color)

    screen.blit(text_fmt1, pos1)
    screen.blit(text_fmt2, pos2)
    screen.blit(text_fmt3, pos3)

搞定了这些内容之后,我们有自信说我们已经可以生成一个像样的排行榜了。那我们再回到开头,输入用户名,该怎么解决呢。
input函数当然是不可行的,这和pygame明显不搭,我们需要好好利用pygame中的模块。
下面这段参考了一个博客:

Pygame中的输入框
https://xbuba.com/questions/46390231

博客中关于输入文字的定义已经非常成熟了,我们基本只需要修改一些参数就可以直接拿来用了,当然,理解其中的每一步也至关重要,我们应该边做注释边一行行地看过去。

def inputUI(screen):
    font = pygame.font.Font(None, 32)
    clock = pygame.time.Clock()
    input_box = pygame.Rect(250, 400, 140, 32)
    color_inactive = (221, 174, 255)
    color_active = (251, 73, 255)
    color = color_inactive
    active = False
    text = ''
    done = False

    while not done:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                done = True
            if event.type == pygame.MOUSEBUTTONDOWN:
                # 如果鼠标点到输入框内
                if input_box.collidepoint(event.pos):
                    # 激活输入框
                    active = not active
                else:
                    active = False
                # 随着输入框的激活改变边框的颜色
                color = color_active if active else color_inactive
            if event.type == pygame.KEYDOWN:
                if active:
                    if event.key == pygame.K_RETURN:
                        return text
                    elif event.key == pygame.K_BACKSPACE:
                        text = text[:-1]
                    else:
                        text += event.unicode

        screen.fill((127, 255, 212))
        # 生成一个新的surface对象并在上面渲染指定的文本
        txt_surface = font.render(text, True, color)
        txt_please = font.render('Input Username:', True, color)
        # 如果字符串太长 则增加输入框的长度
        width = max(200, txt_surface.get_width()+10)
        input_box.w = width
        # 绘制文字
        screen.blit(txt_please, (input_box.x - 180, input_box.y + 5))
        screen.blit(txt_surface, (input_box.x + 5, input_box.y + 5))
        # 绘制输入框
        pygame.draw.rect(screen, color, input_box, 2)
        # 显示图像
        pygame.display.flip()
        clock.tick(30)

又看到了老熟人font.renderscreen.blitpygame.display.flip,这几个函数总是用在一起,它们之间应该怎么区分和理解呢?
我们可以把显示一个画面类比成打扮一个将要放上柜台展示的假人模型。font.render的输出为一个新的surface对象,可以理解成一件脱下来的衣服,我们可以把文字写在(渲染)在上面。而screen.blit则是相当于给你当前缓冲区的屏幕(当前正在编辑的画面)披上了这件衣服,他会覆盖上去,可能会露出手脚(未遮盖住的画面),当然你也可以不止穿一件衣服。而pygame.display.flip则就是将当前的模型摆上柜台,替换柜台上的模型,也可以用update函数进行同样的操作。

另外,我们希望每次打开游戏输入一次用户名而不是每次重新开始游戏都要输入,那样会很麻烦,也会容易出错,因此需要在主函数处定义一个全局变量,我把他定义为key
主函数变成这样:

if __name__ == '__main__':
    key = ''
    while True:
        if not main(snake_cfg):
            break
def main(cfg):
	.......
	.......
	# 开始UI 选择开始后循环可以正常进行
    global key
    isdead, level, username = StartUI(screen, cfg, key)
    key = username
    ......

在开始UI中也要进行判断:

def StartUI(screen, cfg, key):
    # 调用用户名输入UI
    if key == '':
        username = inputUI(screen)
    else:
        username = key

至此,我们才算完成了排行榜的功能。当然还要加很多细节,就需要去码代码的时候慢慢钻研了。

障碍和特殊方块

障碍这部分我本来是打算做的,也去尝试了一下,有这么两种想法或者思路:
一种是像snake一样定义一个封装好的类,或者说,某种程度上它是snake和food的结合。
另一种则是简单粗暴的将屏幕分为二维矩阵点,在上面随机取点。
两种方法听起来好像是一种方法不是么?其实都是要用随机生成的方式,同时也不能忽略掉,要排除当前食物的位置,然后同样是用蛇头的位置去判断。
至于为什么不做呢,纯粹是因为懒,我觉得做障碍就已经纯粹是重复的工作了,也需要对关卡的数量进行调整,想进一步完善的话当然可以考虑自己做下去。
特殊方块也是同理,这也只是我当初的一点脑洞,实现起来会比障碍复杂一些,这部分我就打算摒弃了,不过这样的思路也许以后会用得上。

其他改进

这个蛇头的颜色还是要变一下的,不然分不清蛇头真的蛋疼。

# 画出贪吃蛇的每个身体块
        pygame.draw.rect(screen, (4, 150, 254), snake.body[0], 0)
        for rect in snake.body[1:]:
            pygame.draw.rect(screen, (20, 220, 39), rect, 0)

还有对于输入的判断,多加些条件可以使得程序更稳定:

# 如果按键为方向键 则改变蛇的运动方向
                if event.key in [pygame.K_UP, pygame.K_DOWN, pygame.K_LEFT, pygame.K_RIGHT]:
                    snake.changedirection(event.key)

分数的判断也要改变一下,每次吃方块得1分,每次运动不得分(当然这个随便你怎么改了)。

curscores += 1

食物类的remove方法也可以改进一下

    def remove(self):
        self.rect.x = -25

貌似改成等于号会更稳定一点,之前-=好像有可能会出bug.

另外,在游玩时也发现,同时按两个方向键,会直接死亡,啊这,优化方法我还没找到(其实有个快捷死亡的bug也不错0.0)。

当前效果凑合看吧

搞了个比较长的gif图,可以演示出绝大部分功能了,嘿嘿。

总结什么的最难写了

对比了编写的最初一版的贪吃蛇与最终版的,你可以看到在核心部分,贪吃蛇它还是贪吃蛇,没什么实质性的变化,但是你也能看到,它增加了许多以前不曾有的“小细节”,玩过《巫师三》和《大表哥2》的应该都明白,CDPR和ROCKSTAR这两家游戏公司在细节上的把控也正是它们成功的精髓所在,所以我觉得做用心去做细节,做优化真的很重要很重要。(跑题
pygame是一个功能十分强大的python库,虽然它并不适合游戏开发,但那它来练手我觉得是再合适不过的,可以开放自己的想象力,也能锻炼不少编程能力。
总之,写博客也是为了记录学习进程,并且尝试和自己对话,我觉得这可能会是一个比较好的学习过程,希望能坚持下去。

你可能感兴趣的:(初入python,python,游戏)