Pygame:碰撞检测


🚴大家好!我是近视的脚踏实地,虽然近视,但是脚踏实地,这一篇来学习游戏中的碰撞检测,然后继续完善未完成的小游戏

(一)碰撞检测的原理

大部分游戏都是要做碰撞检测的,因为你要知道小球是否发生了碰撞,子弹是否击中了目标等等。那么其实碰撞原理也很简单,事实就是检测两个精灵是否存在重叠的部分。
在这里插入图片描述

比如上面这张图👆,width表示的就是圆心的距离,r1、r2分别表示他们的半径,此时width是大于他们的半径之和的,那么他们就是没有产生重叠,也就是没有发生碰撞,那么当碰撞发生的那一刹那,如下图👇
在这里插入图片描述

可以看到此时width是等于r1+r2的,圆心距刚好等于两个半径之和.再来看看下面第三张图片
在这里插入图片描述

可以看到当他们重叠,产生交集的时候,width是小于r1+r2的,所以得到的结果就是判断两个小球是否发生碰撞,我们只要检测两个小球的圆心距是否小于等于他们两个半径之和就🉑了

(二)手动写代码实现碰撞检测

1️⃣版本一

collide_check.py:👇

import pygame
import sys
import math
from pygame.locals import *
from random import *

# 球类继承自Spirte类
class Ball(pygame.sprite.Sprite):
    def __init__(self, image, position, speed, bg_size):
        pygame.sprite.Sprite.__init__(self)

        self.image = pygame.image.load(image).convert_alpha()
        self.rect = self.image.get_rect()
        self.rect.left, self.rect.top = position
        self.speed = speed
        self.width, self.height = bg_size[0], bg_size[1]

    def move(self):
        self.rect = self.rect.move(self.speed)

        
        # 如果小球的左侧出了边界,那么将小球左侧的位置改为右侧的边界
        # 这样便实现了从左边进入,右边出来的效果
        if self.rect.right < 0:
            self.rect.left = self.width

        elif self.rect.left > self.width:
            self.rect.right = 0

        elif self.rect.bottom < 0:
            self.rect.top = self.height

        elif self.rect.top > self.height:
            self.rect.bottom = 0

def collide_check(item,target):
     col_balls = []
     for each in target:
         distance = math.sqrt(\
              math.pow((item.rect.center[0] - each.rect.center[0]), 2) + \
              math.pow((item.rect.center[1] - each.rect.center[1]), 2))
         if distance <= (item.rect.width + each.rect.width) / 2:
              col_balls.append(each)
     return col_balls
         
def main():
    pygame.init()

    ball_image = "gray_ball.png"
    bg_image = "background.png"

    running = True

    bg_size = width, height = 1024, 681
    screen = pygame.display.set_mode(bg_size)
    pygame.display.set_caption("Play the ball - Monster ZF")

    background = pygame.image.load(bg_image).convert_alpha()
    
    # 用来存放小球对象的列表
    balls = []

    # 创建五个小球
    BALL_NUM = 5
    for i in range(BALL_NUM):
        # 位置随机,速度随机
        position = randint(0, width-100), randint(0, height-100)
        speed = [randint(-10, 10), randint(-10, 10)]
        ball = Ball(ball_image, position, speed, bg_size)
        balls.append(ball)

    clock = pygame.time.Clock()

    while running:
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()

        screen.blit(background, (0, 0))

        for each in balls:
            each.move()
            screen.blit(each.image, each.rect)

        for i in range(BALL_NUM):
            item = balls.pop(i)

            if collide_check(item,balls):
                 item.speed[0] = -item.speed[0]
                 item.speed[1] = -item.speed[1]

            balls.insert(i, item)

        pygame.display.flip()
        clock.tick(30)
        

if __name__ == "__main__":
    main()

代码解析:这个代码是从上一篇博客中拷贝过来然后接着添加碰撞检测的代码。

那么首先在main函数上面先写个def collide_check(item, target): 用于碰撞检测的函数,这个函数应该有两个参数,第一个参数是传入一个项目,传入一个球,传入一个object,然后第二个参数是传入一组球,一个列表的球,因为我们有5个球,传入第一个球,然后碰撞,检测他跟其他四个球是否发生碰撞,如果是的话,你这个球就应该向别的地方去移动,移动方向应该发生改变,这里用target来存放其他的四个球。

接着用for each in target: 循环把每一个球取出来,然后我们就 distance = math.sqrt(…) 求他们的圆心距,我们的rect矩形对象,就是每一个Surface对象都会有一个rect矩形对象描述这个Surface对象的位置,那么这个rect矩形对象有个center的属性,他就是表示这个rect矩形对象的中心的那个点的坐标,然后这里计算距离需要用到Math函数,去前面导入Math模块,那么这个计算圆心距的原理就是小学数学的问题了,那么就是比如有两个点(x1,y1)、(x2,y2),那他们的距离就是x1-x2的平方 + y1-y2的平方 最后再开根号就🉑了

接着 if distance <= (item.rect.width + each.rect.width) / 2: 再来判断他们的圆心距和他们的半径之和,半径就是他们的宽度之和(直径+直径)除以2,如果比半径和小的话,就说明他们发生了一个重叠,那就把会发生重叠的元素item把它添加到一个列表里边去,然后把这个列表给返回,👉col_balls = []col_balls.append(each),最后return col_balls 把这个列表返回,如果列表里边有内容,那么这些内容就是跟这个item发生发生碰撞的所以的其他的小球,如果说返回的是一个空列表,那么说明这个球很幸运,他没有和任何球发生碰撞

然后我们应该在哪里要添加,那就是应该在移动小球之后,就要对他进行检测,如果说检测到他们移动之后是发生碰撞的,那么我们就把它的方向给取反,我们现在先再加个定义BALL_NUM = 5,这样显得更专业,以后要修小球个数就改这个变量就可以了,接着把for循环里的5换成BALL_NUM

接着写到for i in range(BALL_NUM):item = balls.pop(i),这个循环就是用来检测,检测的时候,因为刚刚写的那个检测碰撞的方法是这里的item是跟target其他的四个小球,而不是全部,因为他自己跟自己肯定会相撞,他是跟其他四个小球,所以说在这里检测的时候就需要先把自己给拿出来,就是用pop方法,

取完之后,if collide_check(item, balls): 就可以调用那个刚刚写好的,用于检测的方法,然后现在这个balls里边剩下的就是四个小球,如果说检测之后列表里有内容,那就 item.speed[0] = -item.speed[0]、item.speed[1] = -item.speed[1] 这个item自身的问题就要对他进行修改,把它两个方向上的速度取反就可以了

接着 balls.insert(i, item) 处理完要记得把小球放回去,放回列表中,从哪拿出来就放回哪去,从i拿就放回i处,比如说刚开始循环是第0号元素,那我拿出来之后原来的1号就变成0号了,那我又放到0号,原来的0号又变成1号了,大概就是这个意思,这个时候先来运行检测一下刚刚完成的代码:如下:👇
在这里插入图片描述
从运行结果来看,碰撞的效果成功了👏👏,老是抖抖的,But!那两个球在干嘛😂😂,缠缠绵绵的也不分开。

原因其实是这样:小球在诞生的时候,它诞生的位置,就是其他小球已经出来了,这个小球诞生的时候,所在的位置正好有其他的小球,那么它就诞生在别的小球的上方了,因此代码中就会检测到两个小球会发生碰撞了,碰到的话,他的speed速度就会取反,但是取反,如果不能一次性将他们移出这个覆盖的范围的话,比如说,假设他们覆盖的范围是5,5个像素的位置相交,然后他们取反之后,他们的速度都是-1,一个1一个-1,那么他们通过一次移动是不能离开这个相交的范围的,所以在他们下一次检测的时候,他又测试到碰到碰撞,所以他们的位置就再次取反,又要往相同的方向去运动,他们又要相交了,所以就一直在那抖动脱离不开,所以要怎么解决呢🤔

2️⃣版本二

那么其实解决的方法也很简单,就是创建小球的时候,先调用检查碰撞的方法就可以了,作如下修改:👇
在这里插入图片描述
代码解析:while collide_check(ball, balls): 这里检测ball跟balls,就是跟已经创建出来,已有的列表其他小球是否发生碰撞的检测,如果说while True,即有碰撞返回碰撞的小球的话,就说明这个ball应该重新分配。

接着ball.rect.left, ball.rect.top = randint(0, width-100), randint(0, height-100) 这里需要的是 不要直接写ball.position,因为 position只是作为参数传进去然后初始的,position并不是这个球类的一个属性,是属性才可以这么调用,但他只是作为参数传进去的,然后position这个参数是传给self.rect.left, self.rect.top 的,所以这里就是这样写ball.rect.left, ball.rect.top

import pygame
import sys
import math
from pygame.locals import *
from random import *

# 球类继承自Spirte类
class Ball(pygame.sprite.Sprite):
    def __init__(self, image, position, speed, bg_size):
        pygame.sprite.Sprite.__init__(self)

        self.image = pygame.image.load(image).convert_alpha()
        self.rect = self.image.get_rect()
        self.rect.left, self.rect.top = position
        self.speed = speed
        self.width, self.height = bg_size[0], bg_size[1]

    def move(self):
        self.rect = self.rect.move(self.speed)

        
        # 如果小球的左侧出了边界,那么将小球左侧的位置改为右侧的边界
        # 这样便实现了从左边进入,右边出来的效果
        if self.rect.right < 0:
            self.rect.left = self.width

        elif self.rect.left > self.width:
            self.rect.right = 0

        elif self.rect.bottom < 0:
            self.rect.top = self.height

        elif self.rect.top > self.height:
            self.rect.bottom = 0

def collide_check(item,target):
     col_balls = []
     for each in target:
         distance = math.sqrt(\
              math.pow((item.rect.center[0] - each.rect.center[0]), 2) + \
              math.pow((item.rect.center[1] - each.rect.center[1]), 2))
         if distance <= (item.rect.width + each.rect.width) / 2:
              col_balls.append(each)
     return col_balls
         
def main():
    pygame.init()

    ball_image = "gray_ball.png"
    bg_image = "background.png"

    running = True

    bg_size = width, height = 1024, 681
    screen = pygame.display.set_mode(bg_size)
    pygame.display.set_caption("Play the ball - Monster ZF")

    background = pygame.image.load(bg_image).convert_alpha()
    
    # 用来存放小球对象的列表
    balls = []

    # 创建五个小球
    BALL_NUM = 5
    for i in range(BALL_NUM):
        # 位置随机,速度随机
        position = randint(0, width-100), randint(0, height-100)
        speed = [randint(-10, 10), randint(-10, 10)]
        ball = Ball(ball_image, position, speed, bg_size)

        while collide_check(ball, balls):
            ball.rect.left, ball.rect.top = randint(0, width-100), randint(0, height-100)
        balls.append(ball)

    clock = pygame.time.Clock()

    while running:
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()

        screen.blit(background, (0, 0))

        for each in balls:
            each.move()
            screen.blit(each.image, each.rect)

        for i in range(BALL_NUM):
            item = balls.pop(i)

            if collide_check(item,balls):
                 item.speed[0] = -item.speed[0]
                 item.speed[1] = -item.speed[1]

            balls.insert(i, item)

        pygame.display.flip()
        clock.tick(30)
        

if __name__ == "__main__":
    main()

在这里插入图片描述

(三)使用sprite 提供的现成的碰撞检测函数

上面的碰撞函数是我们自己实现的,那么下面来继续学习下 sprite 提供的现成的碰撞检测函数,那么我们为什么还要学这个现成的呢,因为我们自己实现的collide_check() 函数只是适用于圆与圆之间的碰撞检测,如果说其他的多边型,或者说矩形,三角形,还有一些不规则的多边形等等,那么自己写的collide_check() 就有局限性了,达不到相应的效果了。

Pygame 的 sprite 模块事实上已经提供了一个成熟的碰撞检测的函数供我们使用,这也是我们要将我们的类继承 sprite 模块的 Sprite 基类的原因。

spritecollide(sprite, group, dokill, collided = None)

如上,sprite模块提供了 spritecollide() 方法用于 检测某个精灵是否与指定的组中其它精灵发生碰撞,事实上跟刚刚自己写的collide_check() 原理基本上是类似的。

他有四个参数,第一个参数是指定被检测的精灵,就是我们刚刚的item。

第二个参数是指定一个组,就是我们刚才里边的列表,这个组的话,他是 sprite 的一个组,所以我们要由sprite.Group() 来生成

第三个参数是设置是否从组中删除检测到碰撞的精灵,如果设置为True的话,如果发生碰撞,他会把组中跟他产生碰撞的精灵给删除掉

第四个参数是指定一个回调函数,他是用于定制特殊的检测方法,如果说第四个参数忽略的话,那么是默认检测精灵的之间的 rect (矩形)属性

那下面也是拿上一篇博客的代码来继续增添检测碰撞的代码

import pygame
import sys
from pygame.locals import *
from random import *

# 球类继承自Spirte类
class Ball(pygame.sprite.Sprite):
    def __init__(self, image, position, speed, bg_size):
        # 初始化动画精灵
        pygame.sprite.Sprite.__init__(self)

        self.image = pygame.image.load(image).convert_alpha()
        self.rect = self.image.get_rect()
        # 将小球放在指定位置
        self.rect.left, self.rect.top = position
        self.speed = speed
        self.width, self.height = bg_size[0], bg_size[1]
        self.radius = self.rect.width / 2

    def move(self):
        self.rect = self.rect.move(self.speed)

        # 如果小球的左侧出了边界,那么将小球左侧的位置改为右侧的边界
        # 这样便实现了从左边进入,右边出来的效果
        if self.rect.right < 0:
            self.rect.left = self.width

        elif self.rect.left > self.width:
            self.rect.right = 0

        elif self.rect.bottom < 0:
            self.rect.top = self.height

        elif self.rect.top > self.height:
            self.rect.bottom = 0
        
def main():
    pygame.init()

    ball_image = "gray_ball.png"
    bg_image = "background.png"

    running = True

    # 根据背景图片指定游戏界面尺寸
    bg_size = width, height = 1024, 681
    screen = pygame.display.set_mode(bg_size)
    pygame.display.set_caption("Play the ball - Monster ZF")

    background = pygame.image.load(bg_image).convert_alpha()

    # 用来存放小球对象的列表
    balls = []
    group = pygame.sprite.Group()

    # 创建五个小球
    for i in range(5):
        # 位置随机,速度随机
        position = randint(0, width-100), randint(0, height-100)
        speed = [randint(-10, 10), randint(-10, 10)]
        ball = Ball(ball_image, position, speed, bg_size)
        while pygame.sprite.spritecollide(ball, group, False, pygame.sprite.collide_circle):
            ball.rect.left, ball.rect.top = randint(0, width-100), randint(0, height-100)
        balls.append(ball)
        group.add(ball)

    clock = pygame.time.Clock()

    while running:
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()
            
        screen.blit(background, (0, 0))

        for each in balls:
            each.move()
            screen.blit(each.image, each.rect)

        for each in group:
            group.remove(each)

            if pygame.sprite.spritecollide(each, group, False, pygame.sprite.collide_circle):
                each.speed[0] = -each.speed[0]
                each.speed[1] = -each.speed[1]

            group.add(each)

        pygame.display.flip()
        clock.tick(30)


if __name__ == "__main__":
    main()

在这里插入图片描述
首先要先添加一个pygame自己的sprite的Group,然后他才能进行一个检测,因为刚刚讲了这个spritecollide函数他是需要他们自己的Group,所以要在这里创建一个他自己的Group

在这里插入图片描述
那么这次我们有经验了,在创建小球的时候先来个碰撞检测,不然又会出现两个球一直在那抖啊抖,spritecollide函数第一个参数就是传入需要检测的小球,第二个参数传入一个group,第三个参数传进False,,第四个参数先不管

接着如果检测到碰撞的话同时来修改他的位置ball.rect.left, ball.rect.top = randint(0, width-100), randint(0, height-100)

接着group.add(ball) add是填加一个元素到组里边去,而remove就是从里边删除,那这里就把ball装进去

在这里插入图片描述
接着让小球移动后也要来做个碰撞检测,还是同样的 group.remove(each) 先把这个小球从组里边拿出来,拿出来之后那对他进行一个检测,pygame.sprite.spritecollide(each, group, False, pygame.sprite.collide_circle): 我们不希望他检测之后从组里删除,所以设置为False,就是按照之前的套路来走,思路是一样

下面就是如果检测到了碰撞,就修改他的方向,然后检测完之后,又把它添加回去,跟之前的思路是一样的

接着就运行调试一下,发现同样可以实现碰撞检测的功能了,但是这还是有一些瑕疵,仔细看可以发现有时候,碰撞检测并不是完美的,有的情况相撞的时候,他们中间是还有空隙的,感觉他们还没有接触,他们就碰撞了,但有时候也不会,那这是什么原因呢🤔,

其实是这样的,刚刚提到spritecollide方法的时候,说了他的第四个参数collided = None 如果不指定的话,就表示检测的精灵是检测rect的属性是否重叠,那我们的小球事实上就是这样的👇:
在这里插入图片描述
这里是对PNG图片的透明部分做了一个加深,所以当两个小球是这样的形式,这样的角度碰撞的话,他事实上这个rect矩形已经撞上了。那么这个sprite模块里边刚好有个collide_circle方法👇:

collide_circle(left,right)

这个函数就是专门用于检测两个圆之间是否发生碰撞,这个函数有两个参数,就分别是两个精灵,他会自动传进去,所以直接把这个函数的名字作为spritecollide方法的第四个参数传进去就好了。

但是要注意**collide_circle(left,right)**这个函数的话他需要精灵对象有一个叫做 radius半径的属性,所以精灵的话再去添加一个属性就好,再去给之前的第四个参数补上
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

到这里他就会去检测两个圆形是否真正发生碰撞,而不是两个rect,那么现在就已经是完美的检测碰撞了,效果如下:👇
在这里插入图片描述

本篇博客到这就完啦,下一篇继续完善这个小游戏,非常感谢您的阅读🙏,如果对您有帮助,可以帮忙点个赞或者来波关注鼓励一下喔😬 ,嘿嘿👀

  • 19
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
pygame提供了两种方法来实现碰撞检测。第一种方法是使用`pygame.sprite.groupcollide()`函数,该函数可以检测两个精灵组中所有精灵的碰撞。具体的函数调用形式为`pygame.sprite.groupcollide(group1, group2, dokill1, dokill2, collided=None)`。 第二种方法是使用`pygame.sprite.spritecollide()`函数,该函数可以检测一个精灵与一个精灵组中的其他精灵是否发生碰撞。具体的函数调用形式为`pygame.sprite.spritecollide(sprite, group, dokill, collided=None)`。 需要注意的是,这两种方法都需要创建精灵组并将精灵添加到组中,才能进行碰撞检测。 在实现飞机大战游戏中的碰撞检测时,可以参考一些相关的操作技巧。例如,可以使用矩形边界框来表示精灵的形状,并使用`pygame.Rect`来管理矩形边界框。然后,使用碰撞检测方法来检测飞机与敌人飞机、子弹与敌人飞机之间的碰撞,从而触发相应的游戏逻辑。 总结起来,pygame提供了`pygame.sprite.groupcollide()`和`pygame.sprite.spritecollide()`两种方法来实现碰撞检测,可以根据具体的需求选择适合的方法来进行碰撞检测操作。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [(十)通过pygame来进行碰撞检测](https://blog.csdn.net/fjswcjswzy/article/details/106102953)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [python飞机大战pygame碰撞检测实现方法分析](https://download.csdn.net/download/weixin_38736018/12857108)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值