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