Python与Pygame:球与圆内壁碰撞

7 投票
3 回答
11339 浏览
提问于 2025-04-16 09:29

我正在制作一个游戏,里面有小球在一个更大的圆圈的内部弹跳。这个大圆圈是静止不动的。

这是我目前用来处理这些碰撞的代码:

def collideCircle(circle, ball):
    """Check for collision between a ball and a circle"""
    dx = circle.x - ball.x
    dy = circle.y - ball.y

    distance = math.hypot(dx, dy)

    if distance >= circle.size + ball.size:
        # We don't need to change anything about the circle, just the ball
        tangent = math.atan2(dy, dx)
        ball.angle = 2 * tangent - ball.angle
        ball.speed *= elasticity + 0.251

        angle = 0.5 * math.pi + tangent
        ball.x -= math.sin(angle)
        ball.y += math.cos(angle)

这个代码是基于Peter Collingridge的一个很棒的教程,你可以在这里找到

圆圈和小球都是类,它们都有(x,y)坐标、半径、角度和速度。

不过,我在这个方法上遇到了两个问题:

  1. 小球从我猜测的“锚点”反弹,这个锚点似乎在圆圈的右上角。
  2. 当小球碰到圆圈的底部5%时,它的反弹高度不够,因此会“沉”出屏幕。我猜这可能是因为反弹的高度不足以让小球超过它(错误放置的)“锚点”?

我已经查看了一些可能的解决方案,比如“快速圆形碰撞检测”[链接因垃圾链接限制已删除],虽然这个是用Java写的,但方法是一样的,这些都处理的是外部碰撞,而我想要的是让小球在圆圈内部弹跳。

这里还有Ball()和Circle()的类定义:

class Ball():
    def __init__(self, (x,y), size):
        """Setting up the new instance"""
        self.x = x
        self.y = y
        self.size = size
        self.colour = (0,128,255)
        self.thickness = 0
        self.speed = 0.01
        self.angle = math.pi/2

    def display(self):
        """Draw the ball"""
        pygame.draw.circle(screen, self.colour, (int(self.x), int(self.y)), self.size, self.thickness)

    def move(self):
        """Move the ball according to angle and speed"""
        self.x += math.sin(self.angle) * self.speed
        self.y -= math.cos(self.angle) * self.speed
        (self.angle, self.speed) = addVectors((self.angle, self.speed), gravity)
        self.speed *= drag

class Circle():
    def __init__(self, (x,y), size):
        """Set up the new instance of the Circle class"""
        self.x = x
        self.y = y
        self.size = size
        self.colour = (236, 236, 236)
        self.thickness = 0
        self.angle = 0 # Needed for collision...
        self.speed = 0 # detection against balls

    def display(self):
        """Draw the circle"""
        pygame.draw.circle(screen, self.colour, (int(self.x), int(self.y)), self.size, self.thickness

谢谢你,Nathan

3 个回答

2

大多数图形软件都是从左上角开始绘制的。你通常需要两组坐标,一组是用来检测碰撞、移动等的,另一组是用来绘制的(比如x半径和y半径)。

另外,虽然我没想太多,但检查是否相交的条件应该是 distance + ball.size >= circle.size 吗?也就是说,球心到中心的距离加上它的半径应该小于圆的半径,如果我理解得没错的话。

14

在不直接回答你问题的情况下,我想谈谈你的实现策略,并推荐一种新的方法。你用极坐标的方式来表示小球的速度,分别是 ball.angleball.speed

我觉得这样做会让你在后续的工作中感到不方便。比如,在你的碰撞代码中,你需要调用 atan2 来把向量(dxdy)转换成一个角度,然后再用 sincos 把这个角度转换回向量。(而且,如果你将来想把代码扩展到三维,你会发现这会让你很头疼。)所以,除非你有特别的需求需要使用极坐标,我建议你像其他人一样,用笛卡尔坐标来表示小球的速度,使用向量(vxvy)。

我还建议你把物理处理方式从静态的(“物体A现在是否与物体B碰撞?”)改为动态的(“物体A在下一步移动中会与物体B碰撞吗?”)。在静态物理系统中,你常常会发现物体在移动结束时相互重叠,然后你需要想办法让它们分开,这个过程很难处理好。

如果你同时做这两点,反弹小球就会变得简单,不需要用到三角函数。

第一步:使用 闵可夫斯基加法 将圆与圆的碰撞转化为点与圆的碰撞:

左侧是小圆在大圆内移动的原始问题,右侧是点在半径为两个圆半径差的圆内移动的转化问题。

第二步:考虑一个时间段,在这个时间段内小球从p = (px,py) 开始,移动的速度是v = (vx,vy)。它是否与圆相交?你可以使用 标准的线段与圆的碰撞检测,不过这个检测的方向是反向的。

第三步:找到碰撞点c = (cx,cy)。小球会像碰到与圆在这个点切线t一样反弹。对于以原点为中心的圆,切线向量就是(−cy,cx),我相信你能算出其他圆的切线向量。

上面描述的图形

查看这个回答,了解如何根据摩擦系数和恢复系数计算小球的新路径。

第四步:别忘了小球可能还会沿着新的向量w继续移动。如果时间步长足够大或者速度足够快,它可能会在同一个时间段内再次碰撞。

12

我很高兴你喜欢我的教程。我也很喜欢你的改进版本,实际上应该更简单一些。

首先,我觉得你需要把碰撞检测的条件改成:

if distance >= circle.size - ball.size:

因为球的大小越大,它的中心和圆心之间的距离就可以越小。这应该能让球在正确的位置反弹(在圆内)。

然后,我觉得你只需要把x和y的符号互换一下,其他的应该就能正常工作了。

ball.x += math.sin(angle)
ball.y -= math.cos(angle)

要让球移动正确的距离,你可以计算重叠部分:

overlap = math.hypot(dx, dy) - (circle.size - ball.size)

if overlap >= 0:
  tangent = math.atan2(dy, dx)
  ball.angle = 2 * tangent - ball.angle
  ball.speed *= elasticity

  angle = 0.5 * math.pi + tangent
  ball.x += math.sin(angle)*overlap
  ball.y -= math.cos(angle)*overlap

祝你好运!

撰写回答