311 lines
11 KiB
Python
311 lines
11 KiB
Python
"""
|
|
Dual-stick Shooter Example
|
|
|
|
A dual-analog stick joystick is the preferred method of input. If a joystick is
|
|
not present, the game will fail back to use keyboard controls (WASD to move, arrows to shoot)
|
|
|
|
If Python and Arcade are installed, this example can be run from the command line with:
|
|
python -m arcade.examples.dual_stick_shooter
|
|
"""
|
|
import arcade
|
|
import random
|
|
import math
|
|
import os
|
|
from typing import cast
|
|
import pprint
|
|
|
|
SCREEN_WIDTH = 1024
|
|
SCREEN_HEIGHT = 768
|
|
SCREEN_TITLE = "Dual-stick Shooter Example"
|
|
MOVEMENT_SPEED = 4
|
|
BULLET_SPEED = 10
|
|
BULLET_COOLDOWN_TICKS = 10
|
|
ENEMY_SPAWN_INTERVAL = 1
|
|
ENEMY_SPEED = 1
|
|
JOY_DEADZONE = 0.2
|
|
|
|
|
|
def dump_obj(obj):
|
|
for key in sorted(vars(obj)):
|
|
val = getattr(obj, key)
|
|
print("{:30} = {} ({})".format(key, val, type(val).__name__))
|
|
|
|
|
|
def dump_joystick(joy):
|
|
print("========== {}".format(joy))
|
|
print("x {}".format(joy.x))
|
|
print("y {}".format(joy.y))
|
|
print("z {}".format(joy.z))
|
|
print("rx {}".format(joy.rx))
|
|
print("ry {}".format(joy.ry))
|
|
print("rz {}".format(joy.rz))
|
|
print("hat_x {}".format(joy.hat_x))
|
|
print("hat_y {}".format(joy.hat_y))
|
|
print("buttons {}".format(joy.buttons))
|
|
print("========== Extra joy")
|
|
dump_obj(joy)
|
|
print("========== Extra joy.device")
|
|
dump_obj(joy.device)
|
|
print("========== pprint joy")
|
|
pprint.pprint(joy)
|
|
print("========== pprint joy.device")
|
|
pprint.pprint(joy.device)
|
|
|
|
|
|
def dump_joystick_state(ticks, joy):
|
|
# print("{:5.2f} {:5.2f} {:>20} {:5}_".format(1.234567, -8.2757272903, "hello", str(True)))
|
|
fmt_str = "{:6d} "
|
|
num_fmts = ["{:5.2f}"] * 6
|
|
fmt_str += " ".join(num_fmts)
|
|
fmt_str += " {:2d} {:2d} {}"
|
|
buttons = " ".join(["{:5}".format(str(b)) for b in joy.buttons])
|
|
print(fmt_str.format(ticks,
|
|
joy.x,
|
|
joy.y,
|
|
joy.z,
|
|
joy.rx,
|
|
joy.ry,
|
|
joy.rz,
|
|
joy.hat_x,
|
|
joy.hat_y,
|
|
buttons))
|
|
|
|
|
|
def get_joy_position(x, y):
|
|
"""Given position of joystick axes, return (x, y, angle_in_degrees).
|
|
If movement is not outside of deadzone, return (None, None, None)"""
|
|
if x > JOY_DEADZONE or x < -JOY_DEADZONE or y > JOY_DEADZONE or y < -JOY_DEADZONE:
|
|
y = -y
|
|
rad = math.atan2(y, x)
|
|
angle = math.degrees(rad)
|
|
return x, y, angle
|
|
return None, None, None
|
|
|
|
|
|
class Player(arcade.sprite.Sprite):
|
|
def __init__(self, filename):
|
|
super().__init__(filename=filename, scale=0.4, center_x=SCREEN_WIDTH/2, center_y=SCREEN_HEIGHT/2)
|
|
self.shoot_up_pressed = False
|
|
self.shoot_down_pressed = False
|
|
self.shoot_left_pressed = False
|
|
self.shoot_right_pressed = False
|
|
|
|
|
|
class Enemy(arcade.sprite.Sprite):
|
|
def __init__(self, x, y):
|
|
super().__init__(filename='images/bumper.png', scale=0.5, center_x=x, center_y=y)
|
|
|
|
def follow_sprite(self, player_sprite):
|
|
"""
|
|
This function will move the current sprite towards whatever
|
|
other sprite is specified as a parameter.
|
|
|
|
We use the 'min' function here to get the sprite to line up with
|
|
the target sprite, and not jump around if the sprite is not off
|
|
an exact multiple of ENEMY_SPEED.
|
|
"""
|
|
|
|
if self.center_y < player_sprite.center_y:
|
|
self.center_y += min(ENEMY_SPEED, player_sprite.center_y - self.center_y)
|
|
elif self.center_y > player_sprite.center_y:
|
|
self.center_y -= min(ENEMY_SPEED, self.center_y - player_sprite.center_y)
|
|
|
|
if self.center_x < player_sprite.center_x:
|
|
self.center_x += min(ENEMY_SPEED, player_sprite.center_x - self.center_x)
|
|
elif self.center_x > player_sprite.center_x:
|
|
self.center_x -= min(ENEMY_SPEED, self.center_x - player_sprite.center_x)
|
|
|
|
|
|
class MyGame(arcade.Window):
|
|
def __init__(self, width, height, title):
|
|
super().__init__(width, height, title)
|
|
|
|
# Set the working directory (where we expect to find files) to the same
|
|
# directory this .py file is in. You can leave this out of your own
|
|
# code, but it is needed to easily run the examples using "python -m"
|
|
# as mentioned at the top of this program.
|
|
file_path = os.path.dirname(os.path.abspath(__file__))
|
|
os.chdir(file_path)
|
|
|
|
arcade.set_background_color(arcade.color.DARK_MIDNIGHT_BLUE)
|
|
self.game_over = False
|
|
self.score = 0
|
|
self.tick = 0
|
|
self.bullet_cooldown = 0
|
|
self.player = Player("images/playerShip2_orange.png")
|
|
self.bullet_list = arcade.SpriteList()
|
|
self.enemy_list = arcade.SpriteList()
|
|
self.joy = None
|
|
joys = arcade.get_joysticks()
|
|
for joy in joys:
|
|
dump_joystick(joy)
|
|
if joys:
|
|
self.joy = joys[0]
|
|
self.joy.open()
|
|
print("Using joystick controls: {}".format(self.joy.device))
|
|
arcade.window_commands.schedule(self.debug_joy_state, 0.1)
|
|
if not self.joy:
|
|
print("No joystick present, using keyboard controls")
|
|
arcade.window_commands.schedule(self.spawn_enemy, ENEMY_SPAWN_INTERVAL)
|
|
|
|
def debug_joy_state(self, delta_time):
|
|
dump_joystick_state(self.tick, self.joy)
|
|
|
|
def spawn_enemy(self, elapsed):
|
|
if self.game_over:
|
|
return
|
|
x = random.randint(0, SCREEN_WIDTH)
|
|
y = random.randint(0, SCREEN_HEIGHT)
|
|
self.enemy_list.append(Enemy(x, y))
|
|
|
|
def update(self, delta_time):
|
|
self.tick += 1
|
|
if self.game_over:
|
|
return
|
|
|
|
self.bullet_cooldown += 1
|
|
|
|
for enemy in self.enemy_list:
|
|
cast(Enemy, enemy).follow_sprite(self.player)
|
|
|
|
if self.joy:
|
|
# Joystick input - movement
|
|
move_x, move_y, move_angle = get_joy_position(self.joy.x, self.joy.y)
|
|
if move_angle:
|
|
self.player.change_x = move_x * MOVEMENT_SPEED
|
|
self.player.change_y = move_y * MOVEMENT_SPEED
|
|
# An angle of "0" means "right", but the player's image is drawn in the "up" direction.
|
|
# So an offset is needed.
|
|
self.player.angle = move_angle - 90
|
|
else:
|
|
self.player.change_x = 0
|
|
self.player.change_y = 0
|
|
# Joystick input - shooting
|
|
shoot_x, shoot_y, shoot_angle = get_joy_position(self.joy.z, self.joy.rz)
|
|
if shoot_angle:
|
|
self.spawn_bullet(shoot_angle)
|
|
else:
|
|
# Keyboard input - shooting
|
|
if self.player.shoot_right_pressed and self.player.shoot_up_pressed:
|
|
self.spawn_bullet(0+45)
|
|
elif self.player.shoot_up_pressed and self.player.shoot_left_pressed:
|
|
self.spawn_bullet(90+45)
|
|
elif self.player.shoot_left_pressed and self.player.shoot_down_pressed:
|
|
self.spawn_bullet(180+45)
|
|
elif self.player.shoot_down_pressed and self.player.shoot_right_pressed:
|
|
self.spawn_bullet(270+45)
|
|
elif self.player.shoot_right_pressed:
|
|
self.spawn_bullet(0)
|
|
elif self.player.shoot_up_pressed:
|
|
self.spawn_bullet(90)
|
|
elif self.player.shoot_left_pressed:
|
|
self.spawn_bullet(180)
|
|
elif self.player.shoot_down_pressed:
|
|
self.spawn_bullet(270)
|
|
|
|
self.enemy_list.update()
|
|
self.player.update()
|
|
self.bullet_list.update()
|
|
ship_death_hit_list = arcade.check_for_collision_with_list(self.player, self.enemy_list)
|
|
if len(ship_death_hit_list) > 0:
|
|
self.game_over = True
|
|
for bullet in self.bullet_list:
|
|
bullet_killed = False
|
|
enemy_shot_list = arcade.check_for_collision_with_list(bullet, self.enemy_list)
|
|
# Loop through each colliding sprite, remove it, and add to the score.
|
|
for enemy in enemy_shot_list:
|
|
enemy.kill()
|
|
bullet.kill()
|
|
bullet_killed = True
|
|
self.score += 1
|
|
if bullet_killed:
|
|
continue
|
|
|
|
def on_key_press(self, key, modifiers):
|
|
if key == arcade.key.W:
|
|
self.player.change_y = MOVEMENT_SPEED
|
|
self.player.angle = 0
|
|
elif key == arcade.key.A:
|
|
self.player.change_x = -MOVEMENT_SPEED
|
|
self.player.angle = 90
|
|
elif key == arcade.key.S:
|
|
self.player.change_y = -MOVEMENT_SPEED
|
|
self.player.angle = 180
|
|
elif key == arcade.key.D:
|
|
self.player.change_x = MOVEMENT_SPEED
|
|
self.player.angle = 270
|
|
elif key == arcade.key.RIGHT:
|
|
self.player.shoot_right_pressed = True
|
|
elif key == arcade.key.UP:
|
|
self.player.shoot_up_pressed = True
|
|
elif key == arcade.key.LEFT:
|
|
self.player.shoot_left_pressed = True
|
|
elif key == arcade.key.DOWN:
|
|
self.player.shoot_down_pressed = True
|
|
|
|
def on_key_release(self, key, modifiers):
|
|
if key == arcade.key.W:
|
|
self.player.change_y = 0
|
|
elif key == arcade.key.A:
|
|
self.player.change_x = 0
|
|
elif key == arcade.key.S:
|
|
self.player.change_y = 0
|
|
elif key == arcade.key.D:
|
|
self.player.change_x = 0
|
|
elif key == arcade.key.RIGHT:
|
|
self.player.shoot_right_pressed = False
|
|
elif key == arcade.key.UP:
|
|
self.player.shoot_up_pressed = False
|
|
elif key == arcade.key.LEFT:
|
|
self.player.shoot_left_pressed = False
|
|
elif key == arcade.key.DOWN:
|
|
self.player.shoot_down_pressed = False
|
|
|
|
def spawn_bullet(self, angle_in_deg):
|
|
# only allow bullet to spawn on an interval
|
|
if self.bullet_cooldown < BULLET_COOLDOWN_TICKS:
|
|
return
|
|
self.bullet_cooldown = 0
|
|
|
|
bullet = arcade.Sprite("images/laserBlue01.png", 0.75)
|
|
|
|
# Position the bullet at the player's current location
|
|
start_x = self.player.center_x
|
|
start_y = self.player.center_y
|
|
bullet.center_x = start_x
|
|
bullet.center_y = start_y
|
|
|
|
# angle the bullet visually
|
|
bullet.angle = angle_in_deg
|
|
angle_in_rad = math.radians(angle_in_deg)
|
|
|
|
# set bullet's movement direction
|
|
bullet.change_x = math.cos(angle_in_rad) * BULLET_SPEED
|
|
bullet.change_y = math.sin(angle_in_rad) * BULLET_SPEED
|
|
|
|
# Add the bullet to the appropriate lists
|
|
self.bullet_list.append(bullet)
|
|
|
|
def on_draw(self):
|
|
# clear screen and start render process
|
|
arcade.start_render()
|
|
|
|
# draw game items
|
|
self.bullet_list.draw()
|
|
self.enemy_list.draw()
|
|
self.player.draw()
|
|
|
|
# Put the score on the screen.
|
|
output = f"Score: {self.score}"
|
|
arcade.draw_text(output, 10, 20, arcade.color.WHITE, 14)
|
|
|
|
# Game over message
|
|
if self.game_over:
|
|
arcade.draw_text("Game Over", SCREEN_WIDTH/2, SCREEN_HEIGHT/2, arcade.color.WHITE, 100, width=SCREEN_WIDTH,
|
|
align="center", anchor_x="center", anchor_y="center")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
game = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
|
|
arcade.run()
|