Merlin's Blog
Just record something
Toggle navigation
Merlin's Blog
Home
Scratch基础教程
About Me
Archives
Tags
pgzero游戏:贪吃蛇
2023-03-05 18:33:04
74
0
0
merlin
主要知识点: - 处理键盘的按键事件 - 控制角色的方向 - 旋转角色的图像 - 字典数据类型的应用 - 使用延迟变量 ##**一、创建场景和角色** ###**1.创建游戏场景** 贪吃蛇的头、身体、实物,都是15×15的图块,所以地图的宽和高都应该是15的倍数,跟拼图游戏一样,可以设置图块的边长。 ``` import pgzrun import random SIZE = 15 WIDTH = SIZE * 30 #横的可以摆放30个图块 HEIGHT = SIZE * 30 #竖的可以摆放30个图块 总共900个图块的空间 ``` 接下来用draw()绘制白色背景(蛇头是黑色的)。 ``` def draw(): screen.fill((255,255,255)) ``` 别忘记添加运行命令,然后运行测试一下场景是否建好。 ``` pgzrun.go() ``` ###**2.创建贪吃蛇** 蛇头的图片放在images目录下,叫snake_head.png,我们可以用Actor()方法来创建蛇头这个角色,代码放在初始化部分。 ``` snake_head = Actor("snake_head",(30,30)) ``` 跟之前创建方式不同,加上了第二个参数(坐标),也就是让蛇头一开始就在坐标30,30位置出现。 在draw()方法中添加绘制命令,然后运行一下测试程序,修改draw()代码后如下。 ``` def draw(): screen.fill((255,255,255)) snake_head.draw() ``` ###**3.处理键盘按键事件** 在这个游戏中,我们通过上下左右方向键来控制蛇头。Pgzero提供了一个专门的按键函数(事件),类似鼠标点击事件,叫on_key_down()。 当这个函数触发的时候,参数会获得当前按键的值:即按了哪个键,用法如下: ``` def on_key_down(key): if key == keys.LEFT: snake_head.x -= SIZE if key == keys.RIGHT: snake_head.x += SIZE if key == keys.UP: snake_head.y -= SIZE if key == keys.DOWN: snake_head.y += SIZE ``` 运行测试一下程序,看是否能用上下左右方向键控制蛇头移动。 ###**4.让蛇头持续移动** 玩过贪吃蛇的知道,蛇是一直在移动的。在pgzero中,update()按间隔一直在更新的,我们可以在它里面写让蛇持续移动的程序。 思路如下: 必须知道当前方向:上、下、左、右。这样就能对应的坐标进行修改,即如果是左,那么角色x坐标减少;如果是右,那么角色x坐标增加。上下同理。 所以需要一个全局变量,用来记录当前的方向。 ``` def update(): if 方向 == "右": snake_head.x += SIZE elif 方向 == "左": snake_head.x -= SIZE elif 方向 == "上": snake_head.y -= SIZE else: snake_head.y += SIZE #剩下的是朝下了 ``` 为了方便理解,我这里用了中文作变量名。 接下来在键盘按键事件中,增加对方向变量的赋值,修改代码如下: ``` def on_key_down(key): global 方向 if key == keys.LEFT and 方向!="右": snake_head.x -= SIZE 方向 = "左" if key == keys.RIGHT and 方向!="左": snake_head.x += SIZE 方向 = "右" if key == keys.UP and 方向!="下": snake_head.y -= SIZE 方向 = "上" if key == keys.DOWN and 方向!="上": snake_head.y += SIZE 方向 = "下" ``` 注意:改变的方向不能是原来方向的反方向,即往左的话,你蛇头当前方向不应该是右,这样马上就跟身体相撞了。 运行程序后,发现蛇头嗖的一下就没有了,因为update()执行的太快了。 ###**5.延迟变量减缓贪吃蛇的移动** 为了延迟移动,我们可以在update()入口处,弄个计数器,次数没到要求就不移动。 先在前面建一个变量,例如counter,update()代码修改如下: ``` def update(): global counter counter += 1 if counter < 10: return else: counter = 0 if 方向 == "右": snake_head.x += SIZE elif 方向 == "左": snake_head.x -= SIZE elif 方向 == "上": snake_head.y -= SIZE else: snake_head.y += SIZE ``` 测试一下,是不是慢了不少,记得反方向是失灵的哦。 ###**5.旋转蛇头的图像** 角色属性中,有一个角度属性可以控制图块的方向。 - 0° 右 - 90° 上 - 180° 左 - -90° 下 修改按键事件代码,因为按下的时候改变方向。 ``` def on_key_down(key): global 方向 if key == keys.LEFT and 方向!="右": snake_head.x -= SIZE 方向 = "左" snake_head.angle = 180 if key == keys.RIGHT and 方向!="左": snake_head.x += SIZE 方向 = "右" snake_head.angle = 0 if key == keys.UP and 方向!="下": snake_head.y -= SIZE 方向 = "上" snake_head.angle = -90 if key == keys.DOWN and 方向!="上": snake_head.y += SIZE 方向 = "下" snake_head.angle = 90 ``` ##**二、添加食物** ###**1.让食物随机出现** images下已放入食物图片,所以只要Actor()方法构建即可。 ``` food = Actor("snake_food") ``` 接着在draw()函数中,添加绘制食物的命令。 ``` food.draw() ``` 现在食物固定在左上角出现,我们希望食物是随机出现的,所以随机生成整数又可以上场了。 目前整个屏幕我们设计的是30×30的大小(共900个15×15个图块大小),如果食物出现的位置在边缘,那么蛇头很容易撞墙,导致游戏失败。体验感也是我们设计者应该考虑的问题,所以要适当的调整随机的范围,不要出现的边缘。 把30×30看成二维数组大小,那么行的取值在0~29,列也是0~29,那么去掉两行或两列后,范围应该设计如下,放在食物角色创建之后: ```python food.x = random.randint(2,WIDTH//SIZE-3)*SIZE food.y = random.randint(2,HEIGHT//SIZE-3)*SIZE ``` 生成的数再乘SIZE,是为了计算实际屏幕中的像素坐标。 现在运行几次试试,看食物每次出现的位置一样吗? ###**2.让贪吃蛇“吃”食物** 如何判断贪吃蛇是否吃到食物呢?从我们的角度上看,当蛇头和食物重叠在一起的时候,就可以认为贪吃蛇吃到了食物。从程序角度理解,应该是蛇头的坐标和食物的坐标发生了重叠。不妨设计成吃食物的函数: ```python def eat_food(): if food.x == snake_head.x and food.y == snake_head.y: sounds.eat.play() food.x = random.randint(2, WIDTH//SIZE - 3) * SIZE food.y = random.randint(2,HEIGHT//SIZE - 3) * SIZE ``` 代码解释: 如果食物和蛇头重叠,则播放吃东西的声音(eat.wav已放在sounds目录下),然后再出现在随机位置。 ###**3.增长贪吃蛇的身体** 按游戏规则,当贪吃蛇吃完一个食物后,身体就会加长。 现在遇到下面的问题: (1).如何让身体跟着蛇头走? (2).如何让身体加长? 问题1:其实身体的每一部分也是一个"角色",把这些"角色"放到列表中,当蛇头移动的时候,移动到的新位置创造一个新的"角色",然后把尾部的"角色"删除。也就是说,实际上身体中的图块并没有移动,只是通过"增加"和"删除"的方式,让我们以为身体在"移动"。这种方式跟生活中的排队很像,在编程中我们叫队列。即一端出口,另一端入口。 问题2: 理解了身体怎么跟着蛇头走的,问题2也好解决了,只要控制好列表(列表保存的是组成身体的图块角色)长度即可。 所以,首先要在初始化部分,增加一个全局变量length。 ``` length = 1 ``` 身体部分的角色,用body列表管理: ``` body = [] ``` 因为一开始只有蛇头,所以长度为1。在吃食物部分的代码判断,修改length的值。 ``` length += 1 ``` 在update()中,实现身体列表的处理:下标0理解成队列的队尾,新增用队列的append()方法。 ``` if len(body)==length: body.remove(body[0]) body.append(Actor("snake_body",(snake_head.x,snake_head.y))) ``` 接着在画蛇头之前,先把身体画出来,不然蛇头会被身体覆盖掉。 目前位置,修改后的代码如下: ```python import pgzrun import random SIZE = 15 WIDTH = SIZE * 30 #横的可以摆放30个图块 HEIGHT = SIZE * 30 #竖的可以摆放30个图块 总共900个图块的空间 方向 = "右" counter = 0 length = 1 body = [] snake_head = Actor("snake_head",(30,30)) food = Actor("snake_food") food.x = random.randint(2, WIDTH//SIZE - 3) * SIZE food.y = random.randint(2,HEIGHT//SIZE - 3) * SIZE def draw(): screen.fill((255,255,255)) food.draw() for b in body: b.draw() snake_head.draw() def on_key_down(key): global 方向 if key == keys.LEFT and 方向!="右": snake_head.x -= SIZE 方向 = "左" snake_head.angle = 180 if key == keys.RIGHT and 方向!="左": snake_head.x += SIZE 方向 = "右" snake_head.angle = 0 if key == keys.UP and 方向!="下": snake_head.y -= SIZE 方向 = "上" snake_head.angle = -90 if key == keys.DOWN and 方向!="上": snake_head.y += SIZE 方向 = "下" snake_head.angle = 90 def update(): eat_food() global counter counter += 1 if counter < 10: return else: counter = 0 if 方向 == "右": snake_head.x += SIZE elif 方向 == "左": snake_head.x -= SIZE elif 方向 == "上": snake_head.y -= SIZE else: snake_head.y += SIZE if len(body)==length: body.remove(body[0]) body.append(Actor("snake_body",(snake_head.x,snake_head.y))) def eat_food(): global length if food.x == snake_head.x and food.y == snake_head.y: sounds.eat.play() food.x = random.randint(2, WIDTH//SIZE - 3) * SIZE food.y = random.randint(2,HEIGHT//SIZE - 3) * SIZE length += 1 pgzrun.go() ``` 测试运行发现一个BUG,蛇头跑太快,身体没接上。 这是因为,按键事件是立刻触发的,而且按键事件中也加了移动,这样无形之中就加速了。所以需要删除按键事件中的移动。 修改如下: ``` def on_key_down(key): global 方向 if key == keys.LEFT and 方向!="右": 方向 = "左" snake_head.angle = 180 if key == keys.RIGHT and 方向!="左": 方向 = "右" snake_head.angle = 0 if key == keys.UP and 方向!="下": 方向 = "上" snake_head.angle = -90 if key == keys.DOWN and 方向!="上": 方向 = "下" snake_head.angle = 90 ``` 再次测试一下,看看头和身体是否还会断开。 ###**4.判断贪吃蛇是否碰到窗口边界** 蛇头如果碰到边界,则游戏结束。我们建一个全局变量finished,用来判断游戏是否结束,在游戏初始化位置,加入下面代码: ``` finished = False ``` 因为角色左上坐标为(left,top),右下坐标为(right,bottom),所以越界有4种情况: - 角色left属性小于0 - 角色right属性大于WIDTH - 角色top属性小于0 - 角色bottom属性大于HEIGHT 创建一个检查游戏是否结束的函数,check_gameover(). ``` def check_gameover(): global finished if snake_head.left < 0 or snake_head.right>WIDTH or snake_head.top < 0 or snake_head.bottom>HEIGHT: sounds.fail.play() finished = True ``` 然后在update()方法中,加入函数check_gameover()。 ###**5.判断贪吃蛇蛇头是否碰到身体** 这个逻辑很简答, 把身体列表的每一个角色的坐标跟蛇头的坐标进行对比,如果重叠,说明撞到了身体。但是,最后一个身体图块,原本就跟蛇头重叠的,因为每次都是在蛇头位置创造角色,然后移动蛇头的。所以,遍历身体列表的时候,要忽略最后一个图块(即跟蛇头重叠的图块) check_gameover()函数修改如下: ``` def check_gameover(): global finished if snake_head.left < 0 or snake_head.right>WIDTH or snake_head.top < 0 or snake_head.bottom>HEIGHT: sounds.fail.play() finished = True for i in range(len(body)-1): if body[i].x == snake_head.x and body[i].y == snake_head.y: sounds.fail.play() finished = True return ``` ###**6.输出游戏结束信息** 在draw()函数中,加上游戏结束判断,然后输出文本。 ```python if finished==True: screen.draw.text("Game Over!",center=(WIDTH//2,HEIGHT//2),fontsize=50,color="red") ```
Pre:
pgzero游戏:打字游戏
Next:
pgzero游戏:拼图
0
likes
74
Weibo
Wechat
Tencent Weibo
QQ Zone
RenRen
Table of content