2

I'm using Pymunk to simulate the physics of a box mounted on a pivot on top of a slender leg, which is attached to the ground. The pivot connecting the box to the leg is a combination of a PivotJoint and a SimpleMotor, simulating a primitive servo, that the user can turn on and off to shift the box's center of mass over the leg. The following is my Pymunk+Pygame code implementing this model:

from __future__ import print_function
import sys
from math import pi

import pygame
from pygame.locals import USEREVENT, QUIT, KEYDOWN, KEYUP, K_s, K_r, K_q, K_ESCAPE, K_UP, K_DOWN
from pygame.color import THECOLORS

import pymunk
from pymunk import Vec2d
import pymunk.pygame_util

LEG_GROUP = 1

class Simulator(object):

    def __init__(self):
        self.display_flags = 0
        self.display_size = (600, 600)

        self.space = pymunk.Space()
        self.space.gravity = (0.0, -1900.0)
        self.space.damping = 0.999 # to prevent it from blowing up.

        # Pymunk physics coordinates start from the lower right-hand corner of the screen.
        self.ground_y = 100
        ground = pymunk.Segment(self.space.static_body, (5, self.ground_y), (595, self.ground_y), 1.0)
        ground.friction = 1.0
        self.space.add(ground)

        self.screen = None

        self.draw_options = None

    def reset_bodies(self):
        for body in self.space.bodies:
            if not hasattr(body, 'start_position'):
                continue
            body.position = Vec2d(body.start_position)
            body.force = 0, 0
            body.torque = 0
            body.velocity = 0, 0
            body.angular_velocity = 0
            body.angle = body.start_angle

    def draw(self):
        ### Clear the screen
        self.screen.fill(THECOLORS["white"])

        ### Draw space
        self.space.debug_draw(self.draw_options)

        ### All done, lets flip the display
        pygame.display.flip()

    def main(self):

        pygame.init()
        self.screen = pygame.display.set_mode(self.display_size, self.display_flags)
        width, height = self.screen.get_size()
        self.draw_options = pymunk.pygame_util.DrawOptions(self.screen)

        def to_pygame(p):
            """Small hack to convert pymunk to pygame coordinates"""
            return int(p.x), int(-p.y+height)
        def from_pygame(p):
            return to_pygame(p)

        clock = pygame.time.Clock()
        running = True
        font = pygame.font.Font(None, 16)

        # Create the torso box.
        box_width = 50
        box_height = 100
        leg_length = 100
        leg_thickness = 2

        mass = 200
        points = [(-box_width/2, -box_height/2), (-box_width/2, box_height/2), (box_width/2, box_height/2), (box_width/2, -box_height/2)]
        moment = pymunk.moment_for_poly(mass, points)
        body1 = pymunk.Body(mass, moment)
        body1.position = (self.display_size[0]/2, self.ground_y+box_height/2+leg_length)
        body1.start_position = Vec2d(body1.position)
        body1.start_angle = body1.angle
        shape1 = pymunk.Poly(body1, points)
        shape1.group = LEG_GROUP
        shape1.friction = 0.8
        shape1.elasticity = 0.0
        self.space.add(body1, shape1)

        # Create bar 2 extending from the right to the origin.
        mass = 10
        points = [
            (leg_thickness/2, -leg_length/2),
            (-leg_thickness/2, -leg_length/2),
            (-leg_thickness/2, leg_length/2-leg_thickness),
            (leg_thickness/2, leg_length/2-leg_thickness/2)
        ]
        moment = pymunk.moment_for_poly(mass, points)
        body2 = pymunk.Body(mass, moment)
        body2.position = (self.display_size[0]/2-box_width/2, self.ground_y+leg_length/2)
        body2.start_position = Vec2d(body2.position)
        body2.start_angle = body2.angle
        shape2 = pymunk.Poly(body2, points)
        shape2.group = LEG_GROUP
        shape2.friction = 0.8
        shape2.elasticity = 0.0
        self.space.add(body2, shape2)

        # Link bars together at end.
        pj = pymunk.PivotJoint(body1, body2, (self.display_size[0]/2-box_width/2, self.ground_y+leg_length-leg_thickness))
        self.space.add(pj)

        # Attach the foot to the ground in a fixed position.
        pj = pymunk.PivotJoint(self.space.static_body, body2, (self.display_size[0]/2-box_width/2, self.ground_y+leg_thickness))
        self.space.add(pj)

        # Actuate the bars via a motor.
        motor_joint = pymunk.SimpleMotor(body1, body2, 0)
        self.space.add(motor_joint)

        # Add hard stops to leg pivot so the torso can't rotate through leg.
        hip_limit_joint = pymunk.RotaryLimitJoint(body1, body2, -pi/4., pi/4.) # -45deg:+45deg
        self.space.add(hip_limit_joint)

        pygame.time.set_timer(USEREVENT+1, 70000) # apply force
        pygame.time.set_timer(USEREVENT+2, 120000) # reset
        pygame.event.post(pygame.event.Event(USEREVENT+1))
        pygame.mouse.set_visible(False)

        simulate = False
        while running:
            # print('angles:', body1.angle, body2.angle)
            servo_angle = (body1.angle - body2.angle) * 180/pi # 0 degrees means leg is angled straight down
            servo_cw_enabled = servo_angle > -45
            servo_ccw_enabled = servo_angle < 45

            for event in pygame.event.get():
                if event.type == QUIT or (event.type == KEYDOWN and event.key in (K_q, K_ESCAPE)):
                    #running = False
                    sys.exit(0)
                elif event.type == KEYDOWN and event.key == K_s:
                    # Start/stop simulation.
                    simulate = not simulate
                elif event.type == KEYDOWN and event.key == K_r:
                    # Reset.
                    # simulate = False
                    self.reset_bodies()
                elif event.type == KEYDOWN and event.key == K_UP:
                    if servo_ccw_enabled:
                        motor_joint.rate = 5
                    else:
                        motor_joint.rate = 0
                elif event.type == KEYDOWN and event.key == K_DOWN:
                    if servo_cw_enabled:
                        motor_joint.rate = -5
                    else:
                        motor_joint.rate = 0
                elif event.type == KEYUP:
                    motor_joint.rate = 0

            self.draw()

            ### Update physics
            fps = 50
            iterations = 25
            dt = 1.0/float(fps)/float(iterations)
            if simulate:
                for x in range(iterations): # 10 iterations to get a more stable simulation
                    self.space.step(dt)

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

if __name__ == '__main__':
    sim = Simulator()
    sim.main()

You begin the simulation by pressing "s" and then use the up/down keys to rotate the box about the leg. It mostly behaves as expected...until it touches the ground, and then it begins to "bounce" violently. See this screen capture for an example.

How do I stop this? I've made the box very heavy by giving it a lot of mass, and I've set the elasticity of everything to 0. The docs mention things might be bouncy or squishy if you use less than 10 iterations per update, but I'm using 25. What else am I missing?

Cerin
  • 60,957
  • 96
  • 316
  • 522
  • 1
    When I tried the attached code it worked without any bouncing.. Which version of pymunk are you using? – viblo Apr 01 '18 at 08:22
  • 1
    See also the answer to your other question https://stackoverflow.com/a/49596407/412772 – viblo Apr 01 '18 at 12:52
  • 1
    @viblo I'm using the current version on PyPI, 5.3.2. Did you try letting it fall to the ground and then pressing up/down to actuate the servo? The bouncing usually happens automatically, but sometimes it requires an initial impulse to start, and when it does start, it doesn't stop. I applied your shape filter fix to this, but it had no effect. – Cerin Apr 01 '18 at 14:03

1 Answers1

2

Try setting the max_force of the motor joint. Without it the motor joint can use infinite power.

For example like this: motor_joint.max_force = 100000000

Also note that the group setting on the two shapes is wrong. shape2.group = LEG_GROUP does not do anything. In previous versions (<5.0) groups worked like this, but now you should use the ShapeFilter instead: shape1.filter = pymunk.ShapeFilter(group=LEG_GROUP)

viblo
  • 4,159
  • 4
  • 20
  • 28