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.
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()