Pymunk Physics Engine - Pegboard
This uses the Pymunk physics engine to simulate balls falling on a pegboard.

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