贪吃蛇“大作战”(终结篇)
经历几天的“挖坑”、“填坑”,贪吃蛇程序也要完结了,现在已经可以做到正常运行贪吃蛇这个游戏了。在编写贪吃蛇这个游戏的过程中,真的是酸甜苦辣都经历个遍,不过靠自己编写而不是完全“借鉴”别人的代码,这种收获感和成就感真的是满满的。好了,多的不说,先上代码。(在从命令行贪吃蛇到pygame贪吃蛇的过程中参考了别人的思路,这是链接:https://www.jianshu.com/p/f339356fd9ee)
之前的博客贴出了草地、食物、贪吃蛇三个对象的类模板,这边我再给出主程序的代码。(“填坑”过程中对三个类模板有做修改,后面会把最新的代码更新
def terminate(): if g.ccbox("GameOver!!!", choices=('Continue','Exit')): main() else: pygame.quit() exit() #控制游戏暂停/开始 运行的程序(按一下空格暂停,再按一下重新开始) def pause(): global PAUSE_STATE if PAUSE_STATE == 0: pass elif PAUSE_STATE == 2: PAUSE_STATE = 0 #设置常量 BACK_GROUND_FILL = (255,245,238) #窗体背景填充颜色设置 GRASS_COLOR = (0,250,0) #草地的颜色设置为亮绿色 FOOD_COLOR = (250,0,0) #食物的颜色设置为红色 SNAKE_COLOR = (100,100,0) #贪吃蛇的颜色设置为黄色 SNAKE_INNER_COLOR = (250,250,0) #贪吃蛇身体内部颜色设置为亮黄色 LIVING_SPACE = (60,60,520,360) #设置生存空间的位置和大小 SIZE = 20 #定义贪吃蛇和食物在窗体中的大小(体型) SNAKE_LENGTH = 5 #定义贪吃蛇在游戏初始的长度 SNAKE_SPEED = 10 #定义贪吃蛇的移动速度 PAUSE_STATE = 0 #贪吃蛇游戏暂停标志位 #初始化pygame组件 pygame.init() #创建一个窗口对象s screen = pygame.display.set_mode((640,480),0,32) #设置窗口标题 pygame.display.set_caption("贪吃蛇大作战!") def main(): global PAUSE_STATE DIRECTION = 'RIGHT' #实例化草地对象 grass = GrassMap(GRASS_COLOR,LIVING_SPACE) #实例化食物对象 food = Food(FOOD_COLOR,SIZE,LIVING_SPACE) #实例化贪吃蛇对象 snake = Snake(SNAKE_COLOR,SNAKE_LENGTH,SIZE,LIVING_SPACE) #定义clock对象用以控制FPS(Frame Per Seconds) clock = pygame.time.Clock() #初始化贪吃蛇身体的坐标集 snake.generate() #导出此时的贪吃蛇身体坐标集 snakebody = snake.snake_body #初始化食物坐标 food.generate(snakebody) #开始主循环 while True: #for _ in range(8): pause() #通过巧妙使用while循环来达到暂停/开始 游戏的功能 while True: for event in pygame.event.get(): #监听是否退出程序 if event.type == QUIT: pygame.quit() exit() elif event.type == KEYDOWN: #监听键盘 if event.key == K_LEFT and DIRECTION != 'RIGHT': DIRECTION = 'LEFT' elif event.key == K_RIGHT and DIRECTION != 'LEFT': DIRECTION = 'RIGHT' elif event.key == K_UP and DIRECTION != 'DOWN': DIRECTION = 'UP' elif event.key == K_DOWN and DIRECTION != 'UP': DIRECTION = 'DOWN' elif event.key == K_SPACE: PAUSE_STATE += 1 if PAUSE_STATE != 1: break foodbody = food._food_pos #获取当前食物的坐标,用以判断贪吃蛇移动后是否是吃食物 snake.move(DIRECTION,foodbody) #贪吃蛇开始移动 #print('before:',food._food_pos,snake.snake_body[snake.head]) snakebody = snake.snake_body #获取移动后贪吃蛇的坐标集,用以确保食物不会在贪吃蛇身体中生成 exist = snake.foodstate #获取食物状态标志位,判断食物是否被贪吃蛇吃了 food.if_exist(exist,snakebody) #根据食物状态判断是否随机生成新的食物 #print('foodbody:',food._food_pos) #print('snakebodyhead:',snake.snake_body[snake.head]) if snake.isdead(): #判断贪吃蛇是否死亡 terminate() else: pass screen.fill(BACK_GROUND_FILL) #窗体背景填充 grass.generate(screen) #在窗体中生成草地 snake.draw(screen,SNAKE_INNER_COLOR) #在窗体中画出贪吃蛇 food.draw(screen) #在窗体中画出食物 #print('update') pygame.display.update() #刷新 time_passed = clock.tick(SNAKE_SPEED) #控制帧率,等效蛇的移动速度 #运行主体程序 main()
上面的代码就是贪吃蛇主程序的代码,主体程序的结构介绍如下:
从上到下依次是:贪吃蛇死亡时运行的程序(函数)——控制贪吃蛇游戏 暂停/开始 的程序(函数)——程序中的常量——pygame游戏初始化的过程——main()主函数
main()主函数结构细分:实例化游戏对象(草地、食物、贪吃蛇),可看作主程序的初始化过程——while循环主体
while()循环主体结构细分(while循环主体是贪吃蛇游戏的核心):键盘监听代码——贪吃蛇与食物的坐标变化过程代码——在窗体中渲染草地、食物、贪吃蛇三个对象的代码
在完成贪吃蛇代码的编写后,我满怀期待的第一次运行这个程序,然而,这一切只是开始;(没有什么会是一次成功的,或许有,但对目前的我来说还有很长的路要走。)接下来,各种让我怀疑人生的“填坑”之旅就此展开。(唉,自己挖的坑,再深也得含泪填完)
哦对了,先上张完成程序的效果图(额,暂时没搞懂怎么上视频):
现就我填坑过程中印象比较深刻或经常碰到的“坑”和在此作个简要说明:
- 全局变量与局部变量
这个是我比较常遇到的坑之一,看标题像是基础知识方面的问题,其实,嗯,的确是基础知识,但我就是经常把这个问题忽略了。在python或其他语言中,函数中的变量为局部变量,函数外的变量相对这个函数而言是全局变量。在python中,局部变量在该函数调用完毕后就被回收,腾出内存空间,而全局变量在整个程序运行过程中都会存在;嗯,扯远了,我挖的“坑”其实就是在函数中调用了全局变量,但却没有在函数中定义这个全局变量(python中特别定义全局变量时使用global关键字),而导致python报错。举个例子:
在我把global屏蔽后使用暂停游戏功能,python会报错如下
- 跨模块调用模块
这个是我至今还不是很清楚的问题。我一开始是在主程序中直接采用"import"方法导入草地、食物、贪吃蛇三个类模板,然而程序出现报错,原因是找不到pygame模块和识别不了"randint"方法,然后我尝试在这些类中同样导入pyggame模块和randint方法,再运行主程序,发现主程序可以正常运行。后面我判断单独运行模块和调用模块后运行模块是有区别的,起码这个模块只能识别本模块中导入的模块,而不能识别本模块之外导入的模块(好吧,有点绕口,但意思是这个意思)。我目前是把三个类模板和主程序放在同一个py文件中,共同使用pygame等模块。
- 代码执行顺序
我记得我在之前的博客也提到过这个问题,代码执行的顺序特别重要,特别是在while循环中的代码,代码执行的先后顺序直接与运行结果相关,要把代码一条一条地在脑中过一遍,模拟执行结果,这样才能避免错误,有兴趣的可以自己试试换个顺序会发生什么。特别的,在pygame中pygame.display.update()的顺序要注意一下。我在设计游戏 暂停/播放功能时就利用了这一特点。
- 类中的变量、函数定义与调用
在类中定义及调用变量、函数时要特别小心,尤其是在区别类方法、对象方法、静态方法、类属性、对象属性,加上一个全局变量这几者之前的关系时。讲真,一开始有点后悔用对象的方法(采用类)来写贪吃蛇了,有点搬起石头砸自己的脚。(还好后面都解决了,起码用这种方法写看着更清楚,符合python易读的特性)这次的坑大都是由类定义、对象定义的问题引起的。特别是各种属性、方法互相调用,一不小心就会发生问题。比如我之前定义贪吃蛇的头部时是在generate()方法中定义了一个self.head属性,后面统一调用这个属性名;为了找到这个“坑”,我找的都快怀疑人生了,实在太隐蔽了(或许我对python了解还不够),通过各种方法测试,根据测试数据的逐步比对一步步排除,我缩小了范围,最后花了半天才找到这个“罪魁祸首”。这个bug是在贪吃蛇迟到第一个食物,身体长长后出现的。原因是之前定义self.head = len(self.snake_body) -1的self.snake_body还是初始定义的长度,当贪吃蛇长长之后原本的头部索引就变成头部之后一截身体的索引了,由此导致了这个大坑的出现。
其中还有很多“坑”,就不在这一一介绍了(说多了都是泪),此外,我会把贪吃蛇游戏“进阶”,做出一个功能更丰富的贪吃蛇游戏,提高它的可玩性。这会在下篇博客介绍。