Pymunk Physics Engine - Pegboard#
This uses the Pymunk physics engine to simulate balls falling on a pegboard.
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_pegboard
14
15Click and drag with the mouse to move the boxes.
16"""
17
18from __future__ import annotations
19
20import arcade
21import pymunk
22import random
23import timeit
24import math
25
26SCREEN_WIDTH = 800
27SCREEN_HEIGHT = 800
28SCREEN_TITLE = "Pymunk Pegboard Example"
29
30
31class CircleSprite(arcade.Sprite):
32 def __init__(self, filename, pymunk_shape):
33 super().__init__(filename, center_x=pymunk_shape.body.position.x, center_y=pymunk_shape.body.position.y)
34 self.width = pymunk_shape.radius * 2
35 self.height = pymunk_shape.radius * 2
36 self.pymunk_shape = pymunk_shape
37
38
39class MyGame(arcade.Window):
40 """ Main application class. """
41
42 def __init__(self, width, height, title):
43 super().__init__(width, height, title)
44
45 self.peg_list = arcade.SpriteList()
46 self.ball_list: arcade.SpriteList[CircleSprite] = arcade.SpriteList()
47 self.background_color = arcade.color.DARK_SLATE_GRAY
48
49 self.draw_time = 0
50 self.processing_time = 0
51 self.time = 0
52
53 # -- Pymunk
54 self.space = pymunk.Space()
55 self.space.gravity = (0.0, -900.0)
56
57 self.static_lines = []
58
59 self.ticks_to_next_ball = 10
60
61 body = pymunk.Body(body_type=pymunk.Body.STATIC)
62 shape = pymunk.Segment(body, (0, 10), (SCREEN_WIDTH, 10), 0.0)
63 shape.friction = 10
64 self.space.add(shape, body)
65 self.static_lines.append(shape)
66
67 body = pymunk.Body(body_type=pymunk.Body.STATIC)
68 shape = pymunk.Segment(body, (SCREEN_WIDTH - 50, 10), (SCREEN_WIDTH, 30), 0.0)
69 shape.friction = 10
70 self.space.add(shape, body)
71 self.static_lines.append(shape)
72
73 body = pymunk.Body(body_type=pymunk.Body.STATIC)
74 shape = pymunk.Segment(body, (50, 10), (0, 30), 0.0)
75 shape.friction = 10
76 self.space.add(shape, body)
77 self.static_lines.append(shape)
78
79 radius = 20
80 separation = 150
81 for row in range(6):
82 for column in range(6):
83 x = column * separation + (separation // 2 * (row % 2))
84 y = row * separation + separation // 2
85 body = pymunk.Body(body_type=pymunk.Body.STATIC)
86 body.position = x, y
87 shape = pymunk.Circle(body, radius, pymunk.Vec2d(0, 0))
88 shape.friction = 0.3
89 self.space.add(body, shape)
90
91 sprite = CircleSprite(":resources:images/pinball/bumper.png", shape)
92 self.peg_list.append(sprite)
93
94 def on_draw(self):
95 """
96 Render the screen.
97 """
98
99 # This command has to happen before we start drawing
100 self.clear()
101
102 draw_start_time = timeit.default_timer()
103 self.peg_list.draw()
104 self.ball_list.draw()
105
106 for line in self.static_lines:
107 body = line.body
108
109 pv1 = body.position + line.a.rotated(body.angle)
110 pv2 = body.position + line.b.rotated(body.angle)
111 arcade.draw_line(pv1.x, pv1.y, pv2.x, pv2.y, arcade.color.WHITE, 2)
112
113 # Display timings
114 output = f"Processing time: {self.processing_time:.3f}"
115 arcade.draw_text(output, 20, SCREEN_HEIGHT - 20, arcade.color.WHITE, 12)
116
117 output = f"Drawing time: {self.draw_time:.3f}"
118 arcade.draw_text(output, 20, SCREEN_HEIGHT - 40, arcade.color.WHITE, 12)
119
120 self.draw_time = timeit.default_timer() - draw_start_time
121
122 def on_update(self, delta_time):
123 start_time = timeit.default_timer()
124
125 self.ticks_to_next_ball -= 1
126 if self.ticks_to_next_ball <= 0:
127 self.ticks_to_next_ball = 20
128 mass = 0.5
129 radius = 15
130 inertia = pymunk.moment_for_circle(mass, 0, radius, (0, 0))
131 body = pymunk.Body(mass, inertia)
132 x = random.randint(0, SCREEN_WIDTH)
133 y = SCREEN_HEIGHT
134 body.position = x, y
135 shape = pymunk.Circle(body, radius, pymunk.Vec2d(0, 0))
136 shape.friction = 0.3
137 self.space.add(body, shape)
138
139 sprite = CircleSprite(":resources:images/items/gold_1.png", shape)
140 self.ball_list.append(sprite)
141
142 # Check for balls that fall off the screen
143 ball: CircleSprite
144 for ball in self.ball_list:
145 if ball.pymunk_shape.body.position.y < 0:
146 # Remove balls from physics space
147 self.space.remove(ball.pymunk_shape, ball.pymunk_shape.body)
148 # Remove balls from physics list
149 ball.remove_from_sprite_lists()
150
151 # Update physics
152 # Use a constant time step, don't use delta_time
153 # See "Game loop / moving time forward"
154 # https://www.pymunk.org/en/latest/overview.html#game-loop-moving-time-forward
155 self.space.step(1 / 60.0)
156
157 # Move sprites to where physics objects are
158 for ball in self.ball_list:
159 ball.center_x = ball.pymunk_shape.body.position.x
160 ball.center_y = ball.pymunk_shape.body.position.y
161 # Reverse angle because pymunk rotates ccw
162 ball.angle = math.degrees(-ball.pymunk_shape.body.angle)
163
164 self.time = timeit.default_timer() - start_time
165
166
167def main():
168 MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
169
170 arcade.run()
171
172
173if __name__ == "__main__":
174 main()