Pymunk Physics Engine - Joint Builder#
This uses the Pymunk physics engine to simulate items with joints
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()