Pymunk Demo - Top Down

Pymunk Demo - Top Down
pymunk_demo_top_down.py
  1"""
  2Example of Pymunk Physics Engine
  3Top-down
  4
  5If Python and Arcade are installed, this example can be run from the command line with:
  6python -m arcade.examples.pymunk_demo_top_down
  7"""
  8import math
  9import random
 10import arcade
 11from arcade.pymunk_physics_engine import PymunkPhysicsEngine
 12
 13WINDOW_TITLE = "PyMunk Top-Down"
 14SPRITE_SCALING_PLAYER = 0.5
 15MOVEMENT_SPEED = 5
 16
 17SPRITE_IMAGE_SIZE = 128
 18SPRITE_SIZE = int(SPRITE_IMAGE_SIZE * SPRITE_SCALING_PLAYER)
 19
 20WINDOW_WIDTH = SPRITE_SIZE * 15
 21WINDOW_HEIGHT = SPRITE_SIZE * 10
 22
 23# Physics force used to move the player. Higher number, faster accelerating.
 24PLAYER_MOVE_FORCE = 4000
 25BULLET_MOVE_FORCE = 2500
 26
 27
 28class GameView(arcade.View):
 29    def __init__(self):
 30        """ Init """
 31        super().__init__()
 32
 33        self.background_color = arcade.color.AMAZON
 34
 35        self.player_list = None
 36        self.wall_list = None
 37        self.bullet_list = None
 38        self.rock_list = None
 39        self.gem_list = None
 40        self.player_sprite = None
 41        self.physics_engine: PymunkPhysicsEngine | None = None
 42
 43        # Track the current state of what key is pressed
 44        self.left_pressed = False
 45        self.right_pressed = False
 46        self.up_pressed = False
 47        self.down_pressed = False
 48
 49    def setup(self):
 50        """ Set up everything """
 51        # Create the sprite lists
 52        self.player_list = arcade.SpriteList()
 53        self.wall_list = arcade.SpriteList()
 54        self.bullet_list = arcade.SpriteList()
 55        self.rock_list = arcade.SpriteList()
 56        self.gem_list = arcade.SpriteList()
 57
 58        # Set up the player
 59        self.player_sprite = arcade.Sprite(
 60            ":resources:images/animated_characters/female_person/femalePerson_idle.png",
 61            scale=SPRITE_SCALING_PLAYER)
 62        self.player_sprite.center_x = 250
 63        self.player_sprite.center_y = 250
 64        self.player_list.append(self.player_sprite)
 65
 66        # Set up the walls
 67        for x in range(0, WINDOW_WIDTH + 1, SPRITE_SIZE):
 68            wall = arcade.Sprite(":resources:images/tiles/grassCenter.png",
 69                                 scale=SPRITE_SCALING_PLAYER)
 70            wall.center_x = x
 71            wall.center_y = 0
 72            self.wall_list.append(wall)
 73
 74            wall = arcade.Sprite(":resources:images/tiles/grassCenter.png",
 75                                 scale=SPRITE_SCALING_PLAYER)
 76            wall.center_x = x
 77            wall.center_y = WINDOW_HEIGHT
 78            self.wall_list.append(wall)
 79
 80        # Set up the walls
 81        for y in range(SPRITE_SIZE, WINDOW_HEIGHT, SPRITE_SIZE):
 82            wall = arcade.Sprite(":resources:images/tiles/grassCenter.png",
 83                                 scale=SPRITE_SCALING_PLAYER)
 84            wall.center_x = 0
 85            wall.center_y = y
 86            self.wall_list.append(wall)
 87
 88            wall = arcade.Sprite(":resources:images/tiles/grassCenter.png",
 89                                 scale=SPRITE_SCALING_PLAYER)
 90            wall.center_x = WINDOW_WIDTH
 91            wall.center_y = y
 92            self.wall_list.append(wall)
 93
 94        # Add some movable rocks
 95        for x in range(SPRITE_SIZE * 2, SPRITE_SIZE * 13, SPRITE_SIZE):
 96            rock = random.randrange(4) + 1
 97            item = arcade.Sprite(f":resources:images/space_shooter/meteorGrey_big{rock}.png",
 98                                 scale=SPRITE_SCALING_PLAYER)
 99            item.center_x = x
100            item.center_y = 400
101            self.rock_list.append(item)
102
103        # Add some movable coins
104        for x in range(SPRITE_SIZE * 2, SPRITE_SIZE * 13, SPRITE_SIZE):
105            items = [":resources:images/items/gemBlue.png",
106                     ":resources:images/items/gemRed.png",
107                     ":resources:images/items/coinGold.png",
108                     ":resources:images/items/keyBlue.png"]
109            item_name = random.choice(items)
110            item = arcade.Sprite(item_name,
111                                 scale=SPRITE_SCALING_PLAYER)
112            item.center_x = x
113            item.center_y = 300
114            self.gem_list.append(item)
115
116        # --- Pymunk Physics Engine Setup ---
117
118        # The default damping for every object controls the percent of velocity
119        # the object will keep each second. A value of 1.0 is no speed loss,
120        # 0.9 is 10% per second, 0.1 is 90% per second.
121        # For top-down games, this is basically the friction for moving objects.
122        # For platformers with gravity, this should probably be set to 1.0.
123        # Default value is 1.0 if not specified.
124        damping = 0.7
125
126        # Set the gravity. (0, 0) is good for outer space and top-down.
127        gravity = (0, 0)
128
129        # Create the physics engine
130        self.physics_engine = PymunkPhysicsEngine(damping=damping,
131                                                  gravity=gravity)
132
133        def rock_hit_handler(sprite_a, sprite_b, arbiter, space, data):
134            """ Called for bullet/rock collision """
135            bullet_shape = arbiter.shapes[0]
136            bullet_sprite = self.physics_engine.get_sprite_for_shape(bullet_shape)
137            bullet_sprite.remove_from_sprite_lists()
138            print("Rock")
139
140        def wall_hit_handler(sprite_a, sprite_b, arbiter, space, data):
141            """ Called for bullet/rock collision """
142            bullet_shape = arbiter.shapes[0]
143            bullet_sprite = self.physics_engine.get_sprite_for_shape(bullet_shape)
144            bullet_sprite.remove_from_sprite_lists()
145            print("Wall")
146
147        self.physics_engine.add_collision_handler(
148            "bullet",
149            "rock",
150            post_handler=rock_hit_handler,
151        )
152        self.physics_engine.add_collision_handler(
153            "bullet",
154            "wall",
155            post_handler=wall_hit_handler,
156        )
157
158        # Add the player.
159        # For the player, we set the damping to a lower value, which increases
160        # the damping rate. This prevents the character from traveling too far
161        # after the player lets off the movement keys.
162        # Setting the moment to PymunkPhysicsEngine.MOMENT_INF prevents it from
163        # rotating.
164        # Friction normally goes between 0 (no friction) and 1.0 (high friction)
165        # Friction is between two objects in contact. It is important to remember
166        # in top-down games that friction moving along the 'floor' is controlled
167        # by damping.
168        self.physics_engine.add_sprite(self.player_sprite,
169                                       friction=0.6,
170                                       moment_of_inertia=PymunkPhysicsEngine.MOMENT_INF,
171                                       damping=0.01,
172                                       collision_type="player",
173                                       max_velocity=400)
174
175        # Create the walls.
176        # By setting the body type to PymunkPhysicsEngine.STATIC the walls can't
177        # move.
178        # Movable objects that respond to forces are PymunkPhysicsEngine.DYNAMIC
179        # PymunkPhysicsEngine.KINEMATIC objects will move, but are assumed to be
180        # repositioned by code and don't respond to physics forces.
181        # Dynamic is default.
182        self.physics_engine.add_sprite_list(self.wall_list,
183                                            friction=0.6,
184                                            collision_type="wall",
185                                            body_type=PymunkPhysicsEngine.STATIC)
186
187        # Create some boxes to push around.
188        # Mass controls, well, the mass of an object. Defaults to 1.
189        self.physics_engine.add_sprite_list(self.rock_list,
190                                            mass=2,
191                                            friction=0.8,
192                                            damping=0.1,
193                                            collision_type="rock")
194        # Create some boxes to push around.
195        # Mass controls, well, the mass of an object. Defaults to 1.
196        self.physics_engine.add_sprite_list(self.gem_list,
197                                            mass=0.5,
198                                            friction=0.8,
199                                            damping=0.4,
200                                            collision_type="rock")
201
202    def on_mouse_press(self, x, y, button, modifiers):
203        """ Called whenever the mouse button is clicked. """
204
205        bullet = arcade.SpriteSolidColor(width=5, height=5, color=arcade.color.RED)
206        self.bullet_list.append(bullet)
207
208        # Position the bullet at the player's current location
209        start_x = self.player_sprite.center_x
210        start_y = self.player_sprite.center_y
211        bullet.position = self.player_sprite.position
212
213        # Get from the mouse the destination location for the bullet
214        # IMPORTANT! If you have a scrolling screen, you will also need
215        # to add in self.view_bottom and self.view_left.
216        dest_x = x
217        dest_y = y
218
219        # Do math to calculate how to get the bullet to the destination.
220        # Calculation the angle in radians between the start points
221        # and end points. This is the angle the bullet will travel.
222        x_diff = dest_x - start_x
223        y_diff = dest_y - start_y
224        angle = math.atan2(y_diff, x_diff)
225
226        force = [math.cos(angle), math.sin(angle)]
227        size = max(self.player_sprite.width, self.player_sprite.height) / 2
228
229        bullet.center_x += size * force[0]
230        bullet.center_y += size * force[1]
231
232        self.physics_engine.add_sprite(bullet,
233                                       mass=0.1,
234                                       damping=1.0,
235                                       friction=0.6,
236                                       collision_type="bullet",
237                                       elasticity=0.9)
238
239        # Taking into account the angle, calculate our force.
240        force[0] *= BULLET_MOVE_FORCE
241        force[1] *= BULLET_MOVE_FORCE
242
243        self.physics_engine.apply_force(bullet, force)
244
245    def on_key_press(self, key, modifiers):
246        """Called whenever a key is pressed. """
247
248        if key == arcade.key.UP:
249            self.up_pressed = True
250        elif key == arcade.key.DOWN:
251            self.down_pressed = True
252        elif key == arcade.key.LEFT:
253            self.left_pressed = True
254        elif key == arcade.key.RIGHT:
255            self.right_pressed = True
256        elif key == arcade.key.SPACE:
257            bullet = arcade.SpriteSolidColor(9, 9, arcade.color.RED)
258            bullet.position = self.player_sprite.position
259            bullet.center_x += 30
260            self.bullet_list.append(bullet)
261            self.physics_engine.add_sprite(bullet,
262                                           mass=0.2,
263                                           damping=1.0,
264                                           friction=0.6,
265                                           collision_type="bullet")
266            force = (3000, 0)
267            self.physics_engine.apply_force(bullet, force)
268
269    def on_key_release(self, key, modifiers):
270        """Called when the user releases a key. """
271
272        if key == arcade.key.UP:
273            self.up_pressed = False
274        elif key == arcade.key.DOWN:
275            self.down_pressed = False
276        elif key == arcade.key.LEFT:
277            self.left_pressed = False
278        elif key == arcade.key.RIGHT:
279            self.right_pressed = False
280
281    def on_update(self, delta_time):
282        """ Movement and game logic """
283
284        # Calculate speed based on the keys pressed
285        self.player_sprite.change_x = 0
286        self.player_sprite.change_y = 0
287
288        if self.up_pressed and not self.down_pressed:
289            force = (0, PLAYER_MOVE_FORCE)
290            self.physics_engine.apply_force(self.player_sprite, force)
291        elif self.down_pressed and not self.up_pressed:
292            force = (0, -PLAYER_MOVE_FORCE)
293            self.physics_engine.apply_force(self.player_sprite, force)
294        if self.left_pressed and not self.right_pressed:
295            self.player_sprite.change_x = -MOVEMENT_SPEED
296            force = (-PLAYER_MOVE_FORCE, 0)
297            self.physics_engine.apply_force(self.player_sprite, force)
298        elif self.right_pressed and not self.left_pressed:
299            force = (PLAYER_MOVE_FORCE, 0)
300            self.physics_engine.apply_force(self.player_sprite, force)
301
302        # --- Move items in the physics engine
303        self.physics_engine.step()
304
305    def on_draw(self):
306        """ Draw everything """
307        self.clear()
308        self.wall_list.draw()
309        self.bullet_list.draw()
310        self.rock_list.draw()
311        self.gem_list.draw()
312        self.player_list.draw()
313
314def main():
315    """ Main function """
316    # Create a window class. This is what actually shows up on screen
317    window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE)
318
319    # Create and setup the GameView
320    game = GameView()
321    game.setup()
322
323    # Show GameView on screen
324    window.show_view(game)
325
326    # Start the arcade game loop
327    arcade.run()
328
329
330if __name__ == "__main__":
331    main()