Pymunk Physics Engine - Pegboard

This uses the Pymunk physics engine to simulate balls falling on a pegboard.

Screenshot of pegboard example.
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()