一.目的
在这一节,我们完成了地图上几乎所有东西的绘制,并且达到了伪3D的效果。不过我们并没有给这些地图上的东西设置碰撞箱,这导致玩家可以随意从他们上面穿过。这次,我们的目的是给他们设置碰撞箱,让玩家无法从一些障碍物上走过。
二.代码的编写
首先,我们不希望精灵的碰撞箱与照片的体积这么大,如下图所示,红色框选的部分为照片的大小,然而实际上主角的大小是黄色框选的部分,所以我们在给每一种精灵设置碰撞箱的时候,都要在它的图片的大小的基础上减去一些大小,这需要根据实际的情况具体分析。
所谓的碰撞箱,其实就是pygame提供Rect类的对象,就是一个矩形。作者给Rect类写了一个叫colliderect(Rect)的函数,这个函数的返回值是一个bool值,如果调用这个函数的Rect对象与参数中的Rect对象发生了重叠,如下图所示,它的返回值就是True,反之就是False
在这里,原作者并没有用精灵或者精灵组的碰撞检测方式,而是用的Rect类的重叠检测方式来判断是否发生了碰撞。所以精灵或者精灵组的碰撞检测方式就不再这里赘述了。
玩家的hitbox由他的rect复制而来,并且在此基础上宽度减小了126,高度减小了70,这个数据是经过原作者测试得来的,这样做视觉效果是最好的
这里用到了一个新的API,rect.inflate((width,height)),作用是对一个rect的宽度和高度分别拓宽width和height,这里我们用的是负数,所以达到的是缩小的效果
接着是对Generic类设置碰撞箱,当然,设置hitbox这个属性不一定会与玩家发生碰撞
我们知道,游戏的地图,房间的地板都是属于Generic类的,但是它们永远不会与玩家发生碰撞
我们在这里设置hitbox属性是因为树木,花朵等等继承自Generic的类,它们会与玩家发生碰撞,我们在父类中写了就不用一个个再子类中再写了,具体谁会与玩家发生碰撞我们在以后的代码中会再细分
这里碰撞箱的宽度比原图片的宽度小了0.25,高度比原图片的高度小了0.75。至于为什么高度会小这么多,是因为我们要达到伪3D的效果,如果碰撞箱不设置的矮一点,我们就会像下图所示的一样,被卡在树顶上下不去
接下来给野花设置hitbox,虽然野花已经继承了父类的hitbox,但是父类的hitbox大小并不太适合他,因为野花是很细的,但是在二维平面上又是很高的,所以我们需要给他的hitbox缩减为原图的0.1
接下来,我们的思路就是,所有会与玩家发生碰撞的精灵,都加入一个叫collision_sprites的精灵组,然后在player这个类里,每次进行移动的时候,都会遍历collision_sprites精灵组中的所有精灵,如果player在移动的过程中与这个精灵组的任何一个精灵碰撞箱发生了重叠,就说明它们发生了碰撞,这时候我们再对发生碰撞的这种情况进行处理
这里原作者并没有把房子的墙壁和家具加入到这个精灵组里面,反而在后期给房子的墙壁和家具增加了空气墙,这点设计就很奇怪
我也明白为什么上一节原作者单独把栅栏从for循环里拉出来创建了,因为他想把栅栏增加到碰撞箱精灵组里,而不把墙壁和家具增加到碰撞箱精灵组里,所以没办法写到一个for循环里。
不过既然我们已经给他写到同一个for循环里了,那就顺便把墙壁和家具也加入到碰撞箱精灵组里面吧,反正最后的效果是一样的。
在这里水流并不用加入碰撞箱精灵组是因为后期我们会用空气墙把湖给围起来,玩家根本不会走到水里去
接着就在player类中书写对碰撞进行处理的代码,我们首先需要知道那些精灵是在碰撞箱精灵组里的,所以我们需要把collision_sprites当作参数传递进去
我们发生碰撞的处理思路是,当玩家的hitbox与其他精灵的hitbox发生重叠的时候,我们要分别对水平方向和垂直方向进行处理。
实际上,我们同时按住上键和右键,视觉效果是斜着向右上方走的,实际上是每一帧先向右走一点,再向上走一点,这是由我们代码的书写顺序决定的
由于每一秒有很多帧,变化的很快,所以视觉上,我们是直着向右上方走的
当我们在水平方向移动的时候,检测水平方向是否发生了碰撞,如果发生了碰撞,就回到发生碰撞之前的位置
接下来,我们在竖直方向移动的时候,检测竖直方向书否发生了碰撞,如果发生了碰撞,就回到碰撞发生之前的位置
至于为什么要对水平和竖直的情况分开讨论,在下面会解释,接下来我们先完成代码
我们想把对碰撞处理的代码封装到一个叫collision的函数里面,这里我们先调用它,每当水平移动一次之后,调用一次函数,竖直移动一次之后,调用一次函数,函数的参数是一个str类型的数据,代表当前进行的是水平还是竖直方向的移动
def collision(self,direction):
# 遍历碰撞箱精灵组的所有精灵
for sprite in self.collision_sprites.sprites():
# 如果该精灵有一个叫hitbox的属性
#实际上collision_sprites精灵组内的所有精灵都应该有hitbox这个属性,这句话属实多余
if hasattr(sprite,'hitbox'):
#如果该精灵的hitbox与玩家的hitbox有重叠
if sprite.hitbox.colliderect(self.hitbox):
#如果此时正在水平方向移动
if direction == 'horizontal':
if self.direction.x > 0: #玩家正在向右移动
self.hitbox.right = sprite.hitbox.left
if self.direction.x < 0: # 玩家正在向左移动
self.hitbox.left = sprite.hitbox.right
self.rect.centerx = self.hitbox.centerx
self.pos.x = self.hitbox.centerx
#如果此时正在竖直方向移动
if direction == 'vertical':
if self.direction.y > 0: # 玩家正在向下移动
self.hitbox.bottom = sprite.hitbox.top
if self.direction.y < 0: # 玩家正在向上移动
self.hitbox.top = sprite.hitbox.bottom
self.rect.centery = self.hitbox.centery
self.pos.y = self.hitbox.centery
当然,我们还忘了,当我们移动的时候hitbox也要跟着我们玩家移动,不然玩家走了,hitbox还留在原地,就永远也发生不了碰撞
这里对hitbox的位置进行了四舍五入的取整。经过测试,不对他进行取整也不会发生任何错误。不过还是遵从原作者的意愿。
至于上面说的为什么要把水平和数值方向分开来进行碰撞箱检测,是因为我们担心出现下下面这种情况
这种情况下,玩家即在水平方向与墙壁发生了重叠,又在竖直方向与墙壁发生重叠,我们不知道究竟要把玩家移动到下图的哪个位置
黄色的玩家hitbox与红色的墙壁hitbox进行了重叠,如果我们把水平和垂直方向的移动分开讨论,那么他就会有如图黑色箭头所示的速度方向
然后会执行黄色箭头所指的代码,会造成玩家瞬移到蓝色方框的位置。造成视觉上的瞬移。
至此,完成了玩家与地图上事物的碰撞效果,此时运行程序,发现玩家的出生点在栅栏之外,所以我们还得修改玩家的出生点
打开.tmx文件,发现有一个叫player的图层,它有如图红色箭头所指的三块区域
接下来,我们给地图边界设置空气墙,空气墙的图层名为Collision,它是下图中橙黄色的区域
可以把由于玩家并不与空气墙产生交互,所以不用给他单独写一个类出来,直接用Generic创建对象即可。
由于创建对象需要一个图片当作参数,我们随便绘制一张64*64的黑色方块图片即可,由于空气墙不需要绘制出来,所以只把他加入到collision_sprites精灵组内即可