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