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