You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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)

问题原因分析

  1. pre_solve回调的执行特性:pre_solve会在碰撞检测的多个阶段被调用(比如碰撞持续接触的帧内会多次触发),全局collided标记的重置逻辑无法精准区分「新碰撞」和「同一碰撞的持续检测」,导致部分碰撞触发被跳过。
  2. 形状修改时机错误:在pre_solve中直接移除并添加形状,会干扰当前帧的碰撞处理流程,导致后续碰撞检测失效。
  3. 转动惯量未更新:半径增大后,球体的转动惯量(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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 11:09:51