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"""
 10from __future__ import annotations
 11
 12import math
 13import pprint
 14import random
 15from typing import cast
 16
 17import arcade
 18
 19SCREEN_WIDTH = 1024
 20SCREEN_HEIGHT = 768
 21SCREEN_TITLE = "Dual-stick Shooter Example"
 22MOVEMENT_SPEED = 4
 23BULLET_SPEED = 10
 24BULLET_COOLDOWN_TICKS = 10
 25ENEMY_SPAWN_INTERVAL = 1
 26ENEMY_SPEED = 1
 27STICK_DEADZONE = 0.05
 28# An angle of "0" means "right", but the player's texture is oriented in the "up" direction.
 29# So an offset is needed.
 30ROTATE_OFFSET = -90
 31
 32
 33def dump_obj(obj):
 34    for key in sorted(vars(obj)):
 35        val = getattr(obj, key)
 36        print("{:30} = {} ({})".format(key, val, type(val).__name__))
 37
 38
 39def dump_controller(controller):
 40    print("========== {}".format(controller))
 41    print("Left X        {}".format(controller.leftx))
 42    print("Left Y        {}".format(controller.lefty))
 43    print("Left Trigger  {}".format(controller.lefttrigger))
 44    print("Right X       {}".format(controller.rightx))
 45    print("Right Y       {}".format(controller.righty))
 46    print("Right Trigger {}".format(controller.righttrigger))
 47    print("========== Extra controller")
 48    dump_obj(controller)
 49    print("========== Extra controller.device")
 50    dump_obj(controller.device)
 51    print("========== pprint controller")
 52    pprint.pprint(controller)
 53    print("========== pprint controller.device")
 54    pprint.pprint(controller.device)
 55
 56
 57def dump_controller_state(ticks, controller):
 58    # print("{:5.2f} {:5.2f} {:>20} {:5}_".format(1.234567, -8.2757272903, "hello", str(True)))
 59    fmt_str = "{:6d} "
 60    num_fmts = ["{:5.2f}"] * 6
 61    fmt_str += " ".join(num_fmts)
 62    print(fmt_str.format(ticks,
 63                         controller.leftx,
 64                         controller.lefty,
 65                         controller.lefttrigger,
 66                         controller.rightx,
 67                         controller.righty,
 68                         controller.righttrigger,
 69                         ))
 70
 71
 72def get_stick_position(x, y):
 73    """Given position of stick axes, return (x, y, angle_in_degrees).
 74    If movement is not outside of deadzone, return (None, None, None)"""
 75    if x > STICK_DEADZONE or x < -STICK_DEADZONE or y > STICK_DEADZONE or y < -STICK_DEADZONE:
 76        rad = math.atan2(y, x)
 77        angle = math.degrees(rad)
 78        return x, y, angle
 79    return None, None, None
 80
 81
 82class Player(arcade.sprite.Sprite):
 83    def __init__(self, filename):
 84        super().__init__(filename, scale=0.4, center_x=SCREEN_WIDTH / 2, center_y=SCREEN_HEIGHT / 2)
 85        self.shoot_up_pressed = False
 86        self.shoot_down_pressed = False
 87        self.shoot_left_pressed = False
 88        self.shoot_right_pressed = False
 89        self.start_pressed = False
 90
 91
 92class Enemy(arcade.sprite.Sprite):
 93    def __init__(self, x, y):
 94        super().__init__(':resources:images/pinball/bumper.png', scale=0.5, center_x=x, center_y=y)
 95
 96    def follow_sprite(self, player_sprite):
 97        """
 98        This function will move the current sprite towards whatever
 99        other sprite is specified as a parameter.
100
101        We use the 'min' function here to get the sprite to line up with
102        the target sprite, and not jump around if the sprite is not off
103        an exact multiple of ENEMY_SPEED.
104        """
105
106        if self.center_y < player_sprite.center_y:
107            self.center_y += min(ENEMY_SPEED, player_sprite.center_y - self.center_y)
108        elif self.center_y > player_sprite.center_y:
109            self.center_y -= min(ENEMY_SPEED, self.center_y - player_sprite.center_y)
110
111        if self.center_x < player_sprite.center_x:
112            self.center_x += min(ENEMY_SPEED, player_sprite.center_x - self.center_x)
113        elif self.center_x > player_sprite.center_x:
114            self.center_x -= min(ENEMY_SPEED, self.center_x - player_sprite.center_x)
115
116
117class MyGame(arcade.Window):
118    def __init__(self, width, height, title):
119        super().__init__(width, height, title)
120        self.game_over = False
121        self.score = 0
122        self.tick = 0
123        self.bullet_cooldown = 0
124        self.player = Player(":resources:images/space_shooter/playerShip2_orange.png")
125        self.bullet_list = arcade.SpriteList()
126        self.enemy_list = arcade.SpriteList()
127        self.controller_manager = arcade.ControllerManager()
128        self.controller = None
129
130        self.background_color = arcade.color.DARK_MIDNIGHT_BLUE
131        controllers = self.controller_manager.get_controllers()
132        for controller in controllers:
133            dump_controller(controller)
134        if controllers:
135            self.connect_controller(controllers[0])
136            arcade.window_commands.schedule(self.debug_controller_state, 0.1)
137
138        arcade.window_commands.schedule(self.spawn_enemy, ENEMY_SPAWN_INTERVAL)
139
140        @self.controller_manager.event
141        def on_connect(controller):
142            if not self.controller:
143                self.connect_controller(controller)
144
145        @self.controller_manager.event
146        def on_disconnect(controller):
147            if self.controller == controller:
148                controller.close()
149                self.controller = None
150
151    def setup(self):
152        self.game_over = False
153        self.score = 0
154        self.tick = 0
155        self.bullet_cooldown = 0
156        self.bullet_list = arcade.SpriteList()
157        self.enemy_list = arcade.SpriteList()
158        self.player.center_x = SCREEN_WIDTH / 2
159        self.player.center_y = SCREEN_HEIGHT / 2
160
161    def connect_controller(self, controller):
162        self.controller = controller
163        self.controller.open()
164
165    def debug_controller_state(self, _delta_time):
166        if self.controller:
167            dump_controller_state(self.tick, self.controller)
168
169    def spawn_enemy(self, _elapsed):
170        if self.game_over:
171            return
172        x = random.randint(0, SCREEN_WIDTH)
173        y = random.randint(0, SCREEN_HEIGHT)
174        self.enemy_list.append(Enemy(x, y))
175
176    def on_update(self, delta_time):
177        self.tick += 1
178        if self.game_over:
179            if self.controller:
180                if self.controller.start:
181                    self.setup()
182                    return
183
184            if self.player.start_pressed:
185                self.setup()
186                return
187
188            return
189
190        self.bullet_cooldown += 1
191
192        for enemy in self.enemy_list:
193            cast(Enemy, enemy).follow_sprite(self.player)
194
195        if self.controller:
196            # Controller input - movement
197            move_x, move_y, move_angle = get_stick_position(self.controller.leftx, self.controller.lefty)
198            if move_angle:
199                self.player.change_x = move_x * MOVEMENT_SPEED
200                self.player.change_y = move_y * MOVEMENT_SPEED
201                self.player.angle = move_angle + ROTATE_OFFSET
202            else:
203                self.player.change_x = 0
204                self.player.change_y = 0
205
206            # Controller input - shooting
207            shoot_x, shoot_y, shoot_angle = get_stick_position(self.controller.rightx, self.controller.righty)
208            if shoot_angle:
209                self.spawn_bullet(shoot_angle)
210        else:
211            # Keyboard input - shooting
212            if self.player.shoot_right_pressed and self.player.shoot_up_pressed:
213                self.spawn_bullet(0 + 45)
214            elif self.player.shoot_up_pressed and self.player.shoot_left_pressed:
215                self.spawn_bullet(90 + 45)
216            elif self.player.shoot_left_pressed and self.player.shoot_down_pressed:
217                self.spawn_bullet(180 + 45)
218            elif self.player.shoot_down_pressed and self.player.shoot_right_pressed:
219                self.spawn_bullet(270 + 45)
220            elif self.player.shoot_right_pressed:
221                self.spawn_bullet(0)
222            elif self.player.shoot_up_pressed:
223                self.spawn_bullet(90)
224            elif self.player.shoot_left_pressed:
225                self.spawn_bullet(180)
226            elif self.player.shoot_down_pressed:
227                self.spawn_bullet(270)
228
229        self.enemy_list.update()
230        self.player.update()
231        self.bullet_list.update()
232        ship_death_hit_list = arcade.check_for_collision_with_list(self.player,
233                                                                   self.enemy_list)
234        if len(ship_death_hit_list) > 0:
235            self.game_over = True
236        for bullet in self.bullet_list:
237            bullet_killed = False
238            enemy_shot_list = arcade.check_for_collision_with_list(bullet,
239                                                                   self.enemy_list)
240            # Loop through each colliding sprite, remove it, and add to the score.
241            for enemy in enemy_shot_list:
242                enemy.remove_from_sprite_lists()
243                bullet.remove_from_sprite_lists()
244                bullet_killed = True
245                self.score += 1
246            if bullet_killed:
247                continue
248
249    def on_key_press(self, key, modifiers):
250        if key == arcade.key.W:
251            self.player.change_y = MOVEMENT_SPEED
252        elif key == arcade.key.A:
253            self.player.change_x = -MOVEMENT_SPEED
254        elif key == arcade.key.S:
255            self.player.change_y = -MOVEMENT_SPEED
256        elif key == arcade.key.D:
257            self.player.change_x = MOVEMENT_SPEED
258        elif key == arcade.key.RIGHT:
259            self.player.shoot_right_pressed = True
260        elif key == arcade.key.UP:
261            self.player.shoot_up_pressed = True
262        elif key == arcade.key.LEFT:
263            self.player.shoot_left_pressed = True
264        elif key == arcade.key.DOWN:
265            self.player.shoot_down_pressed = True
266        elif key == arcade.key.ESCAPE:
267            self.player.start_pressed = True
268
269        rad = math.atan2(self.player.change_y, self.player.change_x)
270        self.player.angle = math.degrees(rad) + ROTATE_OFFSET
271
272    def on_key_release(self, key, modifiers):
273        if key == arcade.key.W:
274            self.player.change_y = 0
275        elif key == arcade.key.A:
276            self.player.change_x = 0
277        elif key == arcade.key.S:
278            self.player.change_y = 0
279        elif key == arcade.key.D:
280            self.player.change_x = 0
281        elif key == arcade.key.RIGHT:
282            self.player.shoot_right_pressed = False
283        elif key == arcade.key.UP:
284            self.player.shoot_up_pressed = False
285        elif key == arcade.key.LEFT:
286            self.player.shoot_left_pressed = False
287        elif key == arcade.key.DOWN:
288            self.player.shoot_down_pressed = False
289
290        rad = math.atan2(self.player.change_y, self.player.change_x)
291        self.player.angle = math.degrees(rad) + ROTATE_OFFSET
292
293    def spawn_bullet(self, angle_in_deg):
294        # only allow bullet to spawn on an interval
295        if self.bullet_cooldown < BULLET_COOLDOWN_TICKS:
296            return
297        self.bullet_cooldown = 0
298
299        bullet = arcade.Sprite(":resources:images/space_shooter/laserBlue01.png", scale=0.75)
300
301        # Position the bullet at the player's current location
302        start_x = self.player.center_x
303        start_y = self.player.center_y
304        bullet.center_x = start_x
305        bullet.center_y = start_y
306
307        # angle the bullet visually
308        bullet.angle = angle_in_deg
309        angle_in_rad = math.radians(angle_in_deg)
310
311        # set bullet's movement direction
312        bullet.change_x = math.cos(angle_in_rad) * BULLET_SPEED
313        bullet.change_y = math.sin(angle_in_rad) * BULLET_SPEED
314
315        # Add the bullet to the appropriate lists
316        self.bullet_list.append(bullet)
317
318    def on_draw(self):
319        # clear screen and start render process
320        self.clear()
321
322        # draw game items
323        self.bullet_list.draw()
324        self.enemy_list.draw()
325        self.player.draw()
326
327        # Put the score on the screen.
328        output = f"Score: {self.score}"
329        arcade.draw_text(output, 10, 20, arcade.color.WHITE, 14)
330
331        # Game over message
332        if self.game_over:
333            arcade.draw_text("Game Over",
334                             SCREEN_WIDTH / 2,
335                             SCREEN_HEIGHT / 2,
336                             arcade.color.WHITE, 100,
337                             width=SCREEN_WIDTH,
338                             align="center",
339                             anchor_x="center",
340                             anchor_y="center")
341
342
343if __name__ == "__main__":
344    game = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
345    arcade.run()