Pymunk Physics Engine - Stacks of Boxes#

This uses the Pymunk physics engine to manage stacks of boxes. The user can interact with the boxes using the mouse.

Screen shot of stacks of boxes.
pymunk_box_stacks.py#
  1"""
  2Use Pymunk physics engine.
  3
  4For more info on Pymunk see:
  5https://www.pymunk.org/en/latest/
  6
  7To install pymunk:
  8pip install pymunk
  9
 10Artwork from https://kenney.nl
 11
 12If Python and Arcade are installed, this example can be run from the command line with:
 13python -m arcade.examples.pymunk_box_stacks
 14
 15Click and drag with the mouse to move the boxes.
 16"""
 17
 18import arcade
 19import pymunk
 20import timeit
 21import math
 22
 23SCREEN_WIDTH = 1800
 24SCREEN_HEIGHT = 800
 25SCREEN_TITLE = "Pymunk test"
 26
 27
 28class PhysicsSprite(arcade.Sprite):
 29    def __init__(self, pymunk_shape, filename):
 30        super().__init__(filename, center_x=pymunk_shape.body.position.x, center_y=pymunk_shape.body.position.y)
 31        self.pymunk_shape = pymunk_shape
 32
 33
 34class CircleSprite(PhysicsSprite):
 35    def __init__(self, pymunk_shape, filename):
 36        super().__init__(pymunk_shape, filename)
 37        self.width = pymunk_shape.radius * 2
 38        self.height = pymunk_shape.radius * 2
 39
 40
 41class BoxSprite(PhysicsSprite):
 42    def __init__(self, pymunk_shape, filename, width, height):
 43        super().__init__(pymunk_shape, filename)
 44        self.width = width
 45        self.height = height
 46
 47
 48class MyGame(arcade.Window):
 49    """ Main application class. """
 50
 51    def __init__(self, width, height, title):
 52        super().__init__(width, height, title)
 53
 54        self.background_color = arcade.color.DARK_SLATE_GRAY
 55
 56        # -- Pymunk
 57        self.space = pymunk.Space()
 58        self.space.iterations = 35
 59        self.space.gravity = (0.0, -900.0)
 60
 61        # Lists of sprites or lines
 62        self.sprite_list: arcade.SpriteList[PhysicsSprite] = arcade.SpriteList()
 63        self.static_lines = []
 64
 65        # Used for dragging shapes around with the mouse
 66        self.shape_being_dragged = None
 67        self.last_mouse_position = 0, 0
 68
 69        self.draw_time = 0
 70        self.processing_time = 0
 71
 72        # Create the floor
 73        floor_height = 80
 74        body = pymunk.Body(body_type=pymunk.Body.STATIC)
 75        shape = pymunk.Segment(body, [0, floor_height], [SCREEN_WIDTH, floor_height], 0.0)
 76        shape.friction = 10
 77        self.space.add(shape, body)
 78        self.static_lines.append(shape)
 79
 80        # Create the stacks of boxes
 81        for row in range(10):
 82            for column in range(10):
 83                size = 32
 84                mass = 1.0
 85                x = 500 + column * 32
 86                y = (floor_height + size / 2) + row * size
 87                moment = pymunk.moment_for_box(mass, (size, size))
 88                body = pymunk.Body(mass, moment)
 89                body.position = pymunk.Vec2d(x, y)
 90                shape = pymunk.Poly.create_box(body, (size, size))
 91                shape.elasticity = 0.2
 92                shape.friction = 0.9
 93                self.space.add(body, shape)
 94                # body.sleep()
 95
 96                sprite = BoxSprite(shape, ":resources:images/tiles/boxCrate_double.png", width=size, height=size)
 97                self.sprite_list.append(sprite)
 98
 99    def on_draw(self):
100        """
101        Render the screen.
102        """
103
104        # This command has to happen before we start drawing
105        self.clear()
106
107        # Start timing how long this takes
108        draw_start_time = timeit.default_timer()
109
110        # Draw all the sprites
111        self.sprite_list.draw()
112
113        # Draw the lines that aren't sprites
114        for line in self.static_lines:
115            body = line.body
116
117            pv1 = body.position + line.a.rotated(body.angle)
118            pv2 = body.position + line.b.rotated(body.angle)
119            arcade.draw_line(pv1.x, pv1.y, pv2.x, pv2.y, arcade.color.WHITE, 2)
120
121        # Display timings
122        output = f"Processing time: {self.processing_time:.3f}"
123        arcade.draw_text(output, 20, SCREEN_HEIGHT - 20, arcade.color.WHITE, 12)
124
125        output = f"Drawing time: {self.draw_time:.3f}"
126        arcade.draw_text(output, 20, SCREEN_HEIGHT - 40, arcade.color.WHITE, 12)
127
128        self.draw_time = timeit.default_timer() - draw_start_time
129
130    def on_mouse_press(self, x, y, button, modifiers):
131        if button == arcade.MOUSE_BUTTON_LEFT:
132            self.last_mouse_position = x, y
133            # See if we clicked on anything
134            shape_list = self.space.point_query((x, y), 1, pymunk.ShapeFilter())
135
136            # If we did, remember what we clicked on
137            if len(shape_list) > 0:
138                self.shape_being_dragged = shape_list[0]
139
140        elif button == arcade.MOUSE_BUTTON_RIGHT:
141            # With right mouse button, shoot a heavy coin fast.
142            mass = 60
143            radius = 10
144            inertia = pymunk.moment_for_circle(mass, 0, radius, (0, 0))
145            body = pymunk.Body(mass, inertia)
146            body.position = x, y
147            body.velocity = 2000, 0
148            shape = pymunk.Circle(body, radius, pymunk.Vec2d(0, 0))
149            shape.friction = 0.3
150            self.space.add(body, shape)
151
152            sprite = CircleSprite(shape, ":resources:images/items/coinGold.png")
153            self.sprite_list.append(sprite)
154
155    def on_mouse_release(self, x, y, button, modifiers):
156        if button == arcade.MOUSE_BUTTON_LEFT:
157            # Release the item we are holding (if any)
158            self.shape_being_dragged = None
159
160    def on_mouse_motion(self, x, y, dx, dy):
161        if self.shape_being_dragged is not None:
162            # If we are holding an object, move it with the mouse
163            self.last_mouse_position = x, y
164            self.shape_being_dragged.shape.body.position = self.last_mouse_position
165            self.shape_being_dragged.shape.body.velocity = dx * 20, dy * 20
166
167    def on_update(self, delta_time):
168        start_time = timeit.default_timer()
169
170        # Check for balls that fall off the screen
171        for sprite in self.sprite_list:
172            if sprite.pymunk_shape.body.position.y < 0:
173                # Remove balls from physics space
174                self.space.remove(sprite.pymunk_shape, sprite.pymunk_shape.body)
175                # Remove balls from physics list
176                sprite.remove_from_sprite_lists()
177
178        # Update physics
179        # Use a constant time step, don't use delta_time
180        # See "Game loop / moving time forward"
181        # https://www.pymunk.org/en/latest/overview.html#game-loop-moving-time-forward
182        self.space.step(1 / 60.0)
183
184        # If we are dragging an object, make sure it stays with the mouse. Otherwise
185        # gravity will drag it down.
186        if self.shape_being_dragged is not None:
187            self.shape_being_dragged.shape.body.position = self.last_mouse_position
188            self.shape_being_dragged.shape.body.velocity = 0, 0
189
190        # Move sprites to where physics objects are
191        for sprite in self.sprite_list:
192            sprite.center_x = sprite.pymunk_shape.body.position.x
193            sprite.center_y = sprite.pymunk_shape.body.position.y
194            sprite.angle = -math.degrees(sprite.pymunk_shape.body.angle)
195
196        # Save the time it took to do this.
197        self.processing_time = timeit.default_timer() - start_time
198
199
200def main():
201    MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
202
203    arcade.run()
204
205
206if __name__ == "__main__":
207    main()