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.

Screenshot of dual-stick shooter example
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()