In our last two mini-projects, we will build a 2D space game RiceRocks that is inspired by the classic arcade game Asteroids (1979). Asteroids is a relatively simple game by today’s standards, but was still immensely popular during its time. (Joe spent countless quarters playing it.) In the game, the player controls a spaceship via four buttons: two buttons that rotate the spaceship clockwise or counterclockwise (independent of its current velocity), a thrust button that accelerates the ship in its forward direction and a fire button that shoots missiles. Large asteroids spawn randomly on the screen with random velocities. The player’s goal is to destroy these asteroids before they strike the player’s ship. In the arcade version, a large rock hit by a missile split into several fast moving small asteroids that themselves must be destroyed. Occasionally, a flying saucer also crosses the screen and attempts to destroy the player’s spaceship. Searching for “asteroids arcade” yields links to multiple versions of Asteroids that are available on the web (including an updated version by Atari, the original creator of Asteroids).
For this mini-project, you will implement a working spaceship plus add a single asteroid and a single missile. We have provided art for your game so its look and feel is that of a more modern game. You should begin by loading the program template.The program template includes all necessary image and audio files. Unfortunately, no audio format is supported by all major browsers so we have decided to provided sounds in the mp3
format which is supported by Chrome (but not by Firefox on some systems). (ogg
versions are also available.) We highly recommend using Chrome for the last two weeks of the class. We have found that Chrome typically has better performance on games with more substantial drawing requirements and standardization on a common browser will make peer assessing projects more reliable.
In this phase, you will implement the control scheme for the spaceship.This includes a complete Spaceship class and the appropriate keyboard handlers to control the spaceship. Your spaceship should behave as follows:
angle_to_vector
. You will need to experiment with scaling each component of this acceleration vector to generate a reasonable acceleration.Now, implement these behaviors above in order. Each step should require just a few lines of code. Here are some hints:
You should now have a ship that flies around the screen,as you would like for RiceRocks. Adjust the constants as you would like to get it to fly how you want.
To implement rocks, we will use the provided Sprite class. Note that the update method for the sprite will be very similar to the update method for the ship. The primary difference is that the ship’s velocity and rotation are controlled by keys, whereas sprites have these set randomly when they are created. Rocks should screen wrap in the same manner as the ship.
In the template, the global variable a_rock
is created at the start with zero velocity. Instead, we want to create version of a_rock
once every second in the timer handler. Next week, we will add multiple rocks. This week, the ship will not die if it hits a rock. We’ll add that next week. To implement rocks, we suggest the following:
a_rock
different starting parameters and ensuring it behaves as you expect.rock_spawner
. In particular, set a_rock
to be a new rock on every tick. (Don’t forget to declare a_rock
as a global in the timer handler.) Choose a velocity, position, and angular velocity randomly for the rock. You will want to tweak the ranges of these random numbers, as that will affect how fun the game is to play. Make sure you generated rocks that spin in both directions and, likewise, move in all directions.To implement missiles, we will use the same sprite class as for rocks. Missiles will always have a zero angular velocity. They will also have a lifespan (they should disappear after a certain amount of time or you will eventually have missiles all over the place), but we will ignore that this week. Also, for now, we will only allow a single missile and it will not yet blow up rocks. We’ll add more next week.
Your missile should be created when you press the spacebar, not on a timer like rocks. They should screen wrap just as the ship and rocks do. Otherwise, the process is very similar:
shoot
method to your ship class. This should spawn a new missile (for now just replace the old missile in a_missile
). The missile’s initial position should be the tip of your ship’s “cannon”. Its velocity should be the sum of the ship’s velocity and a multiple of the ship’s forward vector.Our user interface for RiceRocks simply shows the number of lives remaining and the score. This week neither of those elements ever change, but they will next week. Add code to the draw event handler to draw these on the canvas. Use the lives
and score
global variables as the current lives remaining and score.
# program template for Spaceship
import simplegui
import math
import random
# globals for user interface
WIDTH = 800
HEIGHT = 600
score = 0
lives = 3
time = 0.5
class ImageInfo:
def __init__(self, center, size, radius = 0, lifespan = None, animated = False):
self.center = center
self.size = size
self.radius = radius
if lifespan:
self.lifespan = lifespan
else:
self.lifespan = float('inf')
self.animated = animated
def get_center(self):
return self.center
def get_size(self):
return self.size
def get_radius(self):
return self.radius
def get_lifespan(self):
return self.lifespan
def get_animated(self):
return self.animated
# art assets created by Kim Lathrop, may be freely re-used in non-commercial projects, please credit Kim
# debris images - debris1_brown.png, debris2_brown.png, debris3_brown.png, debris4_brown.png
# debris1_blue.png, debris2_blue.png, debris3_blue.png, debris4_blue.png, debris_blend.png
debris_info = ImageInfo([320, 240], [640, 480])
debris_image = simplegui.load_image("http://commondatastorage.googleapis.com/codeskulptor-assets/lathrop/debris2_blue.png")
# nebula images - nebula_brown.png, nebula_blue.png
nebula_info = ImageInfo([400, 300], [800, 600])
nebula_image = simplegui.load_image("http://commondatastorage.googleapis.com/codeskulptor-assets/lathrop/nebula_blue.f2014.png")
# splash image
splash_info = ImageInfo([200, 150], [400, 300])
splash_image = simplegui.load_image("http://commondatastorage.googleapis.com/codeskulptor-assets/lathrop/splash.png")
# ship image
ship_info = ImageInfo([45, 45], [90, 90], 35)
ship_image = simplegui.load_image("http://commondatastorage.googleapis.com/codeskulptor-assets/lathrop/double_ship.png")
# missile image - shot1.png, shot2.png, shot3.png
missile_info = ImageInfo([5,5], [10, 10], 3, 50)
missile_image = simplegui.load_image("http://commondatastorage.googleapis.com/codeskulptor-assets/lathrop/shot2.png")
# asteroid images - asteroid_blue.png, asteroid_brown.png, asteroid_blend.png
asteroid_info = ImageInfo([45, 45], [90, 90], 40)
asteroid_image = simplegui.load_image("http://commondatastorage.googleapis.com/codeskulptor-assets/lathrop/asteroid_blue.png")
# animated explosion - explosion_orange.png, explosion_blue.png, explosion_blue2.png, explosion_alpha.png
explosion_info = ImageInfo([64, 64], [128, 128], 17, 24, True)
explosion_image = simplegui.load_image("http://commondatastorage.googleapis.com/codeskulptor-assets/lathrop/explosion_alpha.png")
# sound assets purchased from sounddogs.com, please do not redistribute
soundtrack = simplegui.load_sound("http://commondatastorage.googleapis.com/codeskulptor-assets/sounddogs/soundtrack.mp3")
missile_sound = simplegui.load_sound("http://commondatastorage.googleapis.com/codeskulptor-assets/sounddogs/missile.mp3")
missile_sound.set_volume(.5)
ship_thrust_sound = simplegui.load_sound("http://commondatastorage.googleapis.com/codeskulptor-assets/sounddogs/thrust.mp3")
explosion_sound = simplegui.load_sound("http://commondatastorage.googleapis.com/codeskulptor-assets/sounddogs/explosion.mp3")
# helper functions to handle transformations
def angle_to_vector(ang):
return [math.cos(ang), math.sin(ang)]
def dist(p,q):
return math.sqrt((p[0] - q[0]) ** 2 + (p[1] - q[1]) ** 2)
# Ship class
class Ship:
def __init__(self, pos, vel, angle, image, info):
self.pos = [pos[0],pos[1]]
self.vel = [vel[0],vel[1]]
self.thrust = False
self.angle = angle
self.angle_vel = 0
self.image = image
self.image_center = info.get_center()
self.image_size = info.get_size()
self.radius = info.get_radius()
def draw(self,canvas):
if self.thrust:
self.image_center[0] = 135
else:
self.image_center[0] = 45
canvas.draw_image(self.image, self.image_center, self.image_size, self.pos, self.image_size, self.angle)
def update(self):
self.vel[0] *= 0.96
self.vel[1] *= 0.96
self.pos[0] += self.vel[0]
self.pos[0] %= WIDTH
self.pos[1] += self.vel[1]
self.pos[1] %= HEIGHT
self.angle += self.angle_vel
if self.thrust:
ship_thrust_sound.play()
else:
ship_thrust_sound.rewind()
if self.thrust:
foward = angle_to_vector(self.angle)
self.vel[0] += foward[0] / 5
self.vel[1] += foward[1] / 5
def shoot(self):
global a_missile
foward = angle_to_vector(self.angle)
tmppos = [self.pos[0] + 45 * foward[0], self.pos[1] + 45 * foward[1]]
tmpvel = [self.vel[0] + foward[0] * 3, self.vel[1] + foward[1] * 3]
a_missile = Sprite(tmppos, tmpvel, 0, 0, missile_image, missile_info, missile_sound)
missile_sound.play()
# Sprite class
class Sprite:
def __init__(self, pos, vel, ang, ang_vel, image, info, sound = None):
self.pos = [pos[0],pos[1]]
self.vel = [vel[0],vel[1]]
self.angle = ang
self.angle_vel = ang_vel
self.image = image
self.image_center = info.get_center()
self.image_size = info.get_size()
self.radius = info.get_radius()
self.lifespan = info.get_lifespan()
self.animated = info.get_animated()
self.age = 0
if sound:
sound.rewind()
sound.play()
def draw(self, canvas):
canvas.draw_image(self.image, self.image_center, self.image_size, self.pos, self.image_size, self.angle)
def update(self):
self.pos[0] += self.vel[0]
self.pos[0] %= WIDTH
self.pos[1] += self.vel[1]
self.pos[1] %= HEIGHT
self.angle += self.angle_vel
def draw(canvas):
global time
# animiate background
time += 1
wtime = (time / 4) % WIDTH
center = debris_info.get_center()
size = debris_info.get_size()
canvas.draw_image(nebula_image, nebula_info.get_center(), nebula_info.get_size(), [WIDTH / 2, HEIGHT / 2], [WIDTH, HEIGHT])
canvas.draw_image(debris_image, center, size, (wtime - WIDTH / 2, HEIGHT / 2), (WIDTH, HEIGHT))
canvas.draw_image(debris_image, center, size, (wtime + WIDTH / 2, HEIGHT / 2), (WIDTH, HEIGHT))
# draw ship and sprites
my_ship.draw(canvas)
a_rock.draw(canvas)
a_missile.draw(canvas)
# update ship and sprites
my_ship.update()
a_rock.update()
a_missile.update()
# show the number of lives remaining and the score
canvas.draw_text('Lives : '+str(lives), (50, 50), 30, "White")
canvas.draw_text('Score : '+str(score), (650, 50), 30, "White")
# timer handler that spawns a rock
def rock_spawner():
global a_rock
a_rock = Sprite([random.random() * WIDTH, random.random() * HEIGHT], [random.random(), random.random()],
random.random() * 2 * math.pi, random.random() * random.choice([0.1, -0.1]),
asteroid_image, asteroid_info)
# keyboard handler
def keydown(key):
if key == simplegui.KEY_MAP['left']:
my_ship.angle_vel = -0.1
elif key == simplegui.KEY_MAP['right']:
my_ship.angle_vel = 0.1
elif key == simplegui.KEY_MAP['up']:
my_ship.thrust = True
elif key == simplegui.KEY_MAP['space']:
my_ship.shoot()
def keyup(key):
if key == simplegui.KEY_MAP['left']:
my_ship.angle_vel = 0
elif key == simplegui.KEY_MAP['right']:
my_ship.angle_vel = 0
elif key == simplegui.KEY_MAP['up']:
my_ship.thrust = False
# initialize frame
frame = simplegui.create_frame("Asteroids", WIDTH, HEIGHT)
frame.add_label('How to control the ship?')
frame.add_label('- UP ARROW to accelerate')
frame.add_label('- LEFT ARROW to rotate counter clockwise')
frame.add_label('- RIGHT ARROW to rotate clockwise')
frame.add_label('- SPACE to spawn a missile')
# initialize ship and two sprites
my_ship = Ship([WIDTH / 2, HEIGHT / 2], [0, 0], 0, ship_image, ship_info)
a_rock = Sprite([WIDTH / 3, HEIGHT / 3], [1, 1], 0, 0, asteroid_image, asteroid_info)
a_missile = Sprite([2 * WIDTH / 3, 2 * HEIGHT / 3], [-1,1], 0, 0, missile_image, missile_info)
# register handlers
frame.set_draw_handler(draw)
frame.set_keydown_handler(keydown)
frame.set_keyup_handler(keyup)
timer = simplegui.create_timer(1000.0, rock_spawner)
# get things rolling
timer.start()
frame.start()