Dual Stick Shooter
This example shows how to use both joysticks on a gamepad. As gamepad layout
varies a lot, you may need to adjust the mapping with SHOOTING_AXIS_SELECTION.
dual_stick_shooter.py
1"""
2Dual-stick Shooter Example
3
4A dual-analog stick controller is the preferred method of input. If a controller is
5not present, the game will fail back to use keyboard controls (WASD to move, arrows to shoot)
6
7If Python and Arcade are installed, this example can be run from the command line with:
8python -m arcade.examples.dual_stick_shooter
9"""
10import math
11import pprint
12import random
13from typing import cast
14
15import arcade
16
17WINDOW_WIDTH = 1280
18WINDOW_HEIGHT = 720
19WINDOW_TITLE = "Dual-stick Shooter Example"
20MOVEMENT_SPEED = 4
21BULLET_SPEED = 10
22BULLET_COOLDOWN_TICKS = 10
23ENEMY_SPAWN_INTERVAL = 1
24ENEMY_SPEED = 1
25STICK_DEADZONE = 0.05
26# An angle of "0" means "right", but the player's texture is oriented in the "up" direction.
27# So an offset is needed.
28ROTATE_OFFSET = -90
29
30
31def dump_obj(obj):
32 for key in sorted(vars(obj)):
33 val = getattr(obj, key)
34 print("{:30} = {} ({})".format(key, val, type(val).__name__))
35
36
37def dump_controller(controller):
38 print("========== {}".format(controller))
39 print("Left X {}".format(controller.leftx))
40 print("Left Y {}".format(controller.lefty))
41 print("Left Trigger {}".format(controller.lefttrigger))
42 print("Right X {}".format(controller.rightx))
43 print("Right Y {}".format(controller.righty))
44 print("Right Trigger {}".format(controller.righttrigger))
45 print("========== Extra controller")
46 dump_obj(controller)
47 print("========== Extra controller.device")
48 dump_obj(controller.device)
49 print("========== pprint controller")
50 pprint.pprint(controller)
51 print("========== pprint controller.device")
52 pprint.pprint(controller.device)
53
54
55def dump_controller_state(ticks, controller):
56 # print("{:5.2f} {:5.2f} {:>20} {:5}_".format(1.234567, -8.2757272903, "hello", str(True)))
57 fmt_str = "{:6d} "
58 num_fmts = ["{:5.2f}"] * 6
59 fmt_str += " ".join(num_fmts)
60 print(fmt_str.format(ticks,
61 controller.leftx,
62 controller.lefty,
63 controller.lefttrigger,
64 controller.rightx,
65 controller.righty,
66 controller.righttrigger,
67 ))
68
69
70def get_stick_position(x, y):
71 """Given position of stick axes, return (x, y, angle_in_degrees).
72 If movement is not outside of deadzone, return (None, None, None)"""
73 if x > STICK_DEADZONE or x < -STICK_DEADZONE or y > STICK_DEADZONE or y < -STICK_DEADZONE:
74 rad = math.atan2(y, x)
75 angle = math.degrees(rad)
76 return x, y, angle
77 return None, None, None
78
79
80class Player(arcade.sprite.Sprite):
81 def __init__(self, filename):
82 super().__init__(
83 filename,
84 scale=0.4,
85 center_x=WINDOW_WIDTH / 2,
86 center_y=WINDOW_HEIGHT / 2,
87 )
88 self.shoot_up_pressed = False
89 self.shoot_down_pressed = False
90 self.shoot_left_pressed = False
91 self.shoot_right_pressed = False
92 self.start_pressed = False
93
94
95class Enemy(arcade.sprite.Sprite):
96 def __init__(self, x, y):
97 super().__init__(
98 ':resources:images/pinball/bumper.png',
99 scale=0.5,
100 center_x=x,
101 center_y=y,
102 )
103
104 def follow_sprite(self, player_sprite):
105 """
106 This function will move the current sprite towards whatever
107 other sprite is specified as a parameter.
108
109 We use the 'min' function here to get the sprite to line up with
110 the target sprite, and not jump around if the sprite is not off
111 an exact multiple of ENEMY_SPEED.
112 """
113
114 if self.center_y < player_sprite.center_y:
115 self.center_y += min(ENEMY_SPEED, player_sprite.center_y - self.center_y)
116 elif self.center_y > player_sprite.center_y:
117 self.center_y -= min(ENEMY_SPEED, self.center_y - player_sprite.center_y)
118
119 if self.center_x < player_sprite.center_x:
120 self.center_x += min(ENEMY_SPEED, player_sprite.center_x - self.center_x)
121 elif self.center_x > player_sprite.center_x:
122 self.center_x -= min(ENEMY_SPEED, self.center_x - player_sprite.center_x)
123
124
125class GameView(arcade.View):
126 def __init__(self):
127 super().__init__()
128 self.game_over = False
129 self.score = 0
130 self.tick = 0
131 self.bullet_cooldown = 0
132 self.player = Player(":resources:images/space_shooter/playerShip2_orange.png")
133 self.bullet_list = arcade.SpriteList()
134 self.enemy_list = arcade.SpriteList()
135 self.controller_manager = arcade.ControllerManager()
136 self.controller = None
137
138 self.background_color = arcade.color.DARK_MIDNIGHT_BLUE
139 controllers = self.controller_manager.get_controllers()
140 for controller in controllers:
141 dump_controller(controller)
142 if controllers:
143 self.connect_controller(controllers[0])
144 arcade.window_commands.schedule(self.debug_controller_state, 0.1)
145
146 arcade.window_commands.schedule(self.spawn_enemy, ENEMY_SPAWN_INTERVAL)
147
148 @self.controller_manager.event
149 def on_connect(controller):
150 if not self.controller:
151 self.connect_controller(controller)
152
153 @self.controller_manager.event
154 def on_disconnect(controller):
155 if self.controller == controller:
156 controller.close()
157 self.controller = None
158
159 def setup(self):
160 self.game_over = False
161 self.score = 0
162 self.tick = 0
163 self.bullet_cooldown = 0
164 self.bullet_list = arcade.SpriteList()
165 self.enemy_list = arcade.SpriteList()
166 self.player.center_x = WINDOW_WIDTH / 2
167 self.player.center_y = WINDOW_HEIGHT / 2
168
169 def connect_controller(self, controller):
170 self.controller = controller
171 self.controller.open()
172
173 def debug_controller_state(self, _delta_time):
174 if self.controller:
175 dump_controller_state(self.tick, self.controller)
176
177 def spawn_enemy(self, _elapsed):
178 if self.game_over:
179 return
180 x = random.randint(0, WINDOW_WIDTH)
181 y = random.randint(0, WINDOW_HEIGHT)
182 self.enemy_list.append(Enemy(x, y))
183
184 def on_update(self, delta_time):
185 self.tick += 1
186 if self.game_over:
187 if self.controller:
188 if self.controller.start:
189 self.setup()
190 return
191
192 if self.player.start_pressed:
193 self.setup()
194 return
195
196 return
197
198 self.bullet_cooldown += 1
199
200 for enemy in self.enemy_list:
201 cast(Enemy, enemy).follow_sprite(self.player)
202
203 if self.controller:
204 # Controller input - movement
205 move_x, move_y, move_angle = get_stick_position(
206 self.controller.leftx, self.controller.lefty
207 )
208 if move_angle:
209 self.player.change_x = move_x * MOVEMENT_SPEED
210 self.player.change_y = move_y * MOVEMENT_SPEED
211 self.player.angle = move_angle + ROTATE_OFFSET
212 else:
213 self.player.change_x = 0
214 self.player.change_y = 0
215
216 # Controller input - shooting
217 shoot_x, shoot_y, shoot_angle = get_stick_position(
218 self.controller.rightx, self.controller.righty
219 )
220 if shoot_angle:
221 self.spawn_bullet(shoot_angle)
222 else:
223 # Keyboard input - shooting
224 if self.player.shoot_right_pressed and self.player.shoot_up_pressed:
225 self.spawn_bullet(0 + 45)
226 elif self.player.shoot_up_pressed and self.player.shoot_left_pressed:
227 self.spawn_bullet(90 + 45)
228 elif self.player.shoot_left_pressed and self.player.shoot_down_pressed:
229 self.spawn_bullet(180 + 45)
230 elif self.player.shoot_down_pressed and self.player.shoot_right_pressed:
231 self.spawn_bullet(270 + 45)
232 elif self.player.shoot_right_pressed:
233 self.spawn_bullet(0)
234 elif self.player.shoot_up_pressed:
235 self.spawn_bullet(90)
236 elif self.player.shoot_left_pressed:
237 self.spawn_bullet(180)
238 elif self.player.shoot_down_pressed:
239 self.spawn_bullet(270)
240
241 self.enemy_list.update()
242 self.player.update()
243 self.bullet_list.update()
244 ship_death_hit_list = arcade.check_for_collision_with_list(self.player,
245 self.enemy_list)
246 if len(ship_death_hit_list) > 0:
247 self.game_over = True
248 for bullet in self.bullet_list:
249 bullet_killed = False
250 enemy_shot_list = arcade.check_for_collision_with_list(bullet,
251 self.enemy_list)
252 # Loop through each colliding sprite, remove it, and add to the score.
253 for enemy in enemy_shot_list:
254 enemy.remove_from_sprite_lists()
255 bullet.remove_from_sprite_lists()
256 bullet_killed = True
257 self.score += 1
258 if bullet_killed:
259 continue
260
261 def on_key_press(self, key, modifiers):
262 if key == arcade.key.W:
263 self.player.change_y = MOVEMENT_SPEED
264 elif key == arcade.key.A:
265 self.player.change_x = -MOVEMENT_SPEED
266 elif key == arcade.key.S:
267 self.player.change_y = -MOVEMENT_SPEED
268 elif key == arcade.key.D:
269 self.player.change_x = MOVEMENT_SPEED
270 elif key == arcade.key.RIGHT:
271 self.player.shoot_right_pressed = True
272 elif key == arcade.key.UP:
273 self.player.shoot_up_pressed = True
274 elif key == arcade.key.LEFT:
275 self.player.shoot_left_pressed = True
276 elif key == arcade.key.DOWN:
277 self.player.shoot_down_pressed = True
278 elif key == arcade.key.ESCAPE:
279 self.player.start_pressed = True
280 # close the window if the user hits the escape key
281 elif key == arcade.key.ESCAPE:
282 self.close()
283
284 rad = math.atan2(self.player.change_y, self.player.change_x)
285 self.player.angle = math.degrees(rad) + ROTATE_OFFSET
286
287 def on_key_release(self, key, modifiers):
288 if key == arcade.key.W:
289 self.player.change_y = 0
290 elif key == arcade.key.A:
291 self.player.change_x = 0
292 elif key == arcade.key.S:
293 self.player.change_y = 0
294 elif key == arcade.key.D:
295 self.player.change_x = 0
296 elif key == arcade.key.RIGHT:
297 self.player.shoot_right_pressed = False
298 elif key == arcade.key.UP:
299 self.player.shoot_up_pressed = False
300 elif key == arcade.key.LEFT:
301 self.player.shoot_left_pressed = False
302 elif key == arcade.key.DOWN:
303 self.player.shoot_down_pressed = False
304
305 rad = math.atan2(self.player.change_y, self.player.change_x)
306 self.player.angle = math.degrees(rad) + ROTATE_OFFSET
307
308 def spawn_bullet(self, angle_in_deg):
309 # only allow bullet to spawn on an interval
310 if self.bullet_cooldown < BULLET_COOLDOWN_TICKS:
311 return
312 self.bullet_cooldown = 0
313
314 bullet = arcade.Sprite(":resources:images/space_shooter/laserBlue01.png", scale=0.75)
315
316 # Position the bullet at the player's current location
317 start_x = self.player.center_x
318 start_y = self.player.center_y
319 bullet.center_x = start_x
320 bullet.center_y = start_y
321
322 # angle the bullet visually
323 bullet.angle = angle_in_deg
324 angle_in_rad = math.radians(angle_in_deg)
325
326 # set bullet's movement direction
327 bullet.change_x = math.cos(angle_in_rad) * BULLET_SPEED
328 bullet.change_y = math.sin(angle_in_rad) * BULLET_SPEED
329
330 # Add the bullet to the appropriate lists
331 self.bullet_list.append(bullet)
332
333 def on_draw(self):
334 # clear screen and start render process
335 self.clear()
336
337 # draw game items
338 self.bullet_list.draw()
339 self.enemy_list.draw()
340 arcade.draw_sprite(self.player)
341
342 # Put the score on the screen.
343 output = f"Score: {self.score}"
344 arcade.draw_text(output, 10, 20, arcade.color.WHITE, 14)
345
346 # Game over message
347 if self.game_over:
348 arcade.draw_text("Game Over",
349 WINDOW_WIDTH / 2,
350 WINDOW_HEIGHT / 2,
351 arcade.color.WHITE, 100,
352 width=WINDOW_WIDTH,
353 align="center",
354 anchor_x="center",
355 anchor_y="center")
356
357
358
359def main():
360 """ Main function """
361 # Create a window class. This is what actually shows up on screen
362 window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE)
363
364 # Create and setup the GameView
365 game = GameView()
366
367 # Show GameView on screen
368 window.show_view(game)
369
370 # Start the arcade game loop
371 arcade.run()
372
373
374if __name__ == "__main__":
375 main()