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