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("{: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__(
 83            filename,
 84            scale=0.4,
 85            center_x=WINDOW_WIDTH / 2,
 86            center_y=WINDOW_HEIGHT / 2,
 87        )
 88        self.shoot_up_pressed = False
 89        self.shoot_down_pressed = False
 90        self.shoot_left_pressed = False
 91        self.shoot_right_pressed = False
 92        self.start_pressed = False
 93
 94
 95class Enemy(arcade.sprite.Sprite):
 96    def __init__(self, x, y):
 97        super().__init__(
 98            ':resources:images/pinball/bumper.png',
 99            scale=0.5,
100            center_x=x,
101            center_y=y,
102        )
103
104    def follow_sprite(self, player_sprite):
105        """
106        This function will move the current sprite towards whatever
107        other sprite is specified as a parameter.
108
109        We use the 'min' function here to get the sprite to line up with
110        the target sprite, and not jump around if the sprite is not off
111        an exact multiple of ENEMY_SPEED.
112        """
113
114        if self.center_y < player_sprite.center_y:
115            self.center_y += min(ENEMY_SPEED, player_sprite.center_y - self.center_y)
116        elif self.center_y > player_sprite.center_y:
117            self.center_y -= min(ENEMY_SPEED, self.center_y - player_sprite.center_y)
118
119        if self.center_x < player_sprite.center_x:
120            self.center_x += min(ENEMY_SPEED, player_sprite.center_x - self.center_x)
121        elif self.center_x > player_sprite.center_x:
122            self.center_x -= min(ENEMY_SPEED, self.center_x - player_sprite.center_x)
123
124
125class GameView(arcade.View):
126    def __init__(self):
127        super().__init__()
128        self.game_over = False
129        self.score = 0
130        self.tick = 0
131        self.bullet_cooldown = 0
132        self.player = Player(":resources:images/space_shooter/playerShip2_orange.png")
133        self.bullet_list = arcade.SpriteList()
134        self.enemy_list = arcade.SpriteList()
135        self.controller_manager = arcade.ControllerManager()
136        self.controller = None
137
138        self.background_color = arcade.color.DARK_MIDNIGHT_BLUE
139        controllers = self.controller_manager.get_controllers()
140        for controller in controllers:
141            dump_controller(controller)
142        if controllers:
143            self.connect_controller(controllers[0])
144            arcade.window_commands.schedule(self.debug_controller_state, 0.1)
145
146        arcade.window_commands.schedule(self.spawn_enemy, ENEMY_SPAWN_INTERVAL)
147
148        @self.controller_manager.event
149        def on_connect(controller):
150            if not self.controller:
151                self.connect_controller(controller)
152
153        @self.controller_manager.event
154        def on_disconnect(controller):
155            if self.controller == controller:
156                controller.close()
157                self.controller = None
158
159    def setup(self):
160        self.game_over = False
161        self.score = 0
162        self.tick = 0
163        self.bullet_cooldown = 0
164        self.bullet_list = arcade.SpriteList()
165        self.enemy_list = arcade.SpriteList()
166        self.player.center_x = WINDOW_WIDTH / 2
167        self.player.center_y = WINDOW_HEIGHT / 2
168
169    def connect_controller(self, controller):
170        self.controller = controller
171        self.controller.open()
172
173    def debug_controller_state(self, _delta_time):
174        if self.controller:
175            dump_controller_state(self.tick, self.controller)
176
177    def spawn_enemy(self, _elapsed):
178        if self.game_over:
179            return
180        x = random.randint(0, WINDOW_WIDTH)
181        y = random.randint(0, WINDOW_HEIGHT)
182        self.enemy_list.append(Enemy(x, y))
183
184    def on_update(self, delta_time):
185        self.tick += 1
186        if self.game_over:
187            if self.controller:
188                if self.controller.start:
189                    self.setup()
190                    return
191
192            if self.player.start_pressed:
193                self.setup()
194                return
195
196            return
197
198        self.bullet_cooldown += 1
199
200        for enemy in self.enemy_list:
201            cast(Enemy, enemy).follow_sprite(self.player)
202
203        if self.controller:
204            # Controller input - movement
205            move_x, move_y, move_angle = get_stick_position(
206                self.controller.leftx, self.controller.lefty
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                self.controller.rightx, self.controller.righty
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        elif key == arcade.key.ESCAPE:
279            self.player.start_pressed = True
280        # close the window if the user hits the escape key
281        elif key == arcade.key.ESCAPE:
282            self.close()
283
284        rad = math.atan2(self.player.change_y, self.player.change_x)
285        self.player.angle = math.degrees(rad) + ROTATE_OFFSET
286
287    def on_key_release(self, key, modifiers):
288        if key == arcade.key.W:
289            self.player.change_y = 0
290        elif key == arcade.key.A:
291            self.player.change_x = 0
292        elif key == arcade.key.S:
293            self.player.change_y = 0
294        elif key == arcade.key.D:
295            self.player.change_x = 0
296        elif key == arcade.key.RIGHT:
297            self.player.shoot_right_pressed = False
298        elif key == arcade.key.UP:
299            self.player.shoot_up_pressed = False
300        elif key == arcade.key.LEFT:
301            self.player.shoot_left_pressed = False
302        elif key == arcade.key.DOWN:
303            self.player.shoot_down_pressed = False
304
305        rad = math.atan2(self.player.change_y, self.player.change_x)
306        self.player.angle = math.degrees(rad) + ROTATE_OFFSET
307
308    def spawn_bullet(self, angle_in_deg):
309        # only allow bullet to spawn on an interval
310        if self.bullet_cooldown < BULLET_COOLDOWN_TICKS:
311            return
312        self.bullet_cooldown = 0
313
314        bullet = arcade.Sprite(":resources:images/space_shooter/laserBlue01.png", scale=0.75)
315
316        # Position the bullet at the player's current location
317        start_x = self.player.center_x
318        start_y = self.player.center_y
319        bullet.center_x = start_x
320        bullet.center_y = start_y
321
322        # angle the bullet visually
323        bullet.angle = angle_in_deg
324        angle_in_rad = math.radians(angle_in_deg)
325
326        # set bullet's movement direction
327        bullet.change_x = math.cos(angle_in_rad) * BULLET_SPEED
328        bullet.change_y = math.sin(angle_in_rad) * BULLET_SPEED
329
330        # Add the bullet to the appropriate lists
331        self.bullet_list.append(bullet)
332
333    def on_draw(self):
334        # clear screen and start render process
335        self.clear()
336
337        # draw game items
338        self.bullet_list.draw()
339        self.enemy_list.draw()
340        arcade.draw_sprite(self.player)
341
342        # Put the score on the screen.
343        output = f"Score: {self.score}"
344        arcade.draw_text(output, 10, 20, arcade.color.WHITE, 14)
345
346        # Game over message
347        if self.game_over:
348            arcade.draw_text("Game Over",
349                             WINDOW_WIDTH / 2,
350                             WINDOW_HEIGHT / 2,
351                             arcade.color.WHITE, 100,
352                             width=WINDOW_WIDTH,
353                             align="center",
354                             anchor_x="center",
355                             anchor_y="center")
356
357
358
359def main():
360    """ Main function """
361    # Create a window class. This is what actually shows up on screen
362    window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE)
363
364    # Create and setup the GameView
365    game = GameView()
366
367    # Show GameView on screen
368    window.show_view(game)
369
370    # Start the arcade game loop
371    arcade.run()
372
373
374if __name__ == "__main__":
375    main()