Pymunk Physics Engine - Joint Builder#

This uses the Pymunk physics engine to simulate items with joints

Screenshot of pegboard example.
pymunk_joint_builder.py#
  1"""
  2Pymunk 2
  3
  4If Python and Arcade are installed, this example can be run from the command line with:
  5python -m arcade.examples.pymunk_joint_builder
  6"""
  7from __future__ import annotations
  8
  9import arcade
 10import pymunk
 11import timeit
 12import math
 13
 14SCREEN_WIDTH = 1200
 15SCREEN_HEIGHT = 800
 16SCREEN_TITLE = "Pymunk 2 Example"
 17
 18"""
 19Key bindings:
 20
 211 - Drag mode
 222 - Make box mode
 233 - Make PinJoint mode
 244 - Make DampedSpring mode
 25
 26S - No gravity or friction
 27L - Layout, no gravity, lots of friction
 28G - Gravity, little bit of friction
 29
 30Right-click, fire coin
 31
 32"""
 33
 34
 35class PhysicsSprite(arcade.Sprite):
 36    def __init__(self, pymunk_shape, filename):
 37        super().__init__(filename, center_x=pymunk_shape.body.position.x, center_y=pymunk_shape.body.position.y)
 38        self.pymunk_shape = pymunk_shape
 39
 40
 41class CircleSprite(PhysicsSprite):
 42    def __init__(self, pymunk_shape, filename):
 43        super().__init__(pymunk_shape, filename)
 44        self.width = pymunk_shape.radius * 4
 45        self.height = pymunk_shape.radius * 4
 46
 47
 48class BoxSprite(PhysicsSprite):
 49    def __init__(self, pymunk_shape, filename, width, height):
 50        super().__init__(pymunk_shape, filename)
 51        self.width = width
 52        self.height = height
 53
 54
 55class MyApplication(arcade.Window):
 56    """ Main application class. """
 57
 58    def __init__(self, width, height, title):
 59        super().__init__(width, height, title)
 60
 61        self.background_color = arcade.color.DARK_SLATE_GRAY
 62
 63        # -- Pymunk
 64        self.space = pymunk.Space()
 65        self.space.gravity = (0.0, -900.0)
 66
 67        # Lists of sprites or lines
 68        self.sprite_list: arcade.SpriteList[PhysicsSprite] = arcade.SpriteList()
 69        self.static_lines = []
 70
 71        # Used for dragging shapes around with the mouse
 72        self.shape_being_dragged = None
 73        self.last_mouse_position = 0, 0
 74
 75        self.processing_time_text = None
 76        self.draw_time_text = None
 77        self.draw_mode_text = None
 78        self.shape_1 = None
 79        self.shape_2 = None
 80        self.draw_time = 0
 81        self.processing_time = 0
 82        self.joints = []
 83
 84        self.physics = "Normal"
 85        self.mode = "Make Box"
 86
 87        # Create the floor
 88        self.floor_height = 80
 89        body = pymunk.Body(body_type=pymunk.Body.STATIC)
 90        shape = pymunk.Segment(body, (0, self.floor_height), (SCREEN_WIDTH, self.floor_height), 0.0)
 91        shape.friction = 10
 92        self.space.add(shape, body)
 93        self.static_lines.append(shape)
 94
 95    def on_draw(self):
 96        """
 97        Render the screen.
 98        """
 99
100        # This command has to happen before we start drawing
101        self.clear()
102
103        # Start timing how long this takes
104        draw_start_time = timeit.default_timer()
105
106        # Draw all the sprites
107        self.sprite_list.draw()
108
109        # Draw the lines that aren't sprites
110        for line in self.static_lines:
111            body = line.body
112
113            pv1 = body.position + line.a.rotated(body.angle)
114            pv2 = body.position + line.b.rotated(body.angle)
115            arcade.draw_line(pv1.x, pv1.y, pv2.x, pv2.y, arcade.color.WHITE, 2)
116
117        for joint in self.joints:
118            color = arcade.color.WHITE
119            if isinstance(joint, pymunk.DampedSpring):
120                color = arcade.color.DARK_GREEN
121            arcade.draw_line(joint.a.position.x,
122                             joint.a.position.y,
123                             joint.b.position.x,
124                             joint.b.position.y,
125                             color, 3)
126
127        # arcade.draw_text(output, 10, 20, arcade.color.WHITE, 14)
128        # Display timings
129        output = f"Processing time: {self.processing_time:.3f}"
130        arcade.draw_text(output, 20, SCREEN_HEIGHT - 20, arcade.color.WHITE)
131
132        output = f"Drawing time: {self.draw_time:.3f}"
133        arcade.draw_text(output, 20, SCREEN_HEIGHT - 40, arcade.color.WHITE)
134
135        self.draw_time = timeit.default_timer() - draw_start_time
136
137        output = f"Mode: {self.mode}"
138        arcade.draw_text(output, 20, SCREEN_HEIGHT - 60, arcade.color.WHITE)
139
140        output = f"Physics: {self.physics}"
141        arcade.draw_text(output, 20, SCREEN_HEIGHT - 80, arcade.color.WHITE)
142
143    def make_box(self, x, y):
144        size = 45
145        mass = 12.0
146        moment = pymunk.moment_for_box(mass, (size, size))
147        body = pymunk.Body(mass, moment)
148        body.position = pymunk.Vec2d(x, y)
149        shape = pymunk.Poly.create_box(body, (size, size))
150        shape.friction = 0.3
151        self.space.add(body, shape)
152
153        sprite = BoxSprite(shape, ":resources:images/tiles/boxCrate_double.png", width=size, height=size)
154        self.sprite_list.append(sprite)
155
156    def make_circle(self, x, y):
157        size = 20
158        mass = 12.0
159        moment = pymunk.moment_for_circle(mass, 0, size, (0, 0))
160        body = pymunk.Body(mass, moment)
161        body.position = pymunk.Vec2d(x, y)
162        shape = pymunk.Circle(body, size, pymunk.Vec2d(0, 0))
163        shape.friction = 0.3
164        self.space.add(body, shape)
165
166        sprite = CircleSprite(shape, ":resources:images/items/coinGold.png")
167        self.sprite_list.append(sprite)
168
169    def make_pin_joint(self, x, y):
170        shape_selected = self.get_shape(x, y)
171        if shape_selected is None:
172            return
173
174        if self.shape_1 is None:
175            print("Shape 1 Selected")
176            self.shape_1 = shape_selected
177        elif self.shape_2 is None:
178            print("Shape 2 Selected")
179            self.shape_2 = shape_selected
180            joint = pymunk.PinJoint(self.shape_1.shape.body, self.shape_2.shape.body)
181            self.space.add(joint)
182            self.joints.append(joint)
183            self.shape_1 = None
184            self.shape_2 = None
185            print("Joint Made")
186
187    def make_damped_spring(self, x, y):
188        shape_selected = self.get_shape(x, y)
189        if shape_selected is None:
190            return
191
192        if self.shape_1 is None:
193            print("Shape 1 Selected")
194            self.shape_1 = shape_selected
195        elif self.shape_2 is None:
196            print("Shape 2 Selected")
197            self.shape_2 = shape_selected
198            joint = pymunk.DampedSpring(self.shape_1.shape.body, self.shape_2.shape.body, (0, 0), (0, 0), 45, 300, 30)
199            self.space.add(joint)
200            self.joints.append(joint)
201            self.shape_1 = None
202            self.shape_2 = None
203            print("Joint Made")
204
205    def get_shape(self, x, y):
206        # See if we clicked on anything
207        shape_list = self.space.point_query((x, y), 1, pymunk.ShapeFilter())
208
209        # If we did, remember what we clicked on
210        if len(shape_list) > 0:
211            shape = shape_list[0]
212        else:
213            shape = None
214
215        return shape
216
217    def on_mouse_press(self, x, y, button, modifiers):
218
219        if button == 1 and self.mode == "Drag":
220            self.last_mouse_position = x, y
221            self.shape_being_dragged = self.get_shape(x, y)
222
223        elif button == 1 and self.mode == "Make Box":
224            self.make_box(x, y)
225
226        elif button == 1 and self.mode == "Make Circle":
227            self.make_circle(x, y)
228
229        elif button == 1 and self.mode == "Make PinJoint":
230            self.make_pin_joint(x, y)
231
232        elif button == 1 and self.mode == "Make DampedSpring":
233            self.make_damped_spring(x, y)
234
235        elif button == 4:
236            # With right mouse button, shoot a heavy coin fast.
237            mass = 60
238            radius = 10
239            inertia = pymunk.moment_for_circle(mass, 0, radius, (0, 0))
240            body = pymunk.Body(mass, inertia)
241            body.position = x, y
242            body.velocity = 2000, 0
243            shape = pymunk.Circle(body, radius, pymunk.Vec2d(0, 0))
244            shape.friction = 0.3
245            self.space.add(body, shape)
246
247            sprite = CircleSprite(shape, ":resources:images/items/coinGold.png")
248            self.sprite_list.append(sprite)
249
250    def on_mouse_release(self, x, y, button, modifiers):
251        if button == 1:
252            # Release the item we are holding (if any)
253            self.shape_being_dragged = None
254
255    def on_mouse_motion(self, x, y, dx, dy):
256        if self.shape_being_dragged is not None:
257            # If we are holding an object, move it with the mouse
258            self.last_mouse_position = x, y
259            self.shape_being_dragged.shape.body.position = self.last_mouse_position
260            self.shape_being_dragged.shape.body.velocity = dx * 20, dy * 20
261
262    def on_key_press(self, symbol: int, modifiers: int):
263        if symbol == arcade.key.KEY_1:
264            self.mode = "Drag"
265        elif symbol == arcade.key.KEY_2:
266            self.mode = "Make Box"
267        elif symbol == arcade.key.KEY_3:
268            self.mode = "Make Circle"
269
270        elif symbol == arcade.key.KEY_4:
271            self.mode = "Make PinJoint"
272        elif symbol == arcade.key.KEY_5:
273            self.mode = "Make DampedSpring"
274
275        elif symbol == arcade.key.S:
276            self.space.gravity = (0.0, 0.0)
277            self.space.damping = 1
278            self.physics = "Outer Space"
279        elif symbol == arcade.key.L:
280            self.space.gravity = (0.0, 0.0)
281            self.space.damping = 0
282            self.physics = "Layout"
283        elif symbol == arcade.key.G:
284            self.space.damping = 0.95
285            self.space.gravity = (0.0, -900.0)
286            self.physics = "Normal"
287
288    def on_update(self, delta_time):
289        start_time = timeit.default_timer()
290
291        # Check for balls that fall off the screen
292        for sprite in self.sprite_list:
293            if sprite.pymunk_shape.body.position.y < 0:
294                # Remove balls from physics space
295                self.space.remove(sprite.pymunk_shape, sprite.pymunk_shape.body)
296                # Remove balls from physics list
297                sprite.kill()
298
299        # Update physics
300        self.space.step(1 / 80.0)
301
302        # If we are dragging an object, make sure it stays with the mouse. Otherwise
303        # gravity will drag it down.
304        if self.shape_being_dragged is not None:
305            self.shape_being_dragged.shape.body.position = self.last_mouse_position
306            self.shape_being_dragged.shape.body.velocity = 0, 0
307
308        # Move sprites to where physics objects are
309        for sprite in self.sprite_list:
310            sprite.center_x = sprite.pymunk_shape.body.position.x
311            sprite.center_y = sprite.pymunk_shape.body.position.y
312            # Reverse angle because pymunk rotates ccw
313            sprite.angle = -math.degrees(sprite.pymunk_shape.body.angle)
314
315        # Save the time it took to do this.
316        self.processing_time = timeit.default_timer() - start_time
317
318
319window = MyApplication(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
320
321arcade.run()