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
 17SCREEN_WIDTH = 1024
 18SCREEN_HEIGHT = 768
 19SCREEN_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__(filename, scale=0.4, center_x=SCREEN_WIDTH / 2, center_y=SCREEN_HEIGHT / 2)
 83        self.shoot_up_pressed = False
 84        self.shoot_down_pressed = False
 85        self.shoot_left_pressed = False
 86        self.shoot_right_pressed = False
 87        self.start_pressed = False
 88
 89
 90class Enemy(arcade.sprite.Sprite):
 91    def __init__(self, x, y):
 92        super().__init__(':resources:images/pinball/bumper.png', scale=0.5, center_x=x, center_y=y)
 93
 94    def follow_sprite(self, player_sprite):
 95        """
 96        This function will move the current sprite towards whatever
 97        other sprite is specified as a parameter.
 98
 99        We use the 'min' function here to get the sprite to line up with
100        the target sprite, and not jump around if the sprite is not off
101        an exact multiple of ENEMY_SPEED.
102        """
103
104        if self.center_y < player_sprite.center_y:
105            self.center_y += min(ENEMY_SPEED, player_sprite.center_y - self.center_y)
106        elif self.center_y > player_sprite.center_y:
107            self.center_y -= min(ENEMY_SPEED, self.center_y - player_sprite.center_y)
108
109        if self.center_x < player_sprite.center_x:
110            self.center_x += min(ENEMY_SPEED, player_sprite.center_x - self.center_x)
111        elif self.center_x > player_sprite.center_x:
112            self.center_x -= min(ENEMY_SPEED, self.center_x - player_sprite.center_x)
113
114
115class MyGame(arcade.Window):
116    def __init__(self, width, height, title):
117        super().__init__(width, height, title)
118        self.game_over = False
119        self.score = 0
120        self.tick = 0
121        self.bullet_cooldown = 0
122        self.player = Player(":resources:images/space_shooter/playerShip2_orange.png")
123        self.bullet_list = arcade.SpriteList()
124        self.enemy_list = arcade.SpriteList()
125        self.controller_manager = arcade.ControllerManager()
126        self.controller = None
127
128        self.background_color = arcade.color.DARK_MIDNIGHT_BLUE
129        controllers = self.controller_manager.get_controllers()
130        for controller in controllers:
131            dump_controller(controller)
132        if controllers:
133            self.connect_controller(controllers[0])
134            arcade.window_commands.schedule(self.debug_controller_state, 0.1)
135
136        arcade.window_commands.schedule(self.spawn_enemy, ENEMY_SPAWN_INTERVAL)
137
138        @self.controller_manager.event
139        def on_connect(controller):
140            if not self.controller:
141                self.connect_controller(controller)
142
143        @self.controller_manager.event
144        def on_disconnect(controller):
145            if self.controller == controller:
146                controller.close()
147                self.controller = None
148
149    def setup(self):
150        self.game_over = False
151        self.score = 0
152        self.tick = 0
153        self.bullet_cooldown = 0
154        self.bullet_list = arcade.SpriteList()
155        self.enemy_list = arcade.SpriteList()
156        self.player.center_x = SCREEN_WIDTH / 2
157        self.player.center_y = SCREEN_HEIGHT / 2
158
159    def connect_controller(self, controller):
160        self.controller = controller
161        self.controller.open()
162
163    def debug_controller_state(self, _delta_time):
164        if self.controller:
165            dump_controller_state(self.tick, self.controller)
166
167    def spawn_enemy(self, _elapsed):
168        if self.game_over:
169            return
170        x = random.randint(0, SCREEN_WIDTH)
171        y = random.randint(0, SCREEN_HEIGHT)
172        self.enemy_list.append(Enemy(x, y))
173
174    def on_update(self, delta_time):
175        self.tick += 1
176        if self.game_over:
177            if self.controller:
178                if self.controller.start:
179                    self.setup()
180                    return
181
182            if self.player.start_pressed:
183                self.setup()
184                return
185
186            return
187
188        self.bullet_cooldown += 1
189
190        for enemy in self.enemy_list:
191            cast(Enemy, enemy).follow_sprite(self.player)
192
193        if self.controller:
194            # Controller input - movement
195            move_x, move_y, move_angle = get_stick_position(self.controller.leftx, self.controller.lefty)
196            if move_angle:
197                self.player.change_x = move_x * MOVEMENT_SPEED
198                self.player.change_y = move_y * MOVEMENT_SPEED
199                self.player.angle = move_angle + ROTATE_OFFSET
200            else:
201                self.player.change_x = 0
202                self.player.change_y = 0
203
204            # Controller input - shooting
205            shoot_x, shoot_y, shoot_angle = get_stick_position(self.controller.rightx, self.controller.righty)
206            if shoot_angle:
207                self.spawn_bullet(shoot_angle)
208        else:
209            # Keyboard input - shooting
210            if self.player.shoot_right_pressed and self.player.shoot_up_pressed:
211                self.spawn_bullet(0 + 45)
212            elif self.player.shoot_up_pressed and self.player.shoot_left_pressed:
213                self.spawn_bullet(90 + 45)
214            elif self.player.shoot_left_pressed and self.player.shoot_down_pressed:
215                self.spawn_bullet(180 + 45)
216            elif self.player.shoot_down_pressed and self.player.shoot_right_pressed:
217                self.spawn_bullet(270 + 45)
218            elif self.player.shoot_right_pressed:
219                self.spawn_bullet(0)
220            elif self.player.shoot_up_pressed:
221                self.spawn_bullet(90)
222            elif self.player.shoot_left_pressed:
223                self.spawn_bullet(180)
224            elif self.player.shoot_down_pressed:
225                self.spawn_bullet(270)
226
227        self.enemy_list.update()
228        self.player.update()
229        self.bullet_list.update()
230        ship_death_hit_list = arcade.check_for_collision_with_list(self.player,
231                                                                   self.enemy_list)
232        if len(ship_death_hit_list) > 0:
233            self.game_over = True
234        for bullet in self.bullet_list:
235            bullet_killed = False
236            enemy_shot_list = arcade.check_for_collision_with_list(bullet,
237                                                                   self.enemy_list)
238            # Loop through each colliding sprite, remove it, and add to the score.
239            for enemy in enemy_shot_list:
240                enemy.remove_from_sprite_lists()
241                bullet.remove_from_sprite_lists()
242                bullet_killed = True
243                self.score += 1
244            if bullet_killed:
245                continue
246
247    def on_key_press(self, key, modifiers):
248        if key == arcade.key.W:
249            self.player.change_y = MOVEMENT_SPEED
250        elif key == arcade.key.A:
251            self.player.change_x = -MOVEMENT_SPEED
252        elif key == arcade.key.S:
253            self.player.change_y = -MOVEMENT_SPEED
254        elif key == arcade.key.D:
255            self.player.change_x = MOVEMENT_SPEED
256        elif key == arcade.key.RIGHT:
257            self.player.shoot_right_pressed = True
258        elif key == arcade.key.UP:
259            self.player.shoot_up_pressed = True
260        elif key == arcade.key.LEFT:
261            self.player.shoot_left_pressed = True
262        elif key == arcade.key.DOWN:
263            self.player.shoot_down_pressed = True
264        elif key == arcade.key.ESCAPE:
265            self.player.start_pressed = True
266
267        rad = math.atan2(self.player.change_y, self.player.change_x)
268        self.player.angle = math.degrees(rad) + ROTATE_OFFSET
269
270    def on_key_release(self, key, modifiers):
271        if key == arcade.key.W:
272            self.player.change_y = 0
273        elif key == arcade.key.A:
274            self.player.change_x = 0
275        elif key == arcade.key.S:
276            self.player.change_y = 0
277        elif key == arcade.key.D:
278            self.player.change_x = 0
279        elif key == arcade.key.RIGHT:
280            self.player.shoot_right_pressed = False
281        elif key == arcade.key.UP:
282            self.player.shoot_up_pressed = False
283        elif key == arcade.key.LEFT:
284            self.player.shoot_left_pressed = False
285        elif key == arcade.key.DOWN:
286            self.player.shoot_down_pressed = False
287
288        rad = math.atan2(self.player.change_y, self.player.change_x)
289        self.player.angle = math.degrees(rad) + ROTATE_OFFSET
290
291    def spawn_bullet(self, angle_in_deg):
292        # only allow bullet to spawn on an interval
293        if self.bullet_cooldown < BULLET_COOLDOWN_TICKS:
294            return
295        self.bullet_cooldown = 0
296
297        bullet = arcade.Sprite(":resources:images/space_shooter/laserBlue01.png", scale=0.75)
298
299        # Position the bullet at the player's current location
300        start_x = self.player.center_x
301        start_y = self.player.center_y
302        bullet.center_x = start_x
303        bullet.center_y = start_y
304
305        # angle the bullet visually
306        bullet.angle = angle_in_deg
307        angle_in_rad = math.radians(angle_in_deg)
308
309        # set bullet's movement direction
310        bullet.change_x = math.cos(angle_in_rad) * BULLET_SPEED
311        bullet.change_y = math.sin(angle_in_rad) * BULLET_SPEED
312
313        # Add the bullet to the appropriate lists
314        self.bullet_list.append(bullet)
315
316    def on_draw(self):
317        # clear screen and start render process
318        self.clear()
319
320        # draw game items
321        self.bullet_list.draw()
322        self.enemy_list.draw()
323        self.player.draw()
324
325        # Put the score on the screen.
326        output = f"Score: {self.score}"
327        arcade.draw_text(output, 10, 20, arcade.color.WHITE, 14)
328
329        # Game over message
330        if self.game_over:
331            arcade.draw_text("Game Over",
332                             SCREEN_WIDTH / 2,
333                             SCREEN_HEIGHT / 2,
334                             arcade.color.WHITE, 100,
335                             width=SCREEN_WIDTH,
336                             align="center",
337                             anchor_x="center",
338                             anchor_y="center")
339
340
341
342def main():
343    game = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
344    game.run()
345
346
347if __name__ == "__main__":
348    main()