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 # close the window if the user hits the escape key
279 elif key == arcade.key.ESCAPE:
280 self.window.close()
281
282 rad = math.atan2(self.player.change_y, self.player.change_x)
283 self.player.angle = math.degrees(rad) + ROTATE_OFFSET
284
285 def on_key_release(self, key, modifiers):
286 if key == arcade.key.W:
287 self.player.change_y = 0
288 elif key == arcade.key.A:
289 self.player.change_x = 0
290 elif key == arcade.key.S:
291 self.player.change_y = 0
292 elif key == arcade.key.D:
293 self.player.change_x = 0
294 elif key == arcade.key.RIGHT:
295 self.player.shoot_right_pressed = False
296 elif key == arcade.key.UP:
297 self.player.shoot_up_pressed = False
298 elif key == arcade.key.LEFT:
299 self.player.shoot_left_pressed = False
300 elif key == arcade.key.DOWN:
301 self.player.shoot_down_pressed = False
302
303 rad = math.atan2(self.player.change_y, self.player.change_x)
304 self.player.angle = math.degrees(rad) + ROTATE_OFFSET
305
306 def spawn_bullet(self, angle_in_deg):
307 # only allow bullet to spawn on an interval
308 if self.bullet_cooldown < BULLET_COOLDOWN_TICKS:
309 return
310 self.bullet_cooldown = 0
311
312 bullet = arcade.Sprite(":resources:images/space_shooter/laserBlue01.png", scale=0.75)
313
314 # Position the bullet at the player's current location
315 start_x = self.player.center_x
316 start_y = self.player.center_y
317 bullet.center_x = start_x
318 bullet.center_y = start_y
319
320 # angle the bullet visually
321 bullet.angle = angle_in_deg
322 angle_in_rad = math.radians(angle_in_deg)
323
324 # set bullet's movement direction
325 bullet.change_x = math.cos(angle_in_rad) * BULLET_SPEED
326 bullet.change_y = math.sin(angle_in_rad) * BULLET_SPEED
327
328 # Add the bullet to the appropriate lists
329 self.bullet_list.append(bullet)
330
331 def on_draw(self):
332 # clear screen and start render process
333 self.clear()
334
335 # draw game items
336 self.bullet_list.draw()
337 self.enemy_list.draw()
338 arcade.draw_sprite(self.player)
339
340 # Put the score on the screen.
341 output = f"Score: {self.score}"
342 arcade.draw_text(output, 10, 20, arcade.color.WHITE, 14)
343
344 # Game over message
345 if self.game_over:
346 arcade.draw_text("Game Over",
347 WINDOW_WIDTH / 2,
348 WINDOW_HEIGHT / 2,
349 arcade.color.WHITE, 100,
350 width=WINDOW_WIDTH,
351 align="center",
352 anchor_x="center",
353 anchor_y="center")
354
355
356
357def main():
358 """ Main function """
359 # Create a window class. This is what actually shows up on screen
360 window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE)
361
362 # Create and setup the GameView
363 game = GameView()
364
365 # Show GameView on screen
366 window.show_view(game)
367
368 # Start the arcade game loop
369 arcade.run()
370
371
372if __name__ == "__main__":
373 main()