Pygame中基于Mask碰撞的反射角度计算:子弹穿透圆形问题
打砖块游戏:圆角Paddle的小球完美反射问题解决与向量原理讲解
问题背景
开发打砖块游戏时,需要实现带圆角的Paddle与小球的完美角度反射。最初采用角度、sin/cos处理运动方向,改用Pygame的Vector2向量后编写了简化示例,但出现子弹直接穿透圆形的问题。
原示例代码
import sys import pygame pygame.init() w, h = 1200, 800 screen = pygame.display.set_mode((w, h)) clock = pygame.Clock() class Bullet(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.image = pygame.Surface((10, 10)) self.image.set_colorkey((0,0,0)) pygame.draw.circle(self.image, (255, 0, 0), (5, 5), 3) self.rect = self.image.get_rect(topleft = (x, y) ) self.mask = pygame.mask.from_surface(self.image) self.velocity = pygame.Vector2(0, -10) def update(self, circlex, circley, circle_mask): if self.rect.y < 0 or self.rect.y > 800: self.kill() if self.rect.x < 0 or self.rect.x > 1200: self.kill() self.rect.y += self.velocity.y offset = circlex - self.rect.x, circley - self.rect.y if p := self.mask.overlap(circle_mask, offset): v = pygame.Vector2(p) self.velocity.reflect_ip(v) def draw(self, screen): screen.blit(self.image, self.rect) circle = pygame.Surface((400, 400)) pygame.draw.circle(circle, (255, 255, 255), (200, 200), 150) circle_rect = circle.get_rect(topleft = (400, 100)) circle.set_colorkey((0, 0, 0)) circle_mask = pygame.mask.from_surface(circle) gun = pygame.Surface((100, 40)) gun_rect = gun.get_rect(topleft = (200, 750)) gun.fill("red") bullets = pygame.sprite.Group() while True: mx, my = pygame.mouse.get_pos() for e in pygame.event.get(): if e.type == pygame.QUIT: pygame.quit() sys.exit() if e.type == pygame.MOUSEBUTTONDOWN: b = Bullet(mx, 750) bullets.add(b) screen.fill((30, 30, 30)) gun_rect.centerx = mx screen.blit(gun, gun_rect) screen.blit(circle, circle_rect) bullets.update(circle_rect.x, circle_rect.y, circle_mask) bullets.draw(screen) pygame.display.flip() clock.tick(120)
问题分析与修复
原代码的核心问题
- 位置更新不完整:仅更新了子弹的y轴位置,遗漏了x轴,导致子弹只能垂直移动,碰撞逻辑完全失效
- 碰撞向量错误:直接使用mask返回的碰撞偏移点作为反射法线,这不是圆形表面的正确法线方向
- 碰撞后未修正位置:碰撞发生时子弹已经穿透圆形内部,反射后仍可能卡在内部导致连续错误反射
修复后的完整代码
import sys import pygame pygame.init() w, h = 1200, 800 screen = pygame.display.set_mode((w, h)) clock = pygame.Clock() class Bullet(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.image = pygame.Surface((10, 10)) self.image.set_colorkey((0,0,0)) pygame.draw.circle(self.image, (255, 0, 0), (5, 5), 3) self.rect = self.image.get_rect(center = (x, y)) # 改用center定位,更方便计算中心 self.mask = pygame.mask.from_surface(self.image) self.velocity = pygame.Vector2(0, -10) self.radius = 3 # 记录子弹半径,用于位置修正 def update(self, circle_rect, circle_mask): # 边界检测 if self.rect.top < 0 or self.rect.bottom > h: self.kill() if self.rect.left < 0 or self.rect.right > w: self.kill() # 完整更新位置(x和y轴) self.rect.x += self.velocity.x self.rect.y += self.velocity.y # 计算碰撞偏移量 offset = (circle_rect.x - self.rect.x, circle_rect.y - self.rect.y) collision_point = self.mask.overlap(circle_mask, offset) if collision_point: # 计算子弹中心到圆形中心的向量 bullet_center = pygame.Vector2(self.rect.center) circle_center = pygame.Vector2(circle_rect.center) # 碰撞点的法线向量:从碰撞点指向圆心(需要归一化) collision_pos = pygame.Vector2(collision_point) + pygame.Vector2(self.rect.topleft) normal = (circle_center - collision_pos).normalize() # 反射速度向量 self.velocity.reflect_ip(normal) # 修正子弹位置,避免穿透 # 将子弹推回到圆形外部,距离等于子弹半径 push_back = normal * self.radius self.rect.center = bullet_center + push_back circle = pygame.Surface((400, 400)) pygame.draw.circle(circle, (255, 255, 255), (200, 200), 150) circle_rect = circle.get_rect(topleft = (400, 100)) circle.set_colorkey((0, 0, 0)) circle_mask = pygame.mask.from_surface(circle) gun = pygame.Surface((100, 40)) gun_rect = gun.get_rect(topleft = (200, 750)) gun.fill("red") bullets = pygame.sprite.Group() while True: mx, my = pygame.mouse.get_pos() for e in pygame.event.get(): if e.type == pygame.QUIT: pygame.quit() sys.exit() if e.type == pygame.MOUSEBUTTONDOWN: b = Bullet(mx, 750) bullets.add(b) screen.fill((30, 30, 30)) gun_rect.centerx = mx screen.blit(gun, gun_rect) screen.blit(circle, circle_rect) bullets.update(circle_rect, circle_mask) bullets.draw(screen) pygame.display.flip() clock.tick(120)
向量在碰撞反射中的工作原理
- 核心逻辑:圆形表面的碰撞反射遵循"入射角等于反射角",向量运算可以直接实现这一规则,无需手动计算角度
- 法线向量:圆形任意一点的法线是从该点指向圆心的向量(因为圆形的切线垂直于半径),必须归一化(长度为1)后才能用于反射计算
- 反射运算:Pygame的
Vector2.reflect_ip()方法基于向量点积实现,公式为:
其中反射向量 = 入射向量 - 2 * (入射向量 · 法线向量) * 法线向量·代表点积,用于计算入射向量与法线向量的夹角余弦值 - 优势:相比角度计算,向量无需处理弧度转换、三角函数精度误差,更适合复杂形状(如圆角Paddle)的碰撞反射,逻辑直观且不易出错
内容的提问来源于stack exchange,提问作者Muhammad huzaifa
相关产品推荐
相关产品推荐

