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        # 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()