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