滑销联动演示分步演示

这是一个分步教程,解释了Pymunk中包含的演示slide_and_pinjoint.py。您会在列表中找到它的屏幕截图 examples .如果我错过了教程中的内容或不清楚的内容,最好将该文件放在附近。

../_images/slide_and_pinjoint1.png

在我们开始之前

在本教程中,您需要:

  • Python (当然)

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

  • Pymunk

本教程和包含的一些演示需要Pygame,但不需要只运行Pymunk。Pymunk也应该与其他类似的库一起工作,例如,您可以轻松地将此教程翻译为使用Pyglet。

Pymunk构建在2D物理库Munk 2D之上。Munk 2D本身是用C编写的,这意味着Pymunk需要调用c代码。Cffi库对此有帮助,但是,如果您所在的平台我无法在其上进行编译,则您可能需要自己进行。好消息是,这非常容易做到,事实上,如果您通过Pip安装了Pymunk,它就已经完成了!

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

>>> import pymunk

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

如果它不起作用或您遇到某种问题,请随时在花栗鼠论坛上发表帖子、直接联系我或将您的问题添加到问题跟踪器中: Contact & Support

空洞的模拟

好吧,开始吧。Munk 2D(以及Pymunk)有几个核心概念,这些概念在Munk 2D文档的引用中得到了很好的解释:

刚体

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

碰撞形状

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

约束/运动类型

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

空间

空间是Munk 2D中的基本模拟单元。您将实体、形状和关节添加到空间,然后更新整个空间。

Munk 2D的文档可以在此处找到:https://viblo.github.io/Munk2D/它用于c-library,但是Pymunk文档的很好补充,因为概念是相同的,只是Pymunk更适合使用Python。

可以在以下位置找到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函数在时间上将模拟向前推进一步。

备注

最好保持步进大小不变,不要根据帧率进行调整。如果步进大小保持不变,RST模拟将工作得更好。

落球

最容易处理(和绘制)的形状是圆形。因此,我们的下一步是偶尔让球产卵。在许多示例演示中,所有代码都在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,我们可以使用 debug_draw 方法已经包含在Pymunk中以简化一点。它首先需要导入,接下来我们必须创建一个 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_类型来创建静态主体。很多时候,使用空间中已经存在的静态物体会更容易 (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. 这是一个将坐标从Pymunk转换到PymGame世界的小功能。现在我们有了它,我们可以在 draw_ball() 功能也是如此。

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. 循环球并检查是否 body.position 小于 0 .

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

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

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

现在,完成了!你应该在屏幕中间有一个倒L形,里面充满了球,翻转而释放它们,翻转而重新开始。您可以检查Pymunk中包含的slide_and_pinjoint.py,但它并不完全遵循本教程,因为我将几个模块分解为函数,以便更容易以教程形式遵循。

如果有什么不清楚的地方,请随时在Github上提出问题。如果您对想阅读的另一个教程有想法,或者想在Pymunk中看到一些示例代码,请将其写在某个地方(例如在花栗鼠论坛中)

本教程的完整代码为:

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
    body = pymunk.Body()
    x = random.randint(120,300)
    body.position = x, 50
    shape = pymunk.Circle(body, radius, (0,0))
    shape.mass
    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()