Pymunk中碰撞触发圆半径增加功能异常排查求助
Pymunk圆形碰撞线段时半径未每次触发增加的问题排查与修复
我用Pymunk搭建了一个简单场景,让圆形和边界线段碰撞,需求是每次碰撞时圆形半径按5%比例增加,且半径增加后能正常反弹。但当前代码存在异常:半径有时会在碰撞时增加,有时不会,无法定位原因。
原代码如下:
import pygame as pg import pymunk import pymunk.pygame_util import sys import math pymunk.pygame_util.positive_y_is_up = False RES = WIDTH, HEIGHT = 1080 // 2, 1920 // 2 FPS = 120 pg.init() surface = pg.display.set_mode(RES) clock = pg.time.Clock() draw_options = pymunk.pygame_util.DrawOptions(surface) space = pymunk.Space() space.gravity = 0, 530 elasticity = 0.999 friction = 0.05 # create the bounding box def create_segments(start, end): segment_shape = pymunk.Segment(space.static_body, start, end, 3) segment_shape.elasticity = elasticity segment_shape.friction = friction segment_shape.collision_type = 1 # Assign a collision type to the walls space.add(segment_shape) create_segments((0, HEIGHT), (WIDTH, HEIGHT)) # floor create_segments((0, 0), (0, HEIGHT)) # left wall create_segments((WIDTH, 0), (WIDTH, HEIGHT)) # right wall create_segments((0, 0), (WIDTH, 0)) # ceiling # create the ball ball_mass = 1 ball_radius = 20 # Separate variable to store the radius ball_moment = pymunk.moment_for_circle(ball_mass, 0, ball_radius) ball_body = pymunk.Body(ball_mass, ball_moment) ball_body.position = WIDTH // 2, 50 ball_body.velocity = (400, 200) ball_shape = pymunk.Circle(ball_body, ball_radius) ball_shape.elasticity = elasticity ball_shape.friction = friction ball_shape.collision_type = 2 # Assign a different collision type to the ball space.add(ball_body, ball_shape) #Collision handler collided = False # Flag to track if collision happened in the current frame def collision_handler(arbiter, space, data): global ball_radius, ball_shape, collided if collided: return False # Calculate new radius new_radius = ball_radius * 1.05 # 5% increase # Adjust the ball's position based on the collision direction normal = arbiter.contact_point_set.normal if normal.y > 0: # Collision with floor ball_body.position += (0, new_radius - ball_radius) elif normal.y < 0: # Collision with ceiling ball_body.position -= (0, new_radius - ball_radius) elif normal.x > 0: # Collision with right wall ball_body.position += (new_radius - ball_radius, 0) elif normal.x < 0: # Collision with left wall ball_body.position -= (new_radius - ball_radius, 0) # Now check against the limits after adjusting the position if new_radius > ball_body.position[0] or new_radius > ball_body.position[1] or \ new_radius > WIDTH - ball_body.position[0] or new_radius > HEIGHT - ball_body.position[1]: return True # Reject radius increase if it exceeds space limits # Increase the radius ball_radius = new_radius # Remove the old ball shape space.remove(ball_shape) # Create the new ball shape ball_shape = pymunk.Circle(ball_body, ball_radius) ball_shape.elasticity = elasticity ball_shape.friction = friction ball_shape.collision_type = 2 # Assign the same collision type as before space.add(ball_shape) collided = True return True # Add collision handler for the ball hitting the walls handler = space.add_collision_handler(1, 2) # Only handle collisions between types 1 and 2 handler.pre_solve = collision_handler # Use pre_solve instead of post_solve # Main loop while True: surface.fill(pg.Color('white')) for i in pg.event.get(): if i.type == pg.QUIT: pg.quit() sys.exit() collided = False space.step(1 / FPS) space.debug_draw(draw_options) pg.display.flip() clock.tick(FPS)
问题原因分析
pre_solve回调的执行特性:pre_solve会在碰撞检测的多个阶段被调用(比如碰撞持续接触的帧内会多次触发),全局collided标记的重置逻辑无法精准区分「新碰撞」和「同一碰撞的持续检测」,导致部分碰撞触发被跳过。- 形状修改时机错误:在
pre_solve中直接移除并添加形状,会干扰当前帧的碰撞处理流程,导致后续碰撞检测失效。 - 转动惯量未更新:半径增大后,球体的转动惯量(moment)未重新计算,会导致物理运动异常,间接影响碰撞触发逻辑。
修复方案与修改后代码
核心修改点:
- 改用
post_solve回调,确保碰撞处理完成后再修改物体属性 - 用
arbiter.is_first_contact()替代全局标记,精准判断新碰撞 - 更新转动惯量,保证物理运动正确性
- 优化边界检查逻辑
修改后完整代码:
import pygame as pg import pymunk import pymunk.pygame_util import sys pymunk.pygame_util.positive_y_is_up = False RES = WIDTH, HEIGHT = 1080 // 2, 1920 // 2 FPS = 120 pg.init() surface = pg.display.set_mode(RES) clock = pg.time.Clock() draw_options = pymunk.pygame_util.DrawOptions(surface) space = pymunk.Space() space.gravity = 0, 530 elasticity = 0.999 friction = 0.05 # 创建边界线段 def create_segments(start, end): segment_shape = pymunk.Segment(space.static_body, start, end, 3) segment_shape.elasticity = elasticity segment_shape.friction = friction segment_shape.collision_type = 1 space.add(segment_shape) create_segments((0, HEIGHT), (WIDTH, HEIGHT)) # 地面 create_segments((0, 0), (0, HEIGHT)) # 左墙 create_segments((WIDTH, 0), (WIDTH, HEIGHT)) # 右墙 create_segments((0, 0), (WIDTH, 0)) # 天花板 # 创建球体 ball_mass = 1 ball_radius = 20 ball_moment = pymunk.moment_for_circle(ball_mass, 0, ball_radius) ball_body = pymunk.Body(ball_mass, ball_moment) ball_body.position = WIDTH // 2, 50 ball_body.velocity = (400, 200) ball_shape = pymunk.Circle(ball_body, ball_radius) ball_shape.elasticity = elasticity ball_shape.friction = friction ball_shape.collision_type = 2 space.add(ball_body, ball_shape) # 碰撞处理函数 def collision_handler(arbiter, space, data): global ball_radius, ball_shape # 仅处理首次碰撞接触 if not arbiter.is_first_contact(): return True new_radius = ball_radius * 1.05 # 检查边界限制:确保增大后不会超出边界 min_pos = new_radius max_x = WIDTH - new_radius max_y = HEIGHT - new_radius if (ball_body.position.x < min_pos or ball_body.position.x > max_x or ball_body.position.y < min_pos or ball_body.position.y > max_y): return True # 根据碰撞方向调整位置,避免球体嵌入边界 normal = arbiter.contact_point_set.normal offset = new_radius - ball_radius if normal.y > 0: # 撞地面 ball_body.position += (0, offset) elif normal.y < 0: # 撞天花板 ball_body.position -= (0, offset) elif normal.x > 0: # 撞右墙 ball_body.position += (offset, 0) elif normal.x < 0: # 撞左墙 ball_body.position -= (offset, 0) # 更新半径 ball_radius = new_radius # 替换球体形状 space.remove(ball_shape) ball_shape = pymunk.Circle(ball_body, ball_radius) ball_shape.elasticity = elasticity ball_shape.friction = friction ball_shape.collision_type = 2 space.add(ball_shape) # 重新计算转动惯量,保证物理运动正确 new_moment = pymunk.moment_for_circle(ball_mass, 0, ball_radius) ball_body.moment = new_moment return True # 绑定碰撞处理器,使用post_solve handler = space.add_collision_handler(1, 2) handler.post_solve = collision_handler # 主循环 while True: surface.fill(pg.Color('white')) for event in pg.event.get(): if event.type == pg.QUIT: pg.quit() sys.exit() space.step(1 / FPS) space.debug_draw(draw_options) pg.display.flip() clock.tick(FPS)
内容的提问来源于stack exchange,提问作者Broly
相关产品推荐
相关产品推荐

