22 May 2019

Pymunk tutorial

If you need to create a 2D physics environment quickly and without bothering about C++ compilation issues, it's better to choose Pymunk over Box2D. The only issue with Pymunk however, is that there's currently only one person (the Pymunk author)  answering questions about Pymunk on StackOverflow.

To get started, install Pymunk and Pygame:
pip install pygame
pip install pymunk


You can also install using pip3 if you use Python 3. Pymunk is used only for doing the 2D physics simulations. It is Pygame that sets up the environment to display the objects created in Pymunk. Instead of Pygame, you can also use Pyglet. Pyglet has features like multiple window support which Pygame doesn't have (yet), but the documentation is in my opinion, in need of improvement.

Do note that although Pymunk has a debug_draw() function to display Pymunk objects, the function is just there for the sake of being there. You are actually supposed to use Pygame's drawing functions to draw Pymunk objects. It's more efficient and supports culling of objects (not needing to draw objects outside screen bounds).


Simple program:


import sys
import pygame
from pygame.locals import * #for key codes etc
from pygame.color import *
import pymunk
import pymunk.pygame_util

fps = 60.0 #frames per second
worldWidth = 1200
worldHeight = 300
#---initializations and setup
pygame.init()
screen = pygame.display.set_mode((worldWidth, worldHeight))
clock = pygame.time.Clock()
space = pymunk.Space()
space.gravity = 0, 900
space.sleep_time_threshold = 1 #The time a group of bodies must remain idle in order to fall asleep
draw_options = pymunk.pygame_util.DrawOptions(screen)
pymunk.pygame_util.positive_y_is_up = False

#---setup line segments in space
x = 10; y = 150; thickness = 2; friction = 1.0
points = [(-100,0),(100,0),(112,12),(125,0),(137,12),(150,0),(162,12),(175,0),(200,0),(250,25),(250,0),(315,0),(320,70),(375,70),(385,0),(425,0),(425,12),(448,12),(448,0),(475,0),(475,12),(485,12),(485,0),(512,0),(512,12),(527,12),(527,0),(590,0),(590,10),(610,10),(610,15),(625,15),(625,25),(646,25),(646,35),(660,35),(660,45),(680,48),(710,-10),(755,5),(755,20),(780,25),(850,25),(825,-20),(865,-20),(895,25),(925,25),(935,0),(980,-75),(1100,-75)];
for i in range(1, len(points)):           
    floor = pymunk.Segment(space.static_body, (x+points[i-1][0], y+points[i-1][1]), (x+points[i][0], y+points[i][1]), thickness)
    floor.friction = friction
    space.add(floor)
   
while True:
    for event in pygame.event.get():
        if event.type == QUIT or event.type == KEYDOWN and (event.key in [K_ESCAPE, K_q]): 
            sys.exit(0)

    #---updations
    space.step(1./fps)#Update the space for the given time step.
    screen.fill(pygame.color.THECOLORS["black"])#clear screen
    space.debug_draw(draw_options)#try using Pygame's drawing functions instead
    pygame.display.flip()#flips the draw buffer to display the objects in current frame
    dt = clock.tick(fps)#creates a delay



Adding ten cars to the space:


You'll notice the cars collide with each other. That's fixed in the next code example below.

import sys
import pygame
from pygame.locals import * #for key codes etc
from pygame.color import *
import pymunk
import pymunk.pygame_util

from pymunk import Vec2d
import random

class Car:
    space = None
    physics = None
    x = 0
    y = 0
    friction = 1.5  
    #---range of values possible for properties of car
    speed_range = range(10, 30, 5)#15
    chWd_range = range(5, 70, 5)#50#chassis width
    chHt_range = range(5, 50, 5)#10#chassis height
    wheel1Radius_range = range(6, 25, 5)#15
    wheel2Radius_range = range(6, 25, 5)#15
    chassisMass_range = range(10, 500, 50)#100
    wheel1Mass_range = range(10, 500, 50)#100
    wheel2Mass_range = range(10, 500, 50)#100 
    #---properties of car   
    speed = 0
    chWd = 0#chassis width
    chHt = 0#chassis height
    wheel1Radius = 0
    wheel2Radius = 0
    chassisMass = 0
    wheel1Mass = 0
    wheel2Mass = 0 
    #---parts to add or remove from space
    chassis_b = None
    chassis_s = None
    wheel1_b = None
    wheel1_s = None
    wheel2_b = None
    wheel2_s = None 
    pin1 = None
    pin2 = None
    pin3 = None
    pin4 = None
    motorJoint1 = None
    motorJoint2 = None
   
    def __init__(self, space, pymunk, xStartPos, yStartPos):
        self.space = space
        self.pymunk = pymunk
        self.x = xStartPos
        self.y = yStartPos
        self.reinitializeWithRandomValues()   
       
    def reinitializeWithRandomValues(self):
        self.speed = random.choice(self.speed_range)
        self.chWd = random.choice(self.chWd_range)
        self.chHt = random.choice(self.chHt_range)
        self.wheel1Radius = random.choice(self.wheel1Radius_range)
        self.wheel2Radius = random.choice(self.wheel2Radius_range)
        self.chassisMass = random.choice(self.chassisMass_range)
        self.wheel1Mass = random.choice(self.wheel1Mass_range)
        self.wheel2Mass = random.choice(self.wheel2Mass_range)    
       
    def setValues(self, v):
        i = 0
        self.speed = v[i]; i = i + 1
        if self.speed < self.speed_range[0] or self.speed > self.speed_range[-1]:
            self.speed = random.choice(self.speed_range)
        self.chWd = v[i]; i = i + 1
        if self.chWd < self.chWd_range[0] or self.chWd > self.chWd_range[-1]:
            self.chWd = random.choice(self.chWd_range)       
        self.chHt = v[i]; i = i + 1
        if self.chHt < self.chHt_range[0] or self.chHt > self.chHt_range[-1]:
            self.chHt = random.choice(self.chHt_range)       
        self.wheel1Radius = v[i]; i = i + 1
        if self.wheel1Radius < self.wheel1Radius_range[0] or self.wheel1Radius > self.wheel1Radius_range[-1]:
            self.wheel1Radius = random.choice(self.wheel1Radius_range)       
        self.wheel2Radius = v[i]; i = i + 1
        if self.wheel2Radius < self.wheel2Radius_range[0] or self.wheel2Radius > self.wheel2Radius_range[-1]:
            self.wheel2Radius = random.choice(self.wheel2Radius_range)       
        self.chassisMass = v[i]; i = i + 1
        if self.chassisMass < self.chassisMass_range[0] or self.chassisMass > self.chassisMass_range[-1]:
            self.chassisMass = random.choice(self.chassisMass_range)       
        self.wheel1Mass = v[i]; i = i + 1
        if self.wheel1Mass < self.wheel1Mass_range[0] or self.wheel1Mass > self.wheel1Mass_range[-1]:
            self.wheel1Mass = random.choice(self.wheel1Mass_range)       
        self.wheel2Mass = v[i]; i = i + 1
        if self.wheel2Mass < self.wheel2Mass_range[0] or self.wheel2Mass > self.wheel2Mass_range[-1]:
            self.wheel2Mass = random.choice(self.wheel2Mass_range)       
   
    def createCar(self):
        chassisXY = Vec2d(self.x, self.y)
        self.fitness = 0
        moment = self.pymunk.moment_for_box(self.chassisMass, (self.chWd, self.chHt))
        self.chassis_b = self.pymunk.Body(self.chassisMass, moment)
        self.chassis_s = self.pymunk.Poly.create_box(self.chassis_b, (self.chWd, self.chHt))
        self.chassis_s.color = 10,150,40
        self.chassis_b.position = chassisXY + (0, 0)
        self.space.add(self.chassis_b, self.chassis_s)          
       
        #---wheel1 (left side wheel)
        #w1 = self.addChassis(chassisXY, -chWd, 0, mass, 5, 30) #special rectangular wheel       
        moment = self.pymunk.moment_for_circle(self.wheel1Mass, 0, self.wheel1Radius)
        self.wheel1_b = self.pymunk.Body(self.wheel1Mass, moment)
        self.wheel1_s = self.pymunk.Circle(self.wheel1_b, self.wheel1Radius)
        self.wheel1_s.friction = self.friction
        self.wheel1_s.color = 180,180,180
        self.wheel1_b.position = chassisXY - (self.chWd, 0)
        self.space.add(self.wheel1_b, self.wheel1_s)         

        #---wheel2 (right side wheel)
        #w1 = self.addChassis(chassisXY, -chWd, 0, mass, 5, 30) #special rectangular wheel       
        moment = self.pymunk.moment_for_circle(self.wheel2Mass, 0, self.wheel2Radius)
        self.wheel2_b = self.pymunk.Body(self.wheel2Mass, moment)
        self.wheel2_s = self.pymunk.Circle(self.wheel2_b, self.wheel2Radius)
        self.wheel2_s.friction = self.friction
        self.wheel2_s.color = 180,180,180
        self.wheel2_b.position = chassisXY - (-self.chWd, 0)
        self.space.add(self.wheel2_b, self.wheel2_s)        
       
        self.pin1 = self.pymunk.PinJoint(self.wheel1_b, self.chassis_b, (0,0), (-self.chWd/2,0))
        self.pin2 = self.pymunk.PinJoint(self.wheel2_b, self.chassis_b, (0,0), (self.chWd/2,0))
        self.pin3 = self.pymunk.PinJoint(self.wheel1_b, self.chassis_b, (0,0), (0,-self.chHt/2))
        self.pin4 = self.pymunk.PinJoint(self.wheel2_b, self.chassis_b, (0,0), (0,-self.chHt/2))
        self.space.add(self.pin1, self.pin2, self.pin3, self.pin4)
        self.motorJoint1 = self.pymunk.SimpleMotor(self.wheel1_b, self.chassis_b, self.speed); self.motorJoint1.max_force = 10000000
        self.motorJoint2 = self.pymunk.SimpleMotor(self.wheel2_b, self.chassis_b, self.speed); self.motorJoint2.max_force = 10000000
        self.space.add(self.motorJoint1, self.motorJoint2)
       
    def stop(self):
        self.motorJoint1.rate(0.0)
        self.motorJoint2.rate(0.0)
       
    def removeCar(self):
        self.space.remove(self.chassis_b)
        self.space.remove(self.chassis_s)
        self.space.remove(self.wheel1_b)
        self.space.remove(self.wheel1_s)
        self.space.remove(self.wheel2_b)
        self.space.remove(self.wheel2_s)
        self.space.remove(self.pin1)
        self.space.remove(self.pin2)
        self.space.remove(self.pin3)
        self.space.remove(self.pin4)
        self.space.remove(self.motorJoint1)
        self.space.remove(self.motorJoint2)
           

fps = 60.0 #frames per second
worldWidth = 1200
worldHeight = 300
#---initializations and setup
pygame.init()
screen = pygame.display.set_mode((worldWidth, worldHeight))
clock = pygame.time.Clock()
space = pymunk.Space()
space.gravity = 0, 900
space.sleep_time_threshold = 1 #The time a group of bodies must remain idle in order to fall asleep
draw_options = pymunk.pygame_util.DrawOptions(screen)
pymunk.pygame_util.positive_y_is_up = False

#---setup line segments in space
x = 10; y = 200; thickness = 2; friction = 1.0
points = [(-100,0),(100,0),(112,12),(125,0),(137,12),(150,0),(162,12),(175,0),(200,0),(250,25),(250,0),(315,0),(320,70),(375,70),(385,0),(425,0),(425,12),(448,12),(448,0),(475,0),(475,12),(485,12),(485,0),(512,0),(512,12),(527,12),(527,0),(590,0),(590,10),(610,10),(610,15),(625,15),(625,25),(646,25),(646,35),(660,35),(660,45),(680,48),(710,-10),(755,5),(755,20),(780,25),(850,25),(825,-20),(865,-20),(895,25),(925,25),(935,0),(980,-75),(1100,-75)];
for i in range(1, len(points)):           
    floor = pymunk.Segment(space.static_body, (x+points[i-1][0], y+points[i-1][1]), (x+points[i][0], y+points[i][1]), thickness)
    floor.friction = friction
    space.add(floor)
#---create cars
xStartPos = 20; yStartPos = 0; numCars = 10; cars = []
for i in range(0, numCars):
    cars.append(Car(space, pymunk, xStartPos, yStartPos))
    cars[i].createCar()

while True:
    for event in pygame.event.get():
        if event.type == QUIT or event.type == KEYDOWN and (event.key in [K_ESCAPE, K_q]): 
            sys.exit(0)

    #---updations
    space.step(1./fps)#Update the space for the given time step.
    screen.fill(pygame.color.THECOLORS["black"])#clear screen
    space.debug_draw(draw_options)#try using Pygame's drawing functions instead
    pygame.display.flip()#flips the draw buffer to display the objects in current frame
    dt = clock.tick(fps)#creates a delay


You'll notice that the cars collide with each other and the wheels of the cars collide with the car's own body too. There's a way to avoid that using ShapeFilters. The shape filter helps categorize objects into "player", "enemy" etc. to help decide which objects should collide and which shouldn't. This is very helpful because if the parts of the body of players collide, it produces very jittery motion.


Cars with ShapeFilters and text display:


import sys
import time
import pygame
from pygame.locals import * #for key codes etc
from pygame.color import *
import pymunk
import pymunk.pygame_util

from pymunk import Vec2d
import random

class Car:
    space = None
    physics = None
    x = 0
    y = 0
    friction = 1.5  
    #---range of values possible for properties of car
    speed_range = range(10, 30, 5)#15
    chWd_range = range(5, 70, 5)#50#chassis width
    chHt_range = range(5, 50, 5)#10#chassis height
    wheel1Radius_range = range(6, 25, 5)#15
    wheel2Radius_range = range(6, 25, 5)#15
    chassisMass_range = range(10, 500, 50)#100
    wheel1Mass_range = range(10, 500, 50)#100
    wheel2Mass_range = range(10, 500, 50)#100 
    #---properties of car   
    speed = 0
    chWd = 0#chassis width
    chHt = 0#chassis height
    wheel1Radius = 0
    wheel2Radius = 0
    chassisMass = 0
    wheel1Mass = 0
    wheel2Mass = 0 
    #---parts to add or remove from space
    chassis_b = None
    chassis_s = None
    wheel1_b = None
    wheel1_s = None
    wheel2_b = None
    wheel2_s = None 
    pin1 = None
    pin2 = None
    pin3 = None
    pin4 = None
    motorJoint1 = None
    motorJoint2 = None
   
    def __init__(self, space, pymunk, xStartPos, yStartPos):
        self.space = space
        self.pymunk = pymunk
        self.x = xStartPos
        self.y = yStartPos
        self.reinitializeWithRandomValues()   
       
    def reinitializeWithRandomValues(self):
        self.speed = random.choice(self.speed_range)
        self.chWd = random.choice(self.chWd_range)
        self.chHt = random.choice(self.chHt_range)
        self.wheel1Radius = random.choice(self.wheel1Radius_range)
        self.wheel2Radius = random.choice(self.wheel2Radius_range)
        self.chassisMass = random.choice(self.chassisMass_range)
        self.wheel1Mass = random.choice(self.wheel1Mass_range)
        self.wheel2Mass = random.choice(self.wheel2Mass_range)    
       
    def setValues(self, v):
        i = 0
        self.speed = v[i]; i = i + 1
        if self.speed < self.speed_range[0] or self.speed > self.speed_range[-1]:
            self.speed = random.choice(self.speed_range)
        self.chWd = v[i]; i = i + 1
        if self.chWd < self.chWd_range[0] or self.chWd > self.chWd_range[-1]:
            self.chWd = random.choice(self.chWd_range)       
        self.chHt = v[i]; i = i + 1
        if self.chHt < self.chHt_range[0] or self.chHt > self.chHt_range[-1]:
            self.chHt = random.choice(self.chHt_range)       
        self.wheel1Radius = v[i]; i = i + 1
        if self.wheel1Radius < self.wheel1Radius_range[0] or self.wheel1Radius > self.wheel1Radius_range[-1]:
            self.wheel1Radius = random.choice(self.wheel1Radius_range)       
        self.wheel2Radius = v[i]; i = i + 1
        if self.wheel2Radius < self.wheel2Radius_range[0] or self.wheel2Radius > self.wheel2Radius_range[-1]:
            self.wheel2Radius = random.choice(self.wheel2Radius_range)       
        self.chassisMass = v[i]; i = i + 1
        if self.chassisMass < self.chassisMass_range[0] or self.chassisMass > self.chassisMass_range[-1]:
            self.chassisMass = random.choice(self.chassisMass_range)       
        self.wheel1Mass = v[i]; i = i + 1
        if self.wheel1Mass < self.wheel1Mass_range[0] or self.wheel1Mass > self.wheel1Mass_range[-1]:
            self.wheel1Mass = random.choice(self.wheel1Mass_range)       
        self.wheel2Mass = v[i]; i = i + 1
        if self.wheel2Mass < self.wheel2Mass_range[0] or self.wheel2Mass > self.wheel2Mass_range[-1]:
            self.wheel2Mass = random.choice(self.wheel2Mass_range)       
   
    def createCar(self, shapeFilter):
        chassisXY = Vec2d(self.x, self.y)
        self.fitness = 0
        moment = self.pymunk.moment_for_box(self.chassisMass, (self.chWd, self.chHt))
        self.chassis_b = self.pymunk.Body(self.chassisMass, moment)
        self.chassis_s = self.pymunk.Poly.create_box(self.chassis_b, (self.chWd, self.chHt))
        self.chassis_s.color = 10,150,40
        self.chassis_b.position = chassisXY + (0, 0)
        self.chassis_s.filter = shapeFilter
        self.space.add(self.chassis_b, self.chassis_s)          
       
        #---wheel1 (left side wheel)      
        moment = self.pymunk.moment_for_circle(self.wheel1Mass, 0, self.wheel1Radius)
        self.wheel1_b = self.pymunk.Body(self.wheel1Mass, moment)
        self.wheel1_s = self.pymunk.Circle(self.wheel1_b, self.wheel1Radius)
        self.wheel1_s.friction = self.friction
        self.wheel1_s.color = 180,180,180
        self.wheel1_b.position = chassisXY - (self.chWd, 0)
        self.wheel1_s.filter = shapeFilter
        self.space.add(self.wheel1_b, self.wheel1_s)         

        #---wheel2 (right side wheel)               
        moment = self.pymunk.moment_for_circle(self.wheel2Mass, 0, self.wheel2Radius)
        self.wheel2_b = self.pymunk.Body(self.wheel2Mass, moment)
        self.wheel2_s = self.pymunk.Circle(self.wheel2_b, self.wheel2Radius)
        self.wheel2_s.friction = self.friction
        self.wheel2_s.color = 180,180,180
        self.wheel2_b.position = chassisXY - (-self.chWd, 0)
        self.wheel2_s.filter = shapeFilter
        self.space.add(self.wheel2_b, self.wheel2_s)        
       
        self.pin1 = self.pymunk.PinJoint(self.wheel1_b, self.chassis_b, (0,0), (-self.chWd/2,0))
        self.pin2 = self.pymunk.PinJoint(self.wheel2_b, self.chassis_b, (0,0), (self.chWd/2,0))
        self.pin3 = self.pymunk.PinJoint(self.wheel1_b, self.chassis_b, (0,0), (0,-self.chHt/2))
        self.pin4 = self.pymunk.PinJoint(self.wheel2_b, self.chassis_b, (0,0), (0,-self.chHt/2))
        self.space.add(self.pin1, self.pin2, self.pin3, self.pin4)
        self.motorJoint1 = self.pymunk.SimpleMotor(self.wheel1_b, self.chassis_b, self.speed); self.motorJoint1.max_force = 10000000
        self.motorJoint2 = self.pymunk.SimpleMotor(self.wheel2_b, self.chassis_b, self.speed); self.motorJoint2.max_force = 10000000
        self.space.add(self.motorJoint1, self.motorJoint2)
       
    def stop(self):
        self.motorJoint1.rate(0.0)
        self.motorJoint2.rate(0.0)
       
    def removeCar(self):
        self.space.remove(self.chassis_b)
        self.space.remove(self.chassis_s)
        self.space.remove(self.wheel1_b)
        self.space.remove(self.wheel1_s)
        self.space.remove(self.wheel2_b)
        self.space.remove(self.wheel2_s)
        self.space.remove(self.pin1)
        self.space.remove(self.pin2)
        self.space.remove(self.pin3)
        self.space.remove(self.pin4)
        self.space.remove(self.motorJoint1)
        self.space.remove(self.motorJoint2)
           

fps = 60.0 #frames per second
worldWidth = 1200
worldHeight = 300
#---initializations and setup
pygame.init()
screen = pygame.display.set_mode((worldWidth, worldHeight))
clock = pygame.time.Clock()
space = pymunk.Space()
space.gravity = 0, 900
space.sleep_time_threshold = 1 #The time a group of bodies must remain idle in order to fall asleep
draw_options = pymunk.pygame_util.DrawOptions(screen)
pymunk.pygame_util.positive_y_is_up = False
font = pygame.font.SysFont("Arial", 20)

#---setup line segments in space
x = 10; y = 200; thickness = 2; friction = 1.0
points = [(-100,0),(100,0),(112,12),(125,0),(137,12),(150,0),(162,12),(175,0),(200,0),(250,25),(250,0),(315,0),(320,70),(375,70),(385,0),(425,0),(425,12),(448,12),(448,0),(475,0),(475,12),(485,12),(485,0),(512,0),(512,12),(527,12),(527,0),(590,0),(590,10),(610,10),(610,15),(625,15),(625,25),(646,25),(646,35),(660,35),(660,45),(680,48),(710,-10),(755,5),(755,20),(780,25),(850,25),(825,-20),(865,-20),(895,25),(925,25),(935,0),(980,-75),(1100,-75)];
for i in range(1, len(points)):           
    floor = pymunk.Segment(space.static_body, (x+points[i-1][0], y+points[i-1][1]), (x+points[i][0], y+points[i][1]), thickness)
    floor.friction = friction
    space.add(floor)
#---create cars
xStartPos = 20; yStartPos = 0; numCars = 10; cars = []
sf = pymunk.ShapeFilter(group=1)
for i in range(0, numCars):
    cars.append(Car(space, pymunk, xStartPos, yStartPos))
    cars[i].createCar(sf)

while True:
    for event in pygame.event.get():
        if event.type == QUIT or event.type == KEYDOWN and (event.key in [K_ESCAPE, K_q]): 
            sys.exit(0)

    #---updations
    space.step(1./fps)#Update the space for the given time step.
    screen.fill(pygame.color.THECOLORS["black"])#clear screen
    space.debug_draw(draw_options)#try using Pygame's drawing functions instead
    screen.blit(font.render("Cars: "+str(numCars)+". Time: "+str(time.time()), 1, THECOLORS["darkgrey"]), (5, 5))
    pygame.display.flip()#flips the draw buffer to display the objects in current frame
    dt = clock.tick(fps)#creates a delay

Hope this gives you an easy-to-understand intro to the awesomeness of Pymunk. I've created a slightly more advanced version of the above code, where the cars try to reach the end of the terrain using learning via an evolutionary algorithm.
I'm not ready to share the code yet, but when I am, I'll be posting the link on this page.

Here's the demo on YouTube:


Few other things:
  • If you are looking to create something like a walking robot, there's an example here
  • I'm relatively new to Python, so forgive me if my code is not "Pythonic" enough :-)
  • You can also look through the examples and test cases in Pymunk to see more examples of how to use various functions.

Say thank you or donate

No comments: