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()