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