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