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(f"{key:30} = {val} ({type(val).__name__})")
35
36
37def dump_controller(controller):
38 print(f"========== {controller}")
39 print(f"Left X,Y {controller.leftstick[0]},{controller.leftstick[1]}")
40 print(f"Left Trigger {controller.lefttrigger}")
41 print(f"Right X,Y {controller.rightstick[0]},{controller.rightstick[1]}")
42 print(f"Right Trigger {controller.righttrigger}")
43 print("========== Extra controller")
44 dump_obj(controller)
45 print("========== Extra controller.device")
46 dump_obj(controller.device)
47 print("========== pprint controller")
48 pprint.pprint(controller)
49 print("========== pprint controller.device")
50 pprint.pprint(controller.device)
51
52
53def dump_controller_state(ticks, controller):
54 # print("{:5.2f} {:5.2f} {:>20} {:5}_".format(1.234567, -8.2757272903, "hello", str(True)))
55 fmt_str = "{:6d} "
56 num_fmts = ["{:5.2f}"] * 6
57 fmt_str += " ".join(num_fmts)
58 print(fmt_str.format(ticks,
59 controller.leftstick[0],
60 controller.leftstick[1],
61 controller.lefttrigger,
62 controller.rightstick[0],
63 controller.rightstick[0],
64 controller.righttrigger,
65 ))
66
67
68def get_stick_position(x, y):
69 """Given position of stick axes, return (x, y, angle_in_degrees).
70 If movement is not outside of deadzone, return (None, None, None)"""
71 if x > STICK_DEADZONE or x < -STICK_DEADZONE or y > STICK_DEADZONE or y < -STICK_DEADZONE:
72 rad = math.atan2(y, x)
73 angle = math.degrees(rad)
74 return x, y, angle
75 return None, None, None
76
77
78class Player(arcade.sprite.Sprite):
79 def __init__(self, filename):
80 super().__init__(
81 filename,
82 scale=0.4,
83 center_x=WINDOW_WIDTH / 2,
84 center_y=WINDOW_HEIGHT / 2,
85 )
86 self.shoot_up_pressed = False
87 self.shoot_down_pressed = False
88 self.shoot_left_pressed = False
89 self.shoot_right_pressed = False
90 self.start_pressed = False
91
92
93class Enemy(arcade.sprite.Sprite):
94 def __init__(self, x, y):
95 super().__init__(
96 ':resources:images/pinball/bumper.png',
97 scale=0.5,
98 center_x=x,
99 center_y=y,
100 )
101
102 def follow_sprite(self, player_sprite):
103 """
104 This function will move the current sprite towards whatever
105 other sprite is specified as a parameter.
106
107 We use the 'min' function here to get the sprite to line up with
108 the target sprite, and not jump around if the sprite is not off
109 an exact multiple of ENEMY_SPEED.
110 """
111
112 if self.center_y < player_sprite.center_y:
113 self.center_y += min(ENEMY_SPEED, player_sprite.center_y - self.center_y)
114 elif self.center_y > player_sprite.center_y:
115 self.center_y -= min(ENEMY_SPEED, self.center_y - player_sprite.center_y)
116
117 if self.center_x < player_sprite.center_x:
118 self.center_x += min(ENEMY_SPEED, player_sprite.center_x - self.center_x)
119 elif self.center_x > player_sprite.center_x:
120 self.center_x -= min(ENEMY_SPEED, self.center_x - player_sprite.center_x)
121
122
123class GameView(arcade.View):
124 def __init__(self):
125 super().__init__()
126 self.game_over = False
127 self.score = 0
128 self.tick = 0
129 self.bullet_cooldown = 0
130 self.player = Player(":resources:images/space_shooter/playerShip2_orange.png")
131 self.bullet_list = arcade.SpriteList()
132 self.enemy_list = arcade.SpriteList()
133 self.controller_manager = arcade.ControllerManager()
134 self.controller = None
135
136 self.background_color = arcade.color.DARK_MIDNIGHT_BLUE
137 controllers = self.controller_manager.get_controllers()
138 for controller in controllers:
139 dump_controller(controller)
140 if controllers:
141 self.connect_controller(controllers[0])
142 arcade.window_commands.schedule(self.debug_controller_state, 0.1)
143
144 arcade.window_commands.schedule(self.spawn_enemy, ENEMY_SPAWN_INTERVAL)
145
146 @self.controller_manager.event
147 def on_connect(controller):
148 if not self.controller:
149 self.connect_controller(controller)
150
151 @self.controller_manager.event
152 def on_disconnect(controller):
153 if self.controller == controller:
154 controller.close()
155 self.controller = None
156
157 def setup(self):
158 self.game_over = False
159 self.score = 0
160 self.tick = 0
161 self.bullet_cooldown = 0
162 self.bullet_list = arcade.SpriteList()
163 self.enemy_list = arcade.SpriteList()
164 self.player.center_x = WINDOW_WIDTH / 2
165 self.player.center_y = WINDOW_HEIGHT / 2
166
167 def connect_controller(self, controller):
168 self.controller = controller
169 self.controller.open()
170
171 def debug_controller_state(self, _delta_time):
172 if self.controller:
173 dump_controller_state(self.tick, self.controller)
174
175 def spawn_enemy(self, _elapsed):
176 if self.game_over:
177 return
178 x = random.randint(0, WINDOW_WIDTH)
179 y = random.randint(0, WINDOW_HEIGHT)
180 self.enemy_list.append(Enemy(x, y))
181
182 def on_update(self, delta_time):
183 self.tick += 1
184 if self.game_over:
185 if self.controller:
186 if self.controller.start:
187 self.setup()
188 return
189
190 if self.player.start_pressed:
191 self.setup()
192 return
193
194 return
195
196 self.bullet_cooldown += 1
197
198 for enemy in self.enemy_list:
199 cast(Enemy, enemy).follow_sprite(self.player)
200
201 if self.controller:
202 # Controller input - movement
203 left_position = self.controller.leftstick
204 right_position = self.controller.rightstick
205 move_x, move_y, move_angle = get_stick_position(
206 left_position[0], left_position[1]
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 right_position[0], right_position[1]
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()