news 2026/10/26 17:51:52

Python 物理引擎pymunk最完整教程(下)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python 物理引擎pymunk最完整教程(下)

上一篇文章:Python 物理引擎pymunk最完整教程(中)
总目录:https://gitcode.com/qq_48979387/csdn-articles/blob/master/pymunk/README.md

8 空间管理

8.1 管理身体、形状、约束

身体、形状、以及约束对象都要被添加到空间中才能起作用。添加的方法是Space.add方法。有一些功能只有在物体被加入空间后才能使用,例如查询。

space.add(body1,shape2,constraint1,constraint2,body2,...)

身体、形状、约束被加入空间后,以列表形式被储存在Space.bodies, shapes, constraints几个属性中。

与add相反,Space.remove方法可以用于删除被加入空间的身体、形状、或约束。但是需要注意:如果要删除一个物体,那么需要同时删除这个物体的Body, Shape, 以及附加的Constraint对象。只删除一个Body对象或是只删除一个Shape都无法彻底地删除这个物体。
Space.remove方法主要用于删除“飞出边界”的物体。例如一个小球从一个斜坡上落下去,如果不加以处理,它会一直下坠而不会自动删除,拖慢了程序的速度。

forshapeinspace.shapes:# 遍历空间内所有形状ifshape.body.position.y>...:# 如果飞出了边界space.remove(shape,shape.body)# 删除形状和对应身体

8.2 static_body

Space对象在创建时自带一个static_body属性,表示一个静态的Body对象,位于(0,0)。通常不需要直接将这个属性和Shape绑定,也不需要将它添加到空间,但是使用它能方便一些操作,比如添加固定支点的约束。

importpymunk space=pymunk.Space()space.gravity=(0,500)b=pymunk.Body()b.position=(100,100)s=pymunk.Segment(b,(0,0),(50,50),5)s.mass=1space.add(b,s,pymunk.PivotJoint(b,space.static_body,b.position))if__name__=="__main__":importutil util.run(space)


这段代码实现了一个钟摆的效果。前文已经介绍过,PivotJoint创建一个单点作为约束点的关节,使用世界坐标表示约束点。这段代码直接将static_body和线段的body进行了约束。

8.3 复制和转储空间

pymunk.Space对象可以被复制或用pickle转储。通过Space.copy方法可以复制Space对象,得到一个新的空间。

>>>space=pymunk.Space()>>>space2=space.copy()

Space也可以被pickle模块进行转储和载入。pickle模块可以直接将一些python对象转换为字符串一样的数据,可以将pickle后的数据转换为python对象。关于pickle详细用法参见:https://blog.csdn.net/Hardworking666/article/details/112754839

importpymunkimportrandom# 随机数模块importpickle# python对象序列化与反序列化模块defadd_circle():b=pymunk.Body()b.position=(random.randint(100,900),100)# 随机位置s=pymunk.Circle(b,random.randint(1,50))# 创建半径随机大小的圆形s.mass=1s.elasticity=0.8# 设置弹性系数space.add(b,s)try:space=pickle.load(open("savefile.p","rb"))# 载入空间,以二进制形式读取exceptFileNotFoundError:# 没找到储存的空间space=pymunk.Space()# 创建新的空间space.gravity=(0,500)body=pymunk.Body(body_type=pymunk.Body.STATIC)# 创建静态地面body.position=(0,550)shape=pymunk.Segment(body,(0,10),(1000,10),3)shape.elasticity=0.8space.add(body,shape)foriinrange(random.randint(2,6)):# 添加随机2-6个圆形add_circle()count=0# 计数器defupdate():globalcount count+=1ifcount>120:# 每过2秒保存一次pymunk空间count=0pickle.dump(space,open("savefile.p","wb"))# 以二进制形式将pymunk空间储存到文件中print("Saved!")if__name__=="__main__":importutil util.run(space,update)

这段代码首先在空间中添加随机数量和随机大小的几个圆形,每过2s就会储存一次空间。打印出"Saved!"后,重新运行程序,发现运行结果和上一次储存时的空间内容一样,说明pickle成功转储了空间。目录下有一个savefile.p文件,表示的就是pickle储存空间的内容。

8.4 Space对象属性参考

classpymunk.Space(threaded: bool = False)
创建一个pymunk空间。所有身体、形状、约束都必须被添加到一个正确的空间中才能起作用。
threaded表示是否使用线程来运行物理求解器,这在空间数量较多时可能有用(Windows系统不支持)。
空间可以被复制或用pickle转储。
add(*objs: Body | Shape | Constraint) → None
将身体、形状、约束加入空间
add_collision_handler(collision_type_a: int, collision_type_b: int) → CollisionHandler
添加碰撞处理器,处理collison_type_a和collision_type_b发生的碰撞(详见7.1)
add_default_collision_handler() → CollisionHandler
添加默认碰撞处理器,用于处理那些没有通过普通碰撞处理器和通配符碰撞处理器进行处理的碰撞(详见7.5)
add_wildcard_collision_handler(collision_type_a: int) → CollisionHandler
添加通配符碰撞处理器,用于处理所有与collision_type_a发生的碰撞(详见7.5)
add_post_step_callback(callback_function: Callable[[…], None], key: Hashable, *args: Any, **kwargs: Any) → bool
注册一个回调函数,将在下一次Space.step结束后调用。
func(space : Space, key, *args, **kwargs)
bb_query(bb: BB, shape_filter: ShapeFilter) → List[Shape]
查询边界框附近的形状列表(详见7.6)
propertybodies: List[Body]
所有加入空间的身体对象
propertyconstraints: List[Constraint]
所有加入空间的约束对象
propertyshapes: List[Shape]
所有加入空间的形状对象
propertycollision_bias: float
决定空间以多快的速度将两个重叠的物体推开,值在0-1之间,但一般不需要改变这个值。pymunk允许快速移动的物体重叠,然后随着时间的推移修复重叠(把重叠的物体推开);例如两个小球相撞,小球的形状之间会发生重叠,然后通过修复而弹开。
默认值约为0.2%,表示经过1s后剩余0.2%的重叠没有修复。
propertycollision_persistence: float
空间保持碰撞解决方案的帧数,一般不用改变这个值。
propertycollision_slop: float
允许的形状之间的重叠量。为了提高稳定性,将这个值设置得尽可能高,就不会出现明显的重叠。默认值为0.1。
copy() → T
复制空间(深层复制)
propertycurrent_time_step: float
上一次调用(或当前调用)Space.step所指定的时间步长。
propertydamping: float
空间的阻尼系数(详见2.5)
propertygravity: Vec2d
空间的重力加速度(详见2.5)
debug_draw(options: SpaceDebugDrawOptions) → None
绘制当前pymunk空间中的内容,options是绘制选项(详见后文)
propertyidle_speed_threshold: float
一个物体被认为是进入了“空闲状态”的速度。如果一个物体的速度很小,基本可以被认为是静止了,这个速度小于idle_speed_threshold,就会认为这个物体进入了空闲状态。默认值为0,表示根据重力加速度估计一个合适的空闲速度。

“空闲”和“睡眠”不是一个概念,空闲是指这个身体静止或几乎静止了,而睡眠是指让这个物体的各属性不再刷新。

propertyiterations: int
物理求解器的迭代数量,默认为10。这个值设置的越大,物理效果越真实,但是也会增加CPU占用。
point_query(point: Tuple[float, float], max_distance: float, shape_filter: ShapeFilter) → List[PointQueryInfo]
查询与point这个点距离为max_distance以内的所有形状,包含传感器(详见7.6)
point_query_nearest(point: Tuple[float, float], max_distance: float, shape_filter: ShapeFilter) → PointQueryInfo | None
查询与point这个点距离为max_distance以内的最近的形状,不包含传感器(详见7.6)
reindex_shape(shape: Shape) → None
当更改了某个身体、形状的一些属性(如:位置)后,如果立即进行形状查询可能不准。此时可以调用这个方法来刷新给定shape的碰撞信息来避免这一点。
reindex_shapes_for_body(body: Body) → None
刷新给定身体对应的所有形状的碰撞信息。
reindex_static() → None
刷新空间中静态形状的碰撞检测信息。只有在移动静态形状时才需要调用这个函数。
remove(*objs: Body | Shape | Constraint) → None
从空间中删除身体、形状、约束。
segment_query(start: Tuple[float, float], end: Tuple[float, float], radius: float, shape_filter: ShapeFilter) → List[SegmentQueryInfo]
沿着start到end,以radius为线段半径,查询与此线段相交的所有形状,包含传感器(详见7.6)
segment_query_first(start: Tuple[float, float], end: Tuple[float, float], radius: float, shape_filter: ShapeFilter) → SegmentQueryInfo | None
沿着start到end,以radius为线段半径,查询与此线段相交的所有形状,不包含传感器(详见7.6)
shape_query(shape: Shape) → List[ShapeQueryInfo]
查询与给定形状相交的所有形状,包含传感器(详见7.6)。
propertysleep_time_threshold: float
物体进入空闲状态多少秒后,进入睡眠状态。默认为float(“inf”)表示禁用睡眠。如果不设置这个值,那么无法使用Body.sleep方法。(详见3.3)
propertystatic_body: Body
表示一个位于(0,0)的静态身体对象(详见8.2)。
step(dt: float) → None
使物理空间执行一段时间
propertythreads: int
如果创建空间时启用了线程,则表示用于通过线程运行step的数量,最大数量为2,默认为1。(Windows系统不支持)
use_spatial_hash(dim: float, count: int) → None
将空间切换为使用空间散列而不是边界框树。dim是哈希单元的大小,count是哈希表中建议的最小单元格数

9 其他功能

9.1 自动几何图形转换

pymunk.autogeometry模块用于将图像类的信息转换成几何图形,从而创建符合形状的多边形。

pymunk.autogeometry.march_hard(bb:BB,x_samples:int,y_samples:int,threshold:float,sample_func:Callable[[Tuple[float,float]],float])→ PolylineSet

autogeometry.march_hard方法对需要转换为几何图形的内容进行采样并返回多边形点集对象autogeometry.PolylineSet。首先需要提供几何图形的边界框作为参数,然后决定x方向上的采样和y方向上的采样数量,还需要提供threshold表示算法的容错率。采样时,pymunk会按照采样数量遍历边界框内的点的坐标,然后将坐标传递给sample_func回调函数。sample_func回调函数接受一个2D向量作为参数,需要返回一个布尔值,决定是否对当前坐标的点进行标记。march_hard会将所有标记的点储存到PolylineSet对象中,并进行一些删减、分类之类的处理,从而就有了创建多边形的每一个点。
示例如下:

importpymunkfrompymunkimportautogeometryfrompprintimportpprint# 漂亮打印,只是为了输出的结果更好看,与pymunk无关defsample_func(point):x=int(point[0])y=int(point[1])# 采样点不是整数,需要转换为整数后进行列表切片marked=img[y][x]ifmarked=="x":# 判断当前采样点是否被标记returnTrue# 被标记的采样点会被储存到PolylineSet中else:returnFalseimg=["...xx...","...xx...","...xx...","...xxxx."]# .表示空像素点区域,x表示这个位置有像素w=len(img[0])# 图像的宽度(x方向上的长度)h=len(img)# 图像的高度(y方向上的长度)pl_set=autogeometry.march_hard(pymunk.BB(0,0,w-1,h-1),w,h,0.1,sample_func)pprint(list(pl_set))# 打印出采样的结果

运行后输出:

[[Vec2d(2.5, 3.0), Vec2d(2.5, 2.0), Vec2d(2.5, 1.0), Vec2d(2.5, 0.0)], [Vec2d(5.5, 0.0), Vec2d(5.5, 1.0), Vec2d(5.5, 2.0), Vec2d(5.5, 2.5), Vec2d(6.0, 2.5), Vec2d(6.5, 2.5), Vec2d(6.5, 3.0)]]

这个示例中,设置了采样范围为:(0,0,w-1,h-1),在x方向上有w个采样点。因此,pymunk在x方向上会从0遍历到w-1,步长为1。采样过程的python代码表示如下(实际算法肯定有所不同,只是下面的写法比文字表述容易理解):

#bb: BB, x_samples: int, y_samples: int, sample_func: Callable[[Tuple[float, float]], float]defmarch_hard(...):forxinrange(bb.left,bb.right+1,int(bb.right-bb.left)//x_samples):foryinrange(bb.bottom,bb.top+1,int(bb.top-bb.bottom)//y_samples):ifsample_func((x,y)):...# 对这个点进行标记else:...# 舍弃

除了march_hard之外,autogeometry还有一个march_soft方法,用法和march_hard完全一样,只是适用范围不同。march_hard适合那种线条横平竖直的图像,march_soft则对采样点进行了抗锯齿的处理,使得采样线条更加平滑。

通过march_hard方法进行采样后,需要对采样后的数据进行处理。需要注意的是,march_hard所返回的PolylineSet对象转换为列表后,里面可能有多个小列表,表示一条条折线。如果要创建多边形Poly的话,首先需要把这些列表拼接起来。直接遍历列表进行拼接效果有时候不是特别理想,如果想要完美复现形状可以将这些小列表中的折线用一条条Segment创建出来。

importpymunkfrompymunkimportautogeometrydefsample_func(point):x=int(point[0])y=int(point[1])# 采样点不是整数,需要转换为整数后进行列表切片marked=img[y][x]ifmarked=="x":# 判断当前采样点是否被标记returnTrue# 被标记的采样点会被储存到PolylineSet中else:returnFalseimg=["...xx...","...xx...","...xx...","...xxxx."]# .表示空像素点区域,x表示这个位置有像素w=len(img[0])# 图像的宽度h=len(img)# 图像的高度pl_set=autogeometry.march_hard(pymunk.BB(0,0,w-1,h-1),w,h,0.1,sample_func)pl=[pointforainpl_setforpointinautogeometry.simplify_vertexes(a,1)]# 将列表拼接起来,调用simplify_curves是为了优化折线效果space=pymunk.Space()b=pymunk.Body()b.position=(100,100)shape=pymunk.Poly(b,pl,pymunk.Transform.scaling(10),radius=1)# 创建多边形,放大10倍shape.mass=1space.add(b,shape)if__name__=="__main__":importutil util.run(space)

运行效果如下:

效果并不是很理想,因为拼接列表并不是按照点的顺序将折线连接起来的,需要其他算法进行处理。
代码中还使用了autogeometry.simplify_curves函数。这个函数接受一个折线的点列表,它会根据算法优化这个列表。这个函数还需要一个参数表示算法容错,根据实际情况设定即可。

>>>autogeometry.simplify_vertexes([(0,0),(10,0),(20,0),(20,20),(10,20),(0,20)],0)[Vec2d(0.0,0.0),Vec2d(20.0,0.0),Vec2d(20.0,20.0),Vec2d(0.0,20.0)]

上面这个示例中,需要优化的点列表表示一个长方形,但是其中包含了长方形的非支点。经过优化后,只保留了长方形的四个支点坐标。
与这个方法类似,simplify_curves方法也用于优化点的列表,但是这个函数比较适合平滑或者轻微弯曲的图形,而simplify_vertexes适合边缘直或有角度的图形。

下面这个示例展示了使用Segment连接图形的轮廓,创建出一个和pymunk logo一样的图形。

importpymunkimportpygameaspgfrompymunkimportautogeometry space=pymunk.Space()space.gravity=(0,500)surf=pg.image.load("pymunk_logo.png")# 导入图像w,h=surf.get_size()# 获取图像的宽和高defsample_func(point):color=surf.get_at(point)# 获取位于point的像素颜色return1ifcolor[3]else0# 像素是否为不透明的pl_set=autogeometry.march_soft(pymunk.BB(0,0,w-1,h-1),100,100,0.1,sample_func)# 抗锯齿取样polyline=[bforainlist(pl_set)forbina]line=pymunk.autogeometry.simplify_curves(polyline,1.0)# 优化多边形各点列表b=pymunk.Body()b.position=(100,100)space.add(b)d=1foriinrange(len(line)-1):p1=line[i]*d# 线段的起始点p2=line[i+1]*d# 线段的终点shape=pymunk.Segment(b,p1,p2,1)shape.mass=1shape.elasticity=0.8space.add(shape)b=pymunk.Body(body_type=pymunk.Body.STATIC)# 创建静态地面b.position=(0,550)shape=pymunk.Segment(b,(0,10),(1000,10),3)shape.elasticity=0.8space.add(b,shape)if__name__=="__main__":importutil util.run(space)

->

关于autogeometry更多方法可参考:https://www.pymunk.org/en/latest/pymunk.autogeometry.html

9.2 调试

通常情况下,pymunk不提供有关绘图的内容,因此需要与pygame, pyglet, matplotlib等图形模块相结合,才能将绘制的内容显示到窗口上。但是为了方便测试,pymunk提供了调试绘图方法Space.debug_draw。
Space.debug_draw方法需要一个参数表示绘制选项。一般的绘图选项类是pymunk.SpaceDebugDrawOptions,但是这个选项并不创建窗口绘制内容,而是输出绘制的内容。

importpymunk space=pymunk.Space()body=pymunk.Body()body.position=(100,100)shape=pymunk.Circle(body,10)shape.mass=1space.add(body,shape)options=pymunk.SpaceDebugDrawOptions()# 创建绘图选项对象whileTrue:space.step(0.01)# 运行空间一段时间(0.01s)space.debug_draw(options)# 绘制空间中的内容

运行后,程序不断输出:

draw_circle (Vec2d(100.0, 100.0), 0.0, 10.0, SpaceDebugColor(r=44.0, g=62.0, b=80.0, a=255.0), SpaceDebugColor(r=52.0, g=152.0, b=219.0, a=255.0)) draw_circle (Vec2d(100.0, 100.0), 0.0, 10.0, SpaceDebugColor(r=44.0, g=62.0, b=80.0, a=255.0), SpaceDebugColor(r=52.0, g=152.0, b=219.0, a=255.0)) ...

除此之外,pymunk.pygame_util, pyglet_util, matplotlib_util中各包含一个DrawOptions类表示绘制选项,分别用于在pygame, pyglet, matplotlib模块中的调试绘图。
以pygame为例:

importpygameaspgimportpymunkfrompymunk.pygame_utilimportDrawOptions# 导入pygame_util中的绘图选项space=pymunk.Space()body=pymunk.Body()body.position=(100,100)shape=pymunk.Circle(body,10)shape.mass=1space.add(body,shape)pg.init()screen=pg.display.set_mode((1000,600))clock=pg.time.Clock()options=DrawOptions(screen)# pygame的绘图选项,需要提供pygame窗口表面作为参数whileTrue:screen.fill((255,255,255))foreventinpg.event.get():ifevent.type==pg.QUIT:pg.quit()exit()space.step(1/60)# 运行pymunk空间1/60sspace.debug_draw(options)# 调试绘图pg.display.flip()clock.tick(60)


事实上,这正类似于前文用于测试代码的util.py所做的(util.py中还包含了一些附加功能)。

DrawOptions有一个重要的功能就是整体变换。可以通过设置一个transform参数,来对整个画面进行缩放、平移、旋转等操作。

options=pymunk.SpaceDebugDrawOptions()options.transform=pymunk.Transform.scaling(2)@ pymunk.Transform.translation(2,3)

关于矩阵变换的使用已在前文https://blog.csdn.net/qq_48979387/article/details/140383410#t4中介绍,这里不详述。

还有很多别的绘图选项也不是一成不变的,形状颜色,绘制哪些东西,怎么绘制,这些都是可以进行修改的,尽管一般不需要。详见:https://www.pymunk.org/en/latest/pymunk.html#pymunk.SpaceDebugDrawOptions

9.3 其他子模块

batch

batch模块用于对一些操作进行批处理,可以有效提高程序的运行效率。但是batch模块目前是实验性的模块,且涉及到较多内存操作,这里暂不详细介绍。
关于batch模块可以参考pymunk.examples.planet。文档位于:https://www.pymunk.org/en/latest/pymunk.batch.html

examples

pymunk的官方教程并不怎么详细,但是读者可以通过pymunk文件夹下的examples文件中的程序学习pymunk。其中包含一些经典的示例。关于全部示例以及介绍参见:https://www.pymunk.org/en/latest/examples.html

10 总结

关于本教程的任何疏漏之处和建议欢迎提出。

另附一pymunk+pygame游戏实战代码,包含详细注释,可在https://gitcode.com/qq_48979387/csdn-articles/tree/master/pymunk/2048下载。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 17:50:25

智能电动汽车电磁屏蔽安全:从EMI原理到工程实践全文解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/26 17:49:37

电容选型与电路设计:从原理、ESR到滤波去耦的实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/26 17:48:27

基于PLC的污水泵站排水联锁控制系统设计与工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/26 17:46:46

CAN总线技术选型指南:经典CAN、CAN FD、CAN XL与TTCAN对比解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/26 17:46:38

嵌入式工程师转机器人岗位:简历被筛掉的真正原因与破局思路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华