Introduction
You have a friend who is really into retro video games. One of their favorites is the Atari classic, Asteroids . Since you are in CS 159 and becoming a super-awesome programmer, you decide to put your skills to the test and create a special version of Asteroids for your friend to play. What a thoughtful gift!
News Flash! 📰
- March 28: the JMU CS 159 S'23 profs would like to thank our dear friend Wes Anderson for releasing the Asteroid City poster this week to help build hype for this PA 🤗
- oooooh, now there’s a trailer !
- Saturday March 25, 2023: An airplane-sized asteroid will pass between the Earth and moon's orbits Saturday 😱
Thus, the goal of this project is to implement your own playable version of Asteroids, called “Generic Space Rock Shooter” (to avoid copyright infringement 🤓). You will need to exercise your growing knowledge of object-oriented concepts, including inheritance, abstract classes, polymorphism, and interfaces.
Your game will look a little something like Dr. Sprague’s version:
Learning Outcomes
- Interpret UML diagrams that include inheritance, abstract classes, and interfaces
- Use inheritance and polymorphism to reduce code duplication and simplify implementation
- Create abstract classes and subclasses that implement abstract methods
- Create, implement, and use interfaces
- Write and utilize enum types
Provided Code
We have given you some code to assist you in your game development adventure! THe following files will let you draw and animate objects on the screen, as well as handle coordinates and angles that may be difficult.
StdDraw, GameDriver, and Playable
You will need to download the following files to handle the drawing window and the main game loop:
- 📄 StdDraw.java (DO NOT MODIFY) Updated Monday March 27 at noon ‼️
- All of the graphics for this application will be handled by the StdDraw library developed by Robert Sedgewick and Kevin Wayne at Princeton University. This library is not particularly powerful or full-featured but it makes it easy to develop simple graphical applications in Java.
- Note: For Spring 2023, we are using patched version of this class that adds a
getLastDraw()
method. This method returns aString
representation of the last thing that was drawn to the screen. This is useful for testing your code. You can use this method to check that your code is drawing the correct things to the screen. For example, if you want to test that yourShip
is drawing a triangle, you can check that the last thing drawn to the screen is a triangle. This is a very simple way to test your code.
- 📄 GameDriver.java (use for testing)
- The
GameDriver
is the entry point for the project. It contains themain
method. This class contains helpful constants (that you can use) and a main method which initializes aPlayable
game and provides the main game loop. Any game that you wish to run must implement the followingPlayable
interface:
- The
- 📄 Playable.java (DO NOT MODIFY) Updated Monday March 27 at noon ‼️
- The
Playable
interface includes an “update” method that should update the state of all game elements at each time step (called a frame in animation and game design), and a “draw” method that should re-draw game elements after they have been updated. Your code will be tested using the driver above.
- The
GameUtils, Point, Pose, Vector2D
In Asteroids (and many other games), some objects will have a location on the screen (in XY coordinates) as well as an orientation they will face (aka rotation angle). Certain objects will also need to move around and have velocities (represented by a 2D vector).
In the video above, you can see that the ship has a location and orientation. Notice also that sometimes the ship is moving in a direction other than it is facing. This is because the ship has a velocity vector that is different from its orientation.
We have provided the following four classes to help represent these types of data:
- 📄 Point.java
- The
Point
class is an encapsulation of a two-dimensional location that uses rectangular screen coordinates.
- The
- 📄 Pose.java
- The
Pose
class is a subclass of Point that includes heading/orientation information (an angular measure). - notice especially the Pose.move method which will be useful for moving objects around the screen.
- The
- 📄 Vector2D.java
- The
Vector2D
class represents a two-dimensional vector. Vectors of this sort may be visualized as arrows with both a direction (or heading) and a length (or magnitude). Vectors may be used in this application to represent both the velocities and accelerations of game elements.
- The
- 📄 GameUtils.java
- Contains utility methods for working with the classes above. In particular, it includes a method for drawing a triangle for a particular Pose (which will you must use when drawing the Ship).
- note: if you look at the implementation of GameUtils.drawPoseAsTriangle, you will find that it uses StadDraw.polygon internally. Therefore if you're testing Ship, you should take advantage of StdDraw.getLastDraw() to check that the correct polygon was drawn.
- Contains utility methods for working with the classes above. In particular, it includes a method for drawing a triangle for a particular Pose (which will you must use when drawing the Ship).
Note 📝
Point
, Pose
, and Vector2D
are immutable classes (just like String
). The use of immutable classes can help to avoid subtle bugs that result from accidentally aliasing mutable objects.Other Provided Files
- 📄 AsteroidsGame.java
- 📄 TestHelpers.java
- 📄 TestStar.java (Updated Thursday 3/30 at 4:15 PM)
- 📄 TestNumericDisplay.java (Updated Thursday 3/30 at 4:15 PM)
Example Game
The GameDriver class is organized so that it will support any game that implements the Playable interface. The following two classes provide an example of a very simple “game” that implements Playable and makes use of some of the helper classes provided above:
You can try out BounceGame
by changing lines ~38-39 of GameDriver.java
as follows:
// Playable game = new AsteroidsGame();
Playable game = new BounceGame();
To be clear 🧐
Neither of these classes will be used in your Asteroids implementation. They are only provided as an example.Game Design
There are a lot of parts that go into creating a Generic Space Rock Shooter game. To help you, we’ve started AsteroidsGame.java
for you, which handles the main game loop and simple keyboard input.
Updatable and Drawable
These simple interfaces each require only 1 method (update
and draw
, respectively). The AsteroidsGame
class will use these interfaces to update and draw the game elements. You will need to implement these interfaces in your game elements.
GameElement
The GameElement
class is an abstract class that implements the Updatable
and Drawable
interfaces (this means every GameElement is responsible not only for storing its own data [“state”], but also for [1] maintaining that data via its update
method and [2] drawing itself). It also provides a few instance variables that will be useful to all game elements. You will need to extend this class in your concrete game elements. Implement this class based on the UML diagram (getters with no specification below should be simple one-line getters that return the instance variable of the same name).
Note 📝
“Getters” is a colloquial term used to describe methods that return the value of an instance variable. They are also called accessor methods. Somegetters
don’t actually start with the prefix “get”. Usually these would be those accessors for a boolean
instance variable such as isDestroyed()
.update()
At each time step, a GameElement should update its position based on its current velocity. A GameElement should also “wrap” around the edges of the game screen when moving:
- For instance, if (after applying velocity) the element flies off the right side of the screen (its X coordinate is greater than the width of the screen), the X position should be changed to 0 instead to make it re-appear on the left side of the screen.
- Likewise, if the element flies off the left side of the screen (its X coordinate is less than 0), it should reappear on the right side of the screen.
- GameElements should also wrap around the top and bottom edges of the screen.
checkCollision(GameElement other)
See Collisions and Scoring below.
Asteroid
The Asteroids must fulfill the following requirements:
- Asteroids (and all other non-filled game elements) must be drawn using the default
StdDraw
pen radius of .002. - The initial location of each asteroid must be generated randomly. Positions must be selected uniformly from the entire game screen.
- The direction of motion for each asteroid must be drawn uniformly from the interval [0, 2π).
- Asteroids must maintain a constant speed of one pixel per time step.
- The location of asteroids must “wrap” at the edges of the game screen. For example, when an asteroid departs from the left side of the screen, it must reappear in the corresponding location on the right side of the screen.
- Asteroids must be displayed as open circles.
- Asteroids come in three different sizes, each with a different point value, see AsteroidSize below.
AsteroidSize
AsteroidSize is an enum that represents the size of an asteroid. It has three values:
- SMALL: radius of 10 pixels, worth 200 points when destroyed.
- MEDIUM: radius of 20 pixels, worth 100 points when destroyed.
- LARGE: radius of 30 pixels, worth 50 points when destroyed.
AsteroidSize also has a static method called randomSize()
that will return a random AsteroidSize based on the probabilities specified in the Initialization section. This method must have a 50% chance of returning AsteroidSize.LARGE
and a 25% chance of returning AsteroidSize.SMALL
or AsteroidSize.MEDIUM
.
Saucer
Enemy saucers must fulfill the following requirements:
- Saucers must be displayed as open rectangles with a width of 20 pixels and a height of 10 pixels.
- The initial location of each saucer must be generated randomly. Positions must be selected uniformly from the entire game screen.
- The initial direction of motion for each saucer must be drawn uniformly from the interval [0, 2π).
- Saucers must maintain a constant speed of 2 pixels per time step.
- On each time step a saucer must select a new direction of motion with probability .05. The direction of motion must be drawn uniformly from the interval [0, 2π).
- The location of the saucer must not “wrap” at the edges of the game screen. Saucers must disappear permanently when they exit the screen.
- Saucers are worth 400 points when destroyed.
- The radius of a Saucer should be defined based on the details in Collisions and Scoring.
Ship (Updated Thursday 4/6 at 4:40 PM)
The Ship
class is a GameElement
that represents the player’s ship. It is responsible for drawing the ship and updating its position based on the current velocity
. You will need to implement this class based on the UML diagram (getters with no specification below should be simple one-line getters that return the instance variable of the same name).
The player’s ship must also fulfill the following requirements.
- The initial pose of the ship must be at the center of the game screen pointing upward.
- The ship must be represented as an open isosceles triangle with a base of 10 pixels and a height of 20 pixels. You may use the
GameUtils.drawPoseAsTriangle
method do draw the ship to the screen. - The movements of the ship must be controlled through the
turnLeft()
,turnRight()
, andthrust()
methods. - TURNING: If
turnLeft()
is called, the ship must be turned .1 radians to the left. IfturnRight()
is called the ship must be turned .1 radians to the right. Note that you don’t need to worry about keeping heading values within the range 0 to 2π. All of the provided heading-related methods will perform correctly for values outside of this range. - THRUST: If
thrust()
is called, a thrust must be applied to the ship’s current velocity (note:thrust()
should not modify the ship’s position). The ship must be pushed in the direction of its currentPose
heading, with a speed of .1 pixels/time step. Because the ship is floating in outer space, this means that its new velocity must now be a combination of its previous velocity plus the thrust. You may use methods from theVector2D
class (consider especially theadd
method ofVector2D
) to help with this. - FRICTION: The ship must slow down over time. Every time update is called (aka one time step), after the ship’s position is updated, the speed of the ship must be decreased by .02 pixels/time step. (hint: speed is defined as the magnitude of velocity)
- The location of the ship must “wrap” at the edges of the game screen.
- The radius of the ship (because it is not a circular object) should be defined based on the details in Collisions and Scoring.
Bullet
- Bullets must be displayed as filled circles with a radius of 1.5 pixels.
- Bullets must be created at the current position of the ship each time the user presses the space bar, as handled by
AsteroidsGame.handleKeyboardInput()
. - Bullets must move at a constant speed of 20 pixels per time step.
- The direction of the bullet’s motion must match the heading of the ship at the time the bullet is fired.
- Bullets must exist for a maximum of 20 time steps (calls to update). After 20 time steps, the bullet should be set to be destroyed.
- The location of bullets must also “wrap” at the edges of the game screen.
Stars
- Stars must be displayed as closed circles with a radius of 1 pixel.
- The initial location of each star must be generated randomly. Positions must be selected uniformly from the entire game screen.
- Stars must remain stationary.
Scoreboard (Updated Thursday 3/30 at 4:15 PM)
The text for the player’s score must be left-aligned 10 pixels from the left edge of the game screen and 20 pixels from the top. The numeric score must be prefixed by the string “Score: “. The score must be initialized to 0.
If you look in the constructor for AsteroidsGame.java, you can see how NumericDisplay is used to present the score.
Lives Remaining (Updated Thursday 3/30 at 4:15 PM)
The text for the number of lives remaining must be left-aligned 10 pixels from the left edge of the game screen and 40 pixels from the top. The number of lives must be prefixed by the string “Lives: “. The number of lives remaining must be initialized to 3.
AsteroidsGame (UPDATED Tuesday 4/12 at 3 PM)
The AsteroidsGame
class is a Playable
class that is responsible for the main game loop and keyboard input. It is also responsible for creating and updating the game elements. We provide a starter version of this class for you, but you will need to implement several methods and modify some of the existing methods. Start by downloading the following file, and then as you proceed through this assignment, work toward your implementation meeting the specifications as outlined below and in the UML diagram.
There are four ArrayLists that handle the objects in the game: drawElements
, updateElements
, shipAndBullets
, and enemies
. Objects that are put in drawElements
and updateElements
should be drawn and updated every time step, respectively. The other two lists, shipAndBullets
and enemies
are used for collisions. Collisions between game elements should be handled through the methods handleCollisions()
, removeDestroyedBullets()
, and removeDestroyedEnemies()
.
This class should also follow the specifications outlined in Game Logic. For instance, AsteroidsGame is also responsible for keeping track of the score and increasing it when enemies are destroyed.
AsteroidsGame() Constructor
Like most constructors, this one initializes its instance variables, but it stops short of creating the game elements. Rather it delegates that responsibility to the startGame()
method, which is called by GameDriver
.
As provided, this class initializes a few of its instance variables, but you will need to ensure all of the instance variables are initialized. Refer to the UML diagram to make sure you have included all the instance variables and methods.
startGame()
This method is responsible for creating the various game elements that are required for the beginning of the game.
As you progress through the different parts of the assignment, you will revisit this method to add in new game elements.
In Part 1, you need to create the Star
s and store references to them to ensure they are drawn (perhaps it would be best to rely on the private helper method here). The game should begin with 100 Star
s that are positioned randomly on the screen.
newStars()
This method should create 100 new Star
s at random positions on the screen and store their references such that they will be drawn. It should rely on the provided randomXPosition()
and randomYPosition()
static methods in AsteroidsGame
.
newShip()
This method should create a new Ship
instance and assign it to the corresponding instance attribute. It should also add the ship to all the appropriate ArrayLists.
addEnemy(Enemy enemy)
This method should add the given enemy to all the appropriate ArrayLists in AsteroidsGame.
newEnemies()
This method should create 10 asteroids of random sizes as determined by the probabilities in AsteroidSize and add them to all the appropriate ArrayLists in AsteroidsGame. (hint: there’s a couple methods that you’ve already written that you could use here!)
handleCollisions()
This method is responsible for checking if objects collide in the game. You should loop through each enemy in the enemies
list and check if it collides with the ship or any of the bullets currently in the game. If a collision occurs, both objects should be set to be destroyed.
removeDestroyedBullets()
This method should go through shipAndBullets
and check if each bullet needs to be destroyed (it’s destroyed
attribute was set to true
). If a bullet needs to be destroyed, then it should be removed from all ArrayLists that it was added to.
removeDestroyedEnemies()
This method should go through the enemies
list and check if each enemy needs to be destroyed. If an enemy needs to be destroyed, then it should be removed from all ArrayLists that it was added to.
Game Logic
Initialization
When the game starts, the ship must be present on the game screen along with 100 stars, the two numeric displays and 10 asteroids. It is not necessary to guarantee that the the ship is safe from asteroids when the game starts. If the ship appears inside an asteroid, that’s just bad luck for the player.
The size of each asteroid must be determined randomly when it is created. Each asteroid must have a 50% chance of being large and a 25% chance of being small or medium.
There must be no saucers on the initial game screen. A new saucer must be added to the game screen on each time step with a probability of .002. Note that this means there may occasionally be more than one saucer present on the game screen. Again, it is possible for saucers to appear in a location that will cause an immediate collision with the ship.
Collisions and Scoring
For the purposes of collision checking, the game elements can be divided into three categories.
- Stars and the numeric display values - These elements are not subject to collisions.
- Ship and Bullets - Elements in this category are able to collide with asteroids and saucers.
- Asteroids and Saucers - Elements in this category are able to collide with the ship and bullets.
It is not necessary to perform pixel-level collision detection. Instead, you should perform approximate collision detection by associating each game element with an appropriate collision radius.
For the case of the circular elements (Asteroids and Bullets) this should match the radius of the element. For the case of non-circular objects (Ship and Saucers) this radius should be chosen to be half the length of the longest axis of the element. For example, the ship’s longest axis is 20 pixels, so the collision radius should be 10 pixels. A collision should occur whenever two objects are near enough that the circles defined by their collision radii overlap.
Whenever a collision occurs, both elements involved in the collision must be destroyed and removed from the screen. The player’s score must be increased by the point value associated with the Asteroid or Saucer that was involved in the collision.
If the player’s ship is destroyed, and the player has remaining lives, then a new ship should be created at the starting location.
If the player loses all three lives, then no future game updates must occur: the game screen must freeze. The Lives Remaining indicator must read 0 and the score should reflect the final collision that caused the game to end.
Any time the player manages to destroy all of the Asteroids and Saucers on the game screen, a new “level” should begin: the correct number of new asteroids must appear at randomly selected locations. The position of the ship and the position of the stars should not be modified.
The following UML diagrams depict the classes you will need to implement the game:
UML Diagram
Seems too large to include here. Click the button below to view the UML diagram in a new tab.
Larger ViewRecommended Steps
Part 1
- Go over the provided files to understand what they do
- Especially take a look at
BounceGame.java
andGameDriver.java
to see what a game looks like (try running these!)
- Especially take a look at
- Make sure
GameDriver
is set up to useAsteroidsGame
and that you have properly downloaded all the provided files to your Eclipse project. - Create the
Drawable
andUpdatable
interfaces - Create the
GameElement
abstract class- Implement the
update
method
- Implement the
- Create the
Star
class- Declare the attributes and implement the constructor
- Implement the
draw
method - In
AsteroidsGame
:- add code in the
newStars()
method to create 100 randomly-placedStar
s - invoke
newStars()
in the startGame() method - add code in the
draw()
method to:- set StdDraw’s pen color to white (read the
StdDraw
docs or code, or take a peek 👀 at howBounceGame
does it) - draw all the
Drawable
s. (👀 Hint: Take a look at how theupdate()
method updates all theUpdatable
s)
- set StdDraw’s pen color to white (read the
- ensure that the lines in
update()
that invokehandleCollisions
,removeDestroyedBullets
, andremoveDestroyedEnemies
are commented out for now (you’ll need these in Part 3)
- add code in the
- Run the game and test your starfield!
- Run the provided tests for your
Star
class. - Create the
NumericDisplay
class - Run the provided tests for your
NumericDisplay
class.
Part 2
- Create the Ship class stubs
- Write the test class for the Ship class (you may want to review the GameUtils methods to support your work here.)
- Name it ShipTest
- Create the Bullet class stubs
- Write the test class for the Bullet class
- Name it BulletTest
- Complete the implementation of the Ship class
- Implement the attributes and constructor
- Implement the draw method
- Implement methods to control the ship (turnLeft, turnRight, thrust)
- In AsteroidsGame, implement method to add a ship and handle movement
- You should be able to run the game and test it at this point
- Add code to AsteroidsGame to shoot a bullet when the spacebar is pressed
Part 3
- Create the Enemy abstract class
- Create the AsteroidSize enum class that represents the three different sizes and point values
- Create the Asteroid class
- Implement the attributes, constructor, and method stubs
- Implement the draw method
- Implement update to have the asteroid move each frame
- In AsteroidsGame, implement newEnemies() to add 10 randomly sized asteroids
- Add collision handling to AsteroidsGame
- You will need to check every Enemy to see if it collides with a Bullet or Ship
- Objects that collide should be destroyed
- Enemies that are destroyed should update the score
- Create the Saucer class
Submitting
As usual you will submit the parts of this assignment to Gradescope. There will be 4 different assignments on Gradescope, dividing this assignment as follows
- Part 1 - 🗓️ Monday April 3
- Submit:
Drawable
Updatable
GameElement
Star
NumericDisplay
- Autograder:
- deducts for checkstyle issues
- Your implementation of
Star
,NumericDisplay
, andGameElement
pass our unit tests
- Submit:
- Part 2A - 🗓️ Wednesday April 5
- Submit:
ShipTest
BulletTest
- Autograder:
- deducts for checkstyle issues
- Runs your
ShipTest
andBulletTest
against our reference solution expecting:- 90% coverage of
Ship
,Bullet
- all tests passing
- 90% coverage of
- Submit:
- Part 2B - 🗓️ Monday April 10
- Submit:
Ship
Bullet
ShipTest
BulletTest
GameElement
- Autograder:
- deducts for checkstyle issues
- Your Tests should have 100% coverage of your implementation, and all of the tests should pass
- Your implementation of
Ship
andBullet
pass our unit tests
- Submit:
- Part 3 - 🗓️ Friday April 14
- Submit:
GameElement
Updatable
Drawable
Star
NumericDisplay
Ship
ShipTest
Bullet
BulletTest
Enemy
Asteroid
Saucer
AsteroidSize
AsteroidsGame
- Autograder:
- deducts for checkstyle issues
- Your implementation of the following pass our unit tests:
Star
NumericDisplay
Ship
Bullet
Asteroid
Saucer
AsteroidsGame
AsteroidSize
- Submit:
Change Log
Many software projects undergo changes over time. This section documents the changes made to this assignment over time. The most recent changes are listed first (this is called “reverse chronological order”).
- Add additional method descriptions for AsteroidsGame -
- Add reference to AsteroidSize probabilities under AsteroidSize -
- Updated Part 3 to list the classes that must pass tests, and added language to Ship thrust -
- Revise wording for Ship thrust and friction -
- Put instructions for Ship together -
- Added hints for Ship radius -
- Updated Part 2A language to not require coverage of GameElement, and add guidance on use of existing code in StdDraw, Pose, and GameUtils -
- Updated Tests and spec to use textLeft instead -
- Update TestStar and TestNumericDisplay to fix issue with JUnit -
- Added TestHelpers and due dates for sub-parts -
- Update Part 1 -
- Provide TestStart and TestNumericalDisplay
- Add instructions about modifications to AsteroidsGame
- Publish remaining specs -
- Updated `AsteroidsGame.java`
- provide random helpers
- provide beginning of `update()`
- provide beginning of `startGame()` stub
- Initial Draft so you can get rolling on Part 1 -