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 joystick is the preferred method of input. If a joystick 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 arcade
11import random
12import time
13import math
14import os
15from typing import cast
16import pprint
17
18import pyglet.input.base
19
20
21SCREEN_WIDTH = 1024
22SCREEN_HEIGHT = 768
23SCREEN_TITLE = "Dual-stick Shooter Example"
24MOVEMENT_SPEED = 4
25BULLET_SPEED = 10
26BULLET_COOLDOWN_TICKS = 10
27ENEMY_SPAWN_INTERVAL = 1
28ENEMY_SPEED = 1
29JOY_DEADZONE = 0.2
30# An angle of "0" means "right", but the player's texture is oriented in the "up" direction.
31# So an offset is needed.
32ROTATE_OFFSET = -90
33
34
35def dump_obj(obj):
36 for key in sorted(vars(obj)):
37 val = getattr(obj, key)
38 print("{:30} = {} ({})".format(key, val, type(val).__name__))
39
40
41def dump_joystick(joy):
42 print("========== {}".format(joy))
43 print("x {}".format(joy.x))
44 print("y {}".format(joy.y))
45 print("z {}".format(joy.z))
46 print("rx {}".format(joy.rx))
47 print("ry {}".format(joy.ry))
48 print("rz {}".format(joy.rz))
49 print("hat_x {}".format(joy.hat_x))
50 print("hat_y {}".format(joy.hat_y))
51 print("buttons {}".format(joy.buttons))
52 print("========== Extra joy")
53 dump_obj(joy)
54 print("========== Extra joy.device")
55 dump_obj(joy.device)
56 print("========== pprint joy")
57 pprint.pprint(joy)
58 print("========== pprint joy.device")
59 pprint.pprint(joy.device)
60
61
62def dump_joystick_state(ticks, joy):
63 # print("{:5.2f} {:5.2f} {:>20} {:5}_".format(1.234567, -8.2757272903, "hello", str(True)))
64 fmt_str = "{:6d} "
65 num_fmts = ["{:5.2f}"] * 6
66 fmt_str += " ".join(num_fmts)
67 fmt_str += " {:2d} {:2d} {}"
68 buttons = " ".join(["{:5}".format(str(b)) for b in joy.buttons])
69 print(fmt_str.format(ticks,
70 joy.x,
71 joy.y,
72 joy.z,
73 joy.rx,
74 joy.ry,
75 joy.rz,
76 joy.hat_x,
77 joy.hat_y,
78 buttons))
79
80
81def get_joy_position(x, y):
82 """Given position of joystick axes, return (x, y, angle_in_degrees).
83 If movement is not outside of deadzone, return (None, None, None)"""
84 if x > JOY_DEADZONE or x < -JOY_DEADZONE or y > JOY_DEADZONE or y < -JOY_DEADZONE:
85 y = -y
86 rad = math.atan2(y, x)
87 angle = math.degrees(rad)
88 return x, y, angle
89 return None, None, None
90
91
92class Player(arcade.sprite.Sprite):
93 def __init__(self, filename):
94 super().__init__(filename=filename, scale=0.4, center_x=SCREEN_WIDTH/2, center_y=SCREEN_HEIGHT/2)
95 self.shoot_up_pressed = False
96 self.shoot_down_pressed = False
97 self.shoot_left_pressed = False
98 self.shoot_right_pressed = False
99
100
101class Enemy(arcade.sprite.Sprite):
102 def __init__(self, x, y):
103 super().__init__(filename=':resources:images/pinball/bumper.png', scale=0.5, center_x=x, center_y=y)
104
105 def follow_sprite(self, player_sprite):
106 """
107 This function will move the current sprite towards whatever
108 other sprite is specified as a parameter.
109
110 We use the 'min' function here to get the sprite to line up with
111 the target sprite, and not jump around if the sprite is not off
112 an exact multiple of ENEMY_SPEED.
113 """
114
115 if self.center_y < player_sprite.center_y:
116 self.center_y += min(ENEMY_SPEED, player_sprite.center_y - self.center_y)
117 elif self.center_y > player_sprite.center_y:
118 self.center_y -= min(ENEMY_SPEED, self.center_y - player_sprite.center_y)
119
120 if self.center_x < player_sprite.center_x:
121 self.center_x += min(ENEMY_SPEED, player_sprite.center_x - self.center_x)
122 elif self.center_x > player_sprite.center_x:
123 self.center_x -= min(ENEMY_SPEED, self.center_x - player_sprite.center_x)
124
125
126class MyGame(arcade.View):
127 def __init__(self):
128 super().__init__()
129 self.game_over = False
130 self.score = 0
131 self.tick = 0
132 self.bullet_cooldown = 0
133 self.player = Player(":resources:images/space_shooter/playerShip2_orange.png")
134 self.bullet_list = arcade.SpriteList()
135 self.enemy_list = arcade.SpriteList()
136 self.joy = None
137
138 def on_show(self):
139 arcade.set_background_color(arcade.color.DARK_MIDNIGHT_BLUE)
140 joys = self.window.joys
141 for joy in joys:
142 dump_joystick(joy)
143 if joys:
144 self.joy = joys[0]
145 print("Using joystick controls: {}".format(self.joy.device))
146 arcade.window_commands.schedule(self.debug_joy_state, 0.1)
147 if not self.joy:
148 print("No joystick present, using keyboard controls")
149 arcade.window_commands.schedule(self.spawn_enemy, ENEMY_SPAWN_INTERVAL)
150
151 def debug_joy_state(self, _delta_time):
152 dump_joystick_state(self.tick, self.joy)
153
154 def spawn_enemy(self, _elapsed):
155 if self.game_over:
156 return
157 x = random.randint(0, SCREEN_WIDTH)
158 y = random.randint(0, SCREEN_HEIGHT)
159 self.enemy_list.append(Enemy(x, y))
160
161 def on_update(self, delta_time):
162 self.tick += 1
163 if self.game_over:
164 return
165
166 self.bullet_cooldown += 1
167
168 for enemy in self.enemy_list:
169 cast(Enemy, enemy).follow_sprite(self.player)
170
171 if self.joy:
172 # Joystick input - movement
173 move_x, move_y, move_angle = get_joy_position(self.joy.move_stick_x, self.joy.move_stick_y)
174 if move_angle:
175 self.player.change_x = move_x * MOVEMENT_SPEED
176 self.player.change_y = move_y * MOVEMENT_SPEED
177 self.player.angle = move_angle + ROTATE_OFFSET
178 else:
179 self.player.change_x = 0
180 self.player.change_y = 0
181
182 # Joystick input - shooting
183 shoot_x, shoot_y, shoot_angle = get_joy_position(self.joy.shoot_stick_x, self.joy.shoot_stick_y)
184 if shoot_angle:
185 self.spawn_bullet(shoot_angle)
186 else:
187 # Keyboard input - shooting
188 if self.player.shoot_right_pressed and self.player.shoot_up_pressed:
189 self.spawn_bullet(0+45)
190 elif self.player.shoot_up_pressed and self.player.shoot_left_pressed:
191 self.spawn_bullet(90+45)
192 elif self.player.shoot_left_pressed and self.player.shoot_down_pressed:
193 self.spawn_bullet(180+45)
194 elif self.player.shoot_down_pressed and self.player.shoot_right_pressed:
195 self.spawn_bullet(270+45)
196 elif self.player.shoot_right_pressed:
197 self.spawn_bullet(0)
198 elif self.player.shoot_up_pressed:
199 self.spawn_bullet(90)
200 elif self.player.shoot_left_pressed:
201 self.spawn_bullet(180)
202 elif self.player.shoot_down_pressed:
203 self.spawn_bullet(270)
204
205 self.enemy_list.update()
206 self.player.update()
207 self.bullet_list.update()
208 ship_death_hit_list = arcade.check_for_collision_with_list(self.player, self.enemy_list)
209 if len(ship_death_hit_list) > 0:
210 self.game_over = True
211 for bullet in self.bullet_list:
212 bullet_killed = False
213 enemy_shot_list = arcade.check_for_collision_with_list(bullet, self.enemy_list)
214 # Loop through each colliding sprite, remove it, and add to the score.
215 for enemy in enemy_shot_list:
216 enemy.remove_from_sprite_lists()
217 bullet.remove_from_sprite_lists()
218 bullet_killed = True
219 self.score += 1
220 if bullet_killed:
221 continue
222
223 def on_key_press(self, key, modifiers):
224 if key == arcade.key.W:
225 self.player.change_y = MOVEMENT_SPEED
226 elif key == arcade.key.A:
227 self.player.change_x = -MOVEMENT_SPEED
228 elif key == arcade.key.S:
229 self.player.change_y = -MOVEMENT_SPEED
230 elif key == arcade.key.D:
231 self.player.change_x = MOVEMENT_SPEED
232 elif key == arcade.key.RIGHT:
233 self.player.shoot_right_pressed = True
234 elif key == arcade.key.UP:
235 self.player.shoot_up_pressed = True
236 elif key == arcade.key.LEFT:
237 self.player.shoot_left_pressed = True
238 elif key == arcade.key.DOWN:
239 self.player.shoot_down_pressed = True
240
241 rad = math.atan2(self.player.change_y, self.player.change_x)
242 self.player.angle = math.degrees(rad) + ROTATE_OFFSET
243
244 def on_key_release(self, key, modifiers):
245 if key == arcade.key.W:
246 self.player.change_y = 0
247 elif key == arcade.key.A:
248 self.player.change_x = 0
249 elif key == arcade.key.S:
250 self.player.change_y = 0
251 elif key == arcade.key.D:
252 self.player.change_x = 0
253 elif key == arcade.key.RIGHT:
254 self.player.shoot_right_pressed = False
255 elif key == arcade.key.UP:
256 self.player.shoot_up_pressed = False
257 elif key == arcade.key.LEFT:
258 self.player.shoot_left_pressed = False
259 elif key == arcade.key.DOWN:
260 self.player.shoot_down_pressed = False
261
262 rad = math.atan2(self.player.change_y, self.player.change_x)
263 self.player.angle = math.degrees(rad) + ROTATE_OFFSET
264
265 def spawn_bullet(self, angle_in_deg):
266 # only allow bullet to spawn on an interval
267 if self.bullet_cooldown < BULLET_COOLDOWN_TICKS:
268 return
269 self.bullet_cooldown = 0
270
271 bullet = arcade.Sprite(":resources:images/space_shooter/laserBlue01.png", 0.75)
272
273 # Position the bullet at the player's current location
274 start_x = self.player.center_x
275 start_y = self.player.center_y
276 bullet.center_x = start_x
277 bullet.center_y = start_y
278
279 # angle the bullet visually
280 bullet.angle = angle_in_deg
281 angle_in_rad = math.radians(angle_in_deg)
282
283 # set bullet's movement direction
284 bullet.change_x = math.cos(angle_in_rad) * BULLET_SPEED
285 bullet.change_y = math.sin(angle_in_rad) * BULLET_SPEED
286
287 # Add the bullet to the appropriate lists
288 self.bullet_list.append(bullet)
289
290 def on_draw(self):
291 # clear screen and start render process
292 arcade.start_render()
293
294 # draw game items
295 self.bullet_list.draw()
296 self.enemy_list.draw()
297 self.player.draw()
298
299 # Put the score on the screen.
300 output = f"Score: {self.score}"
301 arcade.draw_text(output, 10, 20, arcade.color.WHITE, 14)
302
303 # Game over message
304 if self.game_over:
305 arcade.draw_text("Game Over", SCREEN_WIDTH/2, SCREEN_HEIGHT/2, arcade.color.WHITE, 100, width=SCREEN_WIDTH,
306 align="center", anchor_x="center", anchor_y="center")
307
308
309class JoyConfigView(arcade.View):
310 """A View that allows a user to interactively configure their joystick"""
311 REGISTRATION_PAUSE = 1.5
312 NO_JOYSTICK_PAUSE = 2.0
313 JOY_ATTRS = ("x", "y", "z", "rx", "ry", "rz")
314
315 def __init__(self, joy_method_names, joysticks, next_view, width, height):
316 super().__init__()
317 self.next_view = next_view
318 self.width = width
319 self.height = height
320 self.msg = ""
321 self.script = self.joy_config_script()
322 self.joys = joysticks
323 arcade.set_background_color(arcade.color.WHITE)
324 if len(joysticks) > 0:
325 self.joy = joysticks[0]
326 self.joy_method_names = joy_method_names
327 self.axis_ranges = {}
328
329 def config_axis(self, joy_axis_label, method_name):
330 self.msg = joy_axis_label
331 self.axis_ranges = {a: 0.0 for a in self.JOY_ATTRS}
332 while max([v for k, v in self.axis_ranges.items()]) < 0.85:
333 for attr, farthest_val in self.axis_ranges.items():
334 cur_val = getattr(self.joy, attr)
335 if abs(cur_val) > abs(farthest_val):
336 self.axis_ranges[attr] = abs(cur_val)
337 yield
338
339 max_val = 0.0
340 max_attr = None
341 for attr, farthest_val in self.axis_ranges.items():
342 if farthest_val > max_val:
343 max_attr = attr
344 max_val = farthest_val
345 self.msg = f"Registered!"
346
347 setattr(pyglet.input.base.Joystick, method_name, property(lambda that: getattr(that, max_attr), None))
348
349 # pause briefly after registering an axis
350 yield from self._pause(self.REGISTRATION_PAUSE)
351
352 def joy_config_script(self):
353 if len(self.joys) == 0:
354 self.msg = "No joysticks found! Use keyboard controls."
355 yield from self._pause(self.NO_JOYSTICK_PAUSE)
356 return
357
358 for joy_axis_label, method_name in self.joy_method_names:
359 yield from self.config_axis(joy_axis_label, method_name)
360
361 def on_update(self, delta_time):
362 try:
363 next(self.script)
364 except StopIteration:
365 self.window.show_view(self.next_view)
366
367 def on_draw(self):
368 arcade.start_render()
369 arcade.draw_text("Configure your joystick", self.width/2, self.height/2+100,
370 arcade.color.BLACK, font_size=32, anchor_x="center")
371 arcade.draw_text(self.msg, self.width/2, self.height/2,
372 arcade.color.BLACK, font_size=24, anchor_x="center")
373
374 def _pause(self, delay):
375 """Block a generator from advancing for the given delay. Call with 'yield from self._pause(1.0)"""
376 start = time.time()
377 end = start + delay
378 while time.time() < end:
379 yield
380
381
382if __name__ == "__main__":
383 window = arcade.Window(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
384
385 # Set the working directory (where we expect to find files) to the same
386 # directory this .py file is in. You can leave this out of your own
387 # code, but it is needed to easily run the examples using "python -m"
388 # as mentioned at the top of this program.
389 file_path = os.path.dirname(os.path.abspath(__file__))
390 os.chdir(file_path)
391
392 window.joys = arcade.get_joysticks()
393 for j in window.joys:
394 j.open()
395 joy_config_method_names = (
396 ("Move the movement stick left or right", "move_stick_x"),
397 ("Move the movement stick up or down", "move_stick_y"),
398 ("Move the shooting stick left or right", "shoot_stick_x"),
399 ("Move the shooting stick up or down", "shoot_stick_y"),
400 )
401 game = MyGame()
402 window.show_view(JoyConfigView(joy_config_method_names, window.joys, game, SCREEN_WIDTH, SCREEN_HEIGHT))
403 arcade.run()