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