Create an app

In this section we are looking how to make an interactive application using Pymunk.

class App:
    """Create a single-window app with multiple spaces (scenes)."""
    spaces = []
    current = None
    size = 640, 240

    def __init__(self):
        """Initialize pygame and the app."""
        pygame.init()
        self.screen = pygame.display.set_mode(App.size)
        self.running = True
        self.stepping = True

        self.rect = Rect((0, 0), App.size)
        self.draw_options = pymunk.pygame_util.DrawOptions(self.screen)
        self.dt = 1/50

        self.shortcuts = {
            K_a: 'Arrow(get_mouse_pos(self.screen), color=BLACK)',
            K_b: 'Rectangle(get_mouse_pos(self.screen), color=GREEN)',
            K_v: 'Rectangle(get_mouse_pos(self.screen), color=BLUE)',
            
            K_c: 'Circle(get_mouse_pos(self.screen), color=RED)',
            K_n: 'self.next_space()',
            
            K_q: 'self.running = False',
            K_ESCAPE: 'self.running = False',
            K_SPACE: 'self.stepping = not self.stepping',

            K_1: 'self.draw_options.flags ^= 1',
            K_2: 'self.draw_options.flags ^= 2',
            K_3: 'self.draw_options.flags ^= 4',

            K_p: 'self.capture()',
            K_s: 'App.current.space.step(self.dt)',
            K_z: 'App.current.remove_all()',
            K_g: 'App.current.space.gravity = 0, 0',
        }
        
    def run(self):
        """Run the main event loop."""
        while self.running:
            for event in pygame.event.get():
                if event.type == QUIT:
                    self.running = False
                
                elif event.type == KEYDOWN:
                    self.do_shortcut(event)

                App.current.do_event(event)

            for s in App.current.space.shapes:
                if s.body.position.y < -100:
                    App.current.space.remove(s)

            self.draw()

            if self.stepping:
                App.current.space.step(self.dt)

        pygame.quit()

    def draw(self):
            self.screen.fill(App.current.color)
            
            for obj in App.current.objects:
                obj.draw()

            App.current.space.debug_draw(self.draw_options)
            self.draw_cg()
            App.current.draw()

            rect = App.current.sel_rect
            pygame.draw.rect(self.screen, GREEN, rect, 1)
            pygame.display.update()


    def draw_cg(self):
        """Draw the center of gravity."""
        screen = pygame.display.get_surface()
        for b in App.current.space.bodies:
            cg = b.position + b.center_of_gravity
            p = to_pygame(cg, screen)
            pygame.draw.circle(screen, BLUE, p, 5, 1)


    def do_shortcut(self, event):
        """Find the key/mod combination and execute the cmd."""
        k = event.key
        m = event.mod
        cmd = ''
        if k in self.shortcuts:
            cmd = self.shortcuts[k]
        elif (k, m) in self.shortcuts:
            cmd = self.shortcuts[k, m]
        if cmd != '':
            try:
                exec(cmd)
            except:
                print(f'cmd error: <{cmd}>')

    def next_space(self):
        d = 1
        if pygame.key.get_mods() & KMOD_SHIFT:
            d = -1
        n = len(App.spaces)
        i = App.spaces.index(App.current)
        i = (i+d) % n
        App.current = App.spaces[i]
        pygame.display.set_caption(App.current.caption)

        for s in App.current.space.shapes:
            print(s, s.bb)
    

    def draw_positions(self):
        for body in App.current.space.bodies:
            print(body.mass)

    def capture(self):
        """Save a screen capture to the directory of the calling class"""
        name = type(self).__name__
        module = sys.modules['__main__']
        path, name = os.path.split(module.__file__)
        name, ext = os.path.splitext(name)
        filename = path + '/' + name + '.png'
        pygame.image.save(self.screen, filename)

Circle

The Circle class creates a body with an attached circle shape.

class Circle:
    def __init__(self, p0, radius=10, color=None):
        self.body = pymunk.Body()
        self.body.position = p0
        shape = pymunk.Circle(self.body, radius)
        shape.density = 0.01
        shape.elasticity = 0.5
        shape.friction = 0.5
        if color != None:
            shape.color = color
        App.current.space.add(self.body, shape)

This is an exemple of three circles placed in a no-gravity space:

p0 = Vec2d(200, 120)
v = Vec2d(100, 0)

Space('Cercle', GRAY, gravity=(0, 0))
Circle(p0)
Circle(p0+v, 20)
Circle(p0+2*v, 50, RED)
../_images/app1.png

app.py

Segment

The Segment class creates a linear segment starting at position p0 having a direction vector v, a radius and a color.

class Segment:
    def __init__(self, p0, v, radius=10, color=None):
        self.body = pymunk.Body()
        self.body.position = p0
        shape = pymunk.Segment(self.body, (0, 0), v, radius)
        shape.density = 0.01
        shape.elasticity = 0.5
        shape.friction = 0.5
        if color != None:
            shape.color = color
        App.current.space.add(self.body, shape)

This is an example of two segments of different radius, length and color:

Space('Segment', gravity=(0, 0))
Segment(p0, v)
Segment(p0+(50, 50), 2*v, 5, RED)
../_images/app2.png

Poly

The Poly class creates a filled polygon placed at position p0 with the vertices v given with a vertex list.

class Poly:
    def __init__(self, p0, vertices, color=None):
        self.body = pymunk.Body()
        self.body.position = p0
        self.shape = pymunk.Poly(self.body, vertices)
        self.shape.density = 0.01
        self.shape.elasticity = 0.5
        self.shape.friction = 0.5
        if color != None:
            self.shape.color = color
        App.current.space.add(self.body, self.shape)

This is an example of creating a triangle and a square polygon:

Space('Poly', gravity=(0, 0))
triangle = [(-30, -30), (30, -30), (0, 30)]
Poly(p0, triangle)
square = [(-30, -30), (30, -30), (30, 30), (-30, 30)]
Poly(p0+v, square)
../_images/app3.png