滑销联动演示分步演示

这是一个循序渐进的教程,解释了pymunk中包含的DEMO Slide_and_pinjoint.py。您可以在以下列表中找到它的截图 examples 。如果我在教程中遗漏了什么或者有什么不清楚的地方,把文件放在旁边可能是个好主意。

../_images/slide_and_pinjoint1.png

在我们开始之前

在本教程中,您需要:

  • Python (当然)

  • PYGAME(网址:www.pygame.org)

  • Pymunk

本教程和所包含的一些演示程序需要使用PyGame,但并不是只要求运行pymunk。在其他类似的库中,Pymunk应该也可以很好地工作,例如,您可以很容易地将本教程翻译为使用Pyglet。

《Chipmunk》是在2D物理类库《Chipmunk》的基础上建造的。Chipmunk本身是用C编写的,这意味着Chipmunk需要调用C代码。CFFI库对此很有帮助,但是,如果您使用的是我无法编译的平台,则可能需要您自己进行编译。好消息是,这是非常容易做的,事实上,如果你用Pip得到了Pymunk,安装它已经完成了!

安装了pymunk后,尝试从python提示符下导入它,以确保它可以正常工作并且可以导入:

>>> import pymunk

有关安装的更多信息可在此处找到: Installation

如果它不起作用或你有什么问题,请随时在Chipmunk论坛上发表帖子,直接联系我,或将你的问题添加到问题跟踪器中: Contact & Support

空洞的模拟

好的,我们开始吧。Chipmunk有几个核心概念,这一点在《Chipmunk文献》的引文中得到了很好的解释:

刚体

刚体具有对象的物理属性。(质量、位置、旋转、速度等)它本身没有形状。如果你以前对粒子做过物理学,刚体的主要不同之处在于它们能够旋转。

碰撞形状

通过将形状附加到实体,可以定义实体的形状。可以将多个形状附加到单个实体以定义复杂形状,或者如果不需要形状,则不附加任何形状。

约束/运动类型

可以在两个实体之间附着关节以约束其行为。

空间

空间是Chipmunk的基本模拟单位。您可以将实体、形状和关节添加到空间,然后将空间作为一个整体进行更新。

Chipmunk的文档可以在这里找到:http://chipmunk-physics.net/release/ChipmunkLatest-Docs/它是针对c库的,但它是对Pymunk文档的一个很好的补充,因为它们的概念是相同的,只是Pymunk更易于使用。

可以在以下位置找到Pymunk的API文档: API参考

无论如何,我们现在准备编写一些代码::

import sys
import pygame
import pymunk #1

def main():
    pygame.init()
    screen = pygame.display.set_mode((600, 600))
    pygame.display.set_caption("Joints. Just wait and the L will tip over")
    clock = pygame.time.Clock()

    space = pymunk.Space() #2
    space.gravity = (0.0, 900.0)

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit(0)
            elif event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
                sys.exit(0)

        screen.fill((255,255,255))

        space.step(1/50.0) #3

        pygame.display.flip()
        clock.tick(50)

if __name__ == '__main__':
    sys.exit(main())

该代码将显示一个空白窗口,并将运行一个空白空间的物理模拟。

  1. 我们需要引入Pymunk才能使用它。

  2. 然后我们创造一个空间,并把它的重力设置为好的东西。记住,重要的是屏幕上看起来好的东西,而不是真实世界的价值。900将会是一个很好看的模拟,但是当你准备好完整的代码后,你可以自由地进行实验。

  3. 在我们的游戏循环中,我们在空间上调用Step()函数。每次调用时,Step函数在时间上将模拟向前推进一步。

备注

最好保持步长不变,而不是根据帧速率进行调整。在步长不变的情况下,物理模拟效果会好得多。

落球

最容易处理(和绘制)的形状是圆形。因此,我们的下一步是让一个球偶尔产卵。在许多示例演示中,所有代码都放在main()函数中的一个大堆中,因为它们非常小和容易,但我将在本教程中提取一些方法,以使其更容易理解。首先,将球添加到空格的函数::

def add_ball(space):
    mass = 3
    radius = 25
    body = pymunk.Body()  # 1
    x = random.randint(120, 300)
    body.position = x, 50  # 2
    shape = pymunk.Circle(body, radius)  # 3
    shape.mass = mass  # 4
    shape.friction = 1
    space.add(body, shape)  # 5
    return shape
  1. 我们首先创建球的主体。

  2. 我们设定了它的位置

  3. 为了让它与物体碰撞,它需要有一个(或多个)碰撞形状(S)。

  4. 所有物体的转动惯量都必须设定好。在大多数情况下,让Pymunk处理形状的计算是最容易的。所以我们设置每个形状的质量,然后当添加到空间时,物体将自动获得适当的质量和力矩设置。另一种选择是设置每个形状的密度,或者也可以直接在身体上设置值(甚至可以在之后调整它们)。

  5. 为了让球滚动,我们在形状上设置了摩擦力。(默认情况下为0)。

  6. 最后,我们将身体和形状添加到空间中,以将其包含在模拟中。请注意,主体必须始终添加到空间之前或与附加到其上的任何形状同时添加。

现在我们可以创建球了,我们想要展示它们。我们可以使用内置的pymunk_util包直接绘制整个空间,也可以手动绘制。Pymunk中包含的调试绘图功能非常适合于轻松快速地组合一些东西,而例如,一款经过打磨的游戏很可能会想要制作自己的绘图代码。

如果要手动绘制,则绘制函数可能如下所示::

def draw_ball(screen, ball):
    p = int(ball.body.position.x), int(ball.body.position.y)
    pygame.draw.circle(screen, (0,0,255), p, int(ball.radius), 2)

然后以这种方式调用(假设我们收集了一个名为Balls的列表中的所有球形状)::

for ball in balls:
    draw_ball(screen, ball)

然而,因为我们在本例中使用了pyGame,所以我们可以改用已经包含在Pymunk中的DEBUG_DRAW方法来简化一点。首先需要导入它,然后我们必须创建一个DrawOptions对象,其中包含以下选项(在使用PyGame的情况下,在哪个表面上绘制):

import pymunk.pygame_util
...
draw_options = pymunk.pygame_util.DrawOptions(screen)

在那之后,当我们想画出我们所有的形状时,我们就会这样做::

space.debug_draw(draw_options)

Pymunk附带的大多数示例都使用这种绘制方法。

使用ADD_BALL函数和DEBUG_DRAW调用以及一些生成球的代码,您应该会看到几个球落下。耶!

import sys, random
random.seed(1) # make the simulation the same each time, easier to debug
import pygame
import pymunk
import pymunk.pygame_util

#def add_ball(space):

def main():
    pygame.init()
    screen = pygame.display.set_mode((600, 600))
    pygame.display.set_caption("Joints. Just wait and the L will tip over")
    clock = pygame.time.Clock()

    space = pymunk.Space()
    space.gravity = (0.0, 900.0)

    balls = []
    draw_options = pymunk.pygame_util.DrawOptions(screen)


    ticks_to_next_ball = 10
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit(0)
            elif event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
                sys.exit(0)

        ticks_to_next_ball -= 1
        if ticks_to_next_ball <= 0:
            ticks_to_next_ball = 25
            ball_shape = add_ball(space)
            balls.append(ball_shape)

        space.step(1/50.0)

        screen.fill((255,255,255))
        space.debug_draw(draw_options)

        pygame.display.flip()
        clock.tick(50)

if __name__ == '__main__':
    main()

一动不动的L

落球很无聊。除了基本的重力,我们看不到任何物理模拟,每个人都可以在没有物理库帮助的情况下计算重力。所以让我们添加一些球可以落在上面的东西,两条静态的线组成一个L。就像对于球,我们从一个函数开始,在空格中添加一个L::

def add_static_L(space):
    body = pymunk.Body(body_type = pymunk.Body.STATIC) # 1
    body.position = (300, 300)
    l1 = pymunk.Segment(body, (-150, 0), (255, 0), 5) # 2
    l2 = pymunk.Segment(body, (-150, 0), (-150, -50), 5)
    l1.friction = 1 # 3
    l2.friction = 1

    space.add(body, l1, l2) # 4
    return l1,l2
  1. 我们创造了一个“静态”的身体。重要的一步是永远不要把它添加到空间中,就像动态的球体一样。注意静态主体是如何通过设置主体的BODY_TYPE来创建的。很多时候,使用空间中已经存在的静态实体会更容易 (space.static_body ),但我们将在稍后使L形状动态化。

  2. 这里创建了一个线形形状。

  3. 设置摩擦力。

  4. 同样,我们只将片段添加到空间中,而不是身体。

因为我们使用Space.DEBUG_DRAW来绘制空间,所以我们不需要为段执行任何特殊的绘制代码,但我仍然在这里包含一个可能的绘制函数,只是为了展示它可能是什么样子::

def draw_lines(screen, lines):
    for line in lines:
        body = line.body
        pv1 = body.position + line.a.rotated(body.angle) # 1
        pv2 = body.position + line.b.rotated(body.angle)
        p1 = to_pygame(pv1) # 2
        p2 = to_pygame(pv2)
        pygame.draw.lines(screen, THECOLORS["lightgray"], False, [p1,p2])
  1. 为了得到直线旋转时的位置,我们使用了这种计算。Line.a是该线的第一个端点,line.b是第二个端点。目前这些线是静止的,没有旋转,所以我们不需要做额外的计算,但我们很快就会让它们移动和旋转。

  2. 这是一个小函数,可以将坐标从土拨鼠转换到土拨鼠世界。现在我们有了它,我们也可以在DrawBall()函数中使用它。

def to_pygame(p):
    """Small helper to convert pymunk vec2d to pygame integers"""
    return round(p.x), round(p.y)

有了完整的代码,我们应该类似于下面的内容,现在我们应该看到中间倒置的L形状,Will Ball正在产卵并击中该形状。

import sys, random
random.seed(1) # make the simulation the same each time, easier to debug
import pygame
import pymunk
import pymunk.pygame_util

#def to_pygame(p):
#def add_ball(space):
#def add_static_l(space):

def main():
    pygame.init()
    screen = pygame.display.set_mode((600, 600))
    pygame.display.set_caption("Joints. Just wait and the L will tip over")
    clock = pygame.time.Clock()

    space = pymunk.Space()
    space.gravity = (0.0, 900.0)

    lines = add_static_L(space)
    balls = []
    draw_options = pymunk.pygame_util.DrawOptions(screen)

    ticks_to_next_ball = 10
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit(0)
            elif event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
                sys.exit(0)

        ticks_to_next_ball -= 1
        if ticks_to_next_ball <= 0:
            ticks_to_next_ball = 25
            ball_shape = add_ball(space)
            balls.append(ball_shape)

        space.step(1/50.0)

        screen.fill((255,255,255))
        space.debug_draw(draw_options)

        pygame.display.flip()
        clock.tick(50)

if __name__ == '__main__':
    main()

关节(1)

静态的L造型很无聊。因此,让我们通过添加两个关节来使它更令人兴奋,一个关节可以绕它旋转,另一个关节防止它旋转太多。在此部分中,我们只添加旋转关节,在下一个部分中,我们将约束它。因为我们的静态L形状将不再是静态的,所以我们还将该函数重命名为Add_L()。**

def add_L(space):
    rotation_center_body = pymunk.Body(body_type=pymunk.Body.STATIC)  # 1
    rotation_center_body.position = (300, 300)

    body = pymunk.Body()
    body.position = (300, 300)
    l1 = pymunk.Segment(body, (-150, 0), (255.0, 0.0), 5.0)
    l2 = pymunk.Segment(body, (-150.0, 0), (-150.0, -50.0), 5.0)
    l1.friction = 1
    l2.friction = 1
    l1.mass = 8  # 2
    l2.mass = 1
    rotation_center_joint = pymunk.PinJoint(
        body, rotation_center_body, (0, 0), (0, 0)
    )  # 3

    space.add(l1, l2, body, rotation_center_joint)
    return l1, l2
  1. 这是旋转中心体。它的唯一用途是充当关节中的静态点,以便直线可以围绕它旋转。如你所见,我们从不给它添加任何形状。

  2. L的形状现在将在世界上移动,因此它不再是一个静态的物体。在这里,我们看到了将质量设置在形状上而不是身体上的好处,不需要计算力矩应该有多大,Pymunk会自动计算重心。

  3. 销联接允许两个对象绕单个点旋转。在我们的例子中,其中一个物体将粘在世界上。

关节(2)

在前一部分中,我们添加了销联接,现在是约束旋转的L形状以创建更有趣的模拟的时候了。为此,我们修改了Add_L()函数::

def add_L(space):
    rotation_center_body = pymunk.Body(body_type = pymunk.Body.STATIC)
    rotation_center_body.position = (300,300)

    rotation_limit_body = pymunk.Body(body_type = pymunk.Body.STATIC) # 1
    rotation_limit_body.position = (200,300)

    body = pymunk.Body()
    body.position = (300,300)
    l1 = pymunk.Segment(body, (-150, 0), (255.0, 0.0), 5.0)
    l2 = pymunk.Segment(body, (-150.0, 0), (-150.0, -50.0), 5.0)
    l1.friction = 1
    l2.friction = 1
    l1.mass = 8
    l2.mass = 1

    rotation_center_joint = pymunk.PinJoint(body, rotation_center_body, (0,0), (0,0))
    joint_limit = 25
    rotation_limit_joint = pymunk.SlideJoint(body, rotation_limit_body, (-100,0), (0,0), 0, joint_limit) # 2

    space.add(l1, l2, body, rotation_center_joint, rotation_limit_joint)
    return l1,l2
  1. 我们加上一具身体..。

  2. 创建滑动关节。它的行为类似于销联接,但具有最小和最大距离。这两个物体可以在最小和最大之间滑动,在我们的例子中,其中一个物体是静态的,这意味着只有连接了形状的物体才会移动。

收尾

你可能注意到了,我们从不删除球。这会使模拟需要越来越多的内存,占用越来越多的CPU,这当然不是我们想要的。因此,在最后一步中,我们添加了一些代码,以便当球在屏幕下方时从模拟中删除它们。**

balls_to_remove = []
for ball in balls:
    if ball.body.position.y < 0: # 1
        balls_to_remove.append(ball) # 2

for ball in balls_to_remove:
    space.remove(ball, ball.body) # 3
    balls.remove(ball) # 4
  1. 让球循环,检查身体的位置是否小于0。

  2. 如果是这样的话,我们把它添加到我们要移除的球的列表中。

  3. 要从空间中移除一个物体,我们需要移除它的形状和身体。

  4. 然后我们把它从我们的球列表中删除。

现在,完成了!你应该在屏幕中央有一个倒置的L形状,填满意志球,倾倒释放它们,然后向后倾斜,然后重新开始。您可以检查pymunk中包含的lide_and_pinjoint.py,但它并不完全遵循本教程,因为我将几个块分解为函数,以便于以教程的形式进行操作。

如果有任何不清楚的地方,不工作的人可以随时在GitHub上提出问题。如果你对另一个你想读的教程有一个想法,或者你想在Chipmunk中看到一些示例代码,请把它写在某个地方(比如在Chipmunk论坛)

本教程的完整代码为:

import sys, random
random.seed(1) # make the simulation the same each time, easier to debug
import pygame
import pymunk
import pymunk.pygame_util

def add_ball(space):
    """Add a ball to the given space at a random position"""
    mass = 3
    radius = 25
    inertia = pymunk.moment_for_circle(mass, 0, radius, (0,0))
    body = pymunk.Body(mass, inertia)
    x = random.randint(120,300)
    body.position = x, 50
    shape = pymunk.Circle(body, radius, (0,0))
    shape.friction = 1
    space.add(body, shape)
    return shape

def add_L(space):
    """Add a inverted L shape with two joints"""
    rotation_center_body = pymunk.Body(body_type = pymunk.Body.STATIC)
    rotation_center_body.position = (300,300)

    rotation_limit_body = pymunk.Body(body_type = pymunk.Body.STATIC)
    rotation_limit_body.position = (200,300)

    body = pymunk.Body(10, 10000)
    body.position = (300,300)
    l1 = pymunk.Segment(body, (-150, 0), (255.0, 0.0), 5.0)
    l2 = pymunk.Segment(body, (-150.0, 0), (-150.0, -50.0), 5.0)
    l1.friction = 1
    l2.friction = 1
    l1.mass = 8
    l2.mass = 1

    rotation_center_joint = pymunk.PinJoint(body, rotation_center_body, (0,0), (0,0))
    joint_limit = 25
    rotation_limit_joint = pymunk.SlideJoint(body, rotation_limit_body, (-100,0), (0,0), 0, joint_limit)

    space.add(l1, l2, body, rotation_center_joint, rotation_limit_joint)
    return l1,l2

def main():
    pygame.init()
    screen = pygame.display.set_mode((600, 600))
    pygame.display.set_caption("Joints. Just wait and the L will tip over")
    clock = pygame.time.Clock()

    space = pymunk.Space()
    space.gravity = (0.0, 900.0)

    lines = add_L(space)
    balls = []
    draw_options = pymunk.pygame_util.DrawOptions(screen)

    ticks_to_next_ball = 10
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit(0)
            elif event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
                sys.exit(0)

        ticks_to_next_ball -= 1
        if ticks_to_next_ball <= 0:
            ticks_to_next_ball = 25
            ball_shape = add_ball(space)
            balls.append(ball_shape)

        screen.fill((255,255,255))

        balls_to_remove = []
        for ball in balls:
            if ball.body.position.y > 550:
                balls_to_remove.append(ball)

        for ball in balls_to_remove:
            space.remove(ball, ball.body)
            balls.remove(ball)

        space.debug_draw(draw_options)

        space.step(1/50.0)

        pygame.display.flip()
        clock.tick(50)

if __name__ == '__main__':
    main()