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