PA4 – Asteroids!

Introduction

The goal for this project is to develop a version of the classic video game Asteroids. If you’ve never played Asteroids, we encourage you to find an on-line version to try, and to view the demo below.

Honor Code

You must work on programming assignments on your own. You may request help on programming assignments from the course instructor or the lab assistant. If you receive such help you must make note of it in the documentation for each method. The acknowledgement should state the full name of the person helping and a short blurb about what they helped with on a method-to-method basis on the line in the Javadoc comments after the description of the method. If help across methods was given this may be documented once in the class(es) affected again in the Javadoc comment after the description of the class(es), including a list of the methods where help was received. The goal of this is to help us both understand just how much help you are needing to get to the assignment done.

You may request help on general topics from other students and friends. You may even look at code, but you should not copy down any code or write code for another person. Looking at code should be used to help others debug their code only after you have satisfactorily written and submitted that code.

Representing someone else’s work as your own, in any form, constitutes an honor code violation. Directly copying or using the same code is an honor code violation. It is also a violation of the honor code to “render unauthorized assistance to another student by knowingly permitting him or her to copy all or a portion of an examination or any work to be submitted for academic credit.” That means that you can be written up for an Honor code violation if you share your code with someone else, even if you wrote it completely on your own.

Provided Code

Tools

StdDraw, GameDriver and GameConstants

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.

The following driver initializes the drawing window and handles the main game loop: GameDriver.java (DO NOT MODIFY)

It is your responsibility to develop an AsteroidsGame class that provides an “update” method that updates the state of all game elements at each time step, and a “draw” method that re-draws game elements after they have been updated. Your code will be tested using the driver above.

The file GameConstants.java contains a set of constant values such as the window size, the update rate etc. You are free to add additional constants to this file.

GameUtils, Point, Pose, Vector2D

It will be necessary to store and update the poses and velocities of game elements. The following classes will be helpful for maintaining and updating these quantities.

  • Point.java – The Point class is an encapsulation of a two-dimensional location.
  • Pose.java – The Pose class is a subclass of Point that includes heading information.
  • Vector2D.java – The Vector2D class represents a two-dimensional vector. Vectors of this sort may be visualized as arrows with both a direction and a length (or magnitude). Vectors may be used in this application to represent the velocities of game elements.
  • GameUtils.java – This class contains utility methods for working with the classes above.

It should not be necessary to modify any of these classes, but you are free to do so if you wish.

Game Elements

You must provide implementations for the following game elements.

Asteroid

The Asteroids must fulfill the following requirements:

  1. Asteroids must be displayed as open circles with a radius of 30 pixels.
  2. Asteroids (and all other non-filled game elements) should be drawn using the default StdDraw pen radius of .002.
  3. The initial location of each asteroid must be generated randomly. Positions should be selected uniformly from the entire game screen.
  4. The direction of motion for each asteroid must be drawn uniformly from the interval [0, 2π).
  5. Asteroids must maintain a constant speed of one pixel per time step.
  6. 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.
  7. Asteroids are worth 20 points when destroyed.

Saucer

Enemy saucers must fulfill the following requirements:

  1. Saucers must be displayed as open rectangles with a width of 20 pixels and a height of 10 pixels.
  2. The initial location of each saucer must be generated randomly. Positions should be selected uniformly from the entire game screen.
  3. The initial direction of motion for each saucer must be drawn uniformly from the interval [0, 2π).
  4. Saucers must maintain a constant speed of 2 pixels per time step.
  5. 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π).
  6. The location of the saucer must not “wrap” at the edges of the game screen. Saucers must disappear permanently when they exit the screen.
  7. Saucers are worth 200 points when destroyed.

Ship

The player’s ship must fulfill the following requirements.

  1. The initial pose of the ship must be at the center of the game screen pointing upward.
  2. 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.
  3. The movements of the ship should be controlled through keyboard interaction. You must use the method StdDraw.isKeyPressed to detect keyboard events.
  4. TURNING: On each time step, if the user is pressing the left arrow key (java.awt.event.KeyEvent.VK_LEFT) the ship should be turned .1 radians to the left. If the user is pressing the right arrow key java.awt.event.KeyEvent.VK_RIGHT the ship should 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.
  5. THRUST: On each time step, if the user is pressing the down arrow key (which is java.awt.event.KeyEvent.VK_DOWN), an acceleration vector with a magnitude of 0.1 pixels/time-step and a heading that is the current ship direction must be applied. The applyThrust method defined in the GameUtils class may be used to update the ship’s velocity vector using an acceleration vector.
  6. FRICTION: The ship must slow down in the absence of thrust. On each time step the speed of the ship should be reduced to 99% of it’s speed on the previous time step.
  7. The location of the ship must “wrap” at the edges of the game screen.

Bullet

  1. Bullets must be displayed as filled circles with a radius of 1.5 pixels.
  2. Bullets should be created at the current position of the ship each time the user presses the space bar. You must use the StdDraw.hasNextKeyTyped and StdDraw.nextKeyTyped methods to detect space bar presses.
  3. Bullets must move at a constant speed of 20 pixels per time step.
  4. The direction of the bullet’s motion must match the heading of the ship at the time the bullet is fired.
  5. Bullets should exist for a maximum of 20 time steps.
  6. The location of bullets must “wrap” at the edges of the game screen.

Stars

  1. Stars must be displayed as closed circles with a radius of 1 pixel.
  2. The initial location of each star must be generated randomly. Positions should be selected uniformly from the entire game screen.
  3. Stars must remain stationary.
  4. There must be 100 stars.

Scoreboard

The player’s score must appear 60 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.

Lives Remaining

The number of lives remaining must appear 60 pixels from the left edge of the game screen and 60 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.

Game Logic

Initialization

When the game starts, the ship must be present on the game screen along with 10 asteroids, 100 stars and the two numeric displays. 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.

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: 10 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 video illustrates how the completed game should look:

Part A: Readiness Quiz (10%)

The readiness quiz for this assignment will focus on UML syntax. YOU MUST ANSWER ALL QUESTIONS CORRECTLY TO GET ANY CREDIT FOR THIS PART. You may take the quiz as many times as necessary.

Part B: UML (40%)

By the first deadline you must submit a UML diagram illustrating your design. For full credit, your design must make appropriate use of the object oriented features that we have been covering in class: specialization, abstract classes, interfaces, etc. Your UML should be as complete as possible. It should show the attributes of each class as well as the signatures of all public and protected methods. It should also show the relationships between your classes.

Part of your grade for this portion of the assignment will be based on using correct UML “syntax”. Your UML diagram may be prepared electronically or very neatly hand-drawn and scanned. This page lists some tools for preparing UML diagrams. (I usually use UMLet to prepare UML diagrams for this course.)

You are free to brain-storm design ideas with other students in the class. The final UML diagram must be prepared and submitted individually, but you are encouraged to discuss the design with others. If you do work with another student in developing your design, you must acknowledge that assistance in your submission.

Your UML design document must include a few paragraphs describing and defending your design decisions. For full credit this document must include definitions of inheritance and polymorphism. It must explicitly point out where inheritance and polymorphism are used to prevent code duplication. It may also be appropriate to use the concepts of coupling and cohesion to justify aspects of your design.

Your UML diagram must be submitted through Canvas as a .pdf file.

No late submissions will be accepted for Part B.

Design Rubric

Design supports the required functionality 15%
Correct UML “syntax” 15%
UML is visually well organized and neatly drawn or electronically prepared. 10%
Appropriate names (Names are meaningful. Method names are verbs and instance variables and classes are nouns. Plural nouns are only used for collections.) 10%
Appropriate use of inheritance (Superclasses are used to prevent code duplication. Subclasses meaningfully extend the functionality of superclasses. Instance variables are not shadowed in subclasses.) 10%
Classes and methods are abstract where appropriate. 10%
Appropriate use of polymorphism (Using interfaces, superclasses or both.) 10%
Documentation provides a coherent defense of key design decisions. 10%
Documentation uses correct spelling, grammar etc. 10%

Part C: Code (50%)

You must submit your finished project through Autolab by the second deadline. Note that the Autolab submission tests will not be able to verify that your game elements are updated or drawn correctly or that your game logic is correct. Passing the Autolab tests will only mean that your code is formatted correctly and doesn’t crash when it is executed. This makes it particularly important that you carefully test your code before submitting.

Design and Implementation Suggestions

Working with Random Numbers

  • The class java.util.Random should be used to generate pseudo-random numbers for this application. The nextDouble method returns a double selected uniformly from the interval [0, 1). This method can be used to generate initial poses by multiplying the generated values by the desired upper bound for each component of the pose.
  • In order to create a block that occurs with a particular probability, you can compare the output of nextDouble to the desired probability. For example:
    if (generator.nextDouble() < .75)
    {
      // This block will execute with probability .75.
    }

Miscellaneous Advice

  • Make sure you understand the provided code. Take some time to look over the StdDraw library documentation as well as the other provided classes.
  • Test your code incrementally. Make sure that each new game element is working correctly before you start coding the next one.
  • Look for opportunities to use polymorphism and dynamic binding to avoid code duplication. As always, if you find yourself copying and pasting code there is a good chance you are doing something wrong.
  • Don’t wait until after the UML deadline to start working on your code. You can experiment with developing and testing classes even if you haven’t completely solidified your design. It will probably be easier to refactor functional code to match an improved design than it would be to start coding from scratch.
  • When game elements are destroyed, you will probably want to remove them from the collection(s) where they are stored. Unfortunately, modifying Java collections during iteration can lead to problems. For example, the following code will will result in a ConcurrentModificationException:

    ArrayList<String> words = new ArrayList<>();
    
    words.add("tree");
    words.add("car");
    words.add("house");
    
    // Attempt to remove "tree" from the list.
    for (String word : words)
    {
      if (word.equals("tree"))
      {
        words.remove(word); // Throws exception!
      }
    }

    Iterators may be used to safely remove objects. The following code works correctly.

    ArrayList<String> words = new ArrayList<>();
    
    words.add("tree");
    words.add("car");
    words.add("house");
    
    Iterator<String> it = words.iterator();
    
    // Remove "tree" from the list.
    while (it.hasNext())
    {
      String current = it.next();
      if (current.equals("tree"))
      {
        it.remove(); // Safely removes the most recent item.
      }
    }

Another possibility is to use an indexed for loop that indexes backwards through the collection:

ArrayList words = new ArrayList<>(); 

words.add("tree");
words.add("car");
words.add("house");

// Remove "tree" from the list.
for (int i = words.size() - 1; i >= 0; i--)
{
if (words.get(i).equals("tree"))
{
words.remove(i);
}
}

Going Further

There are a huge number of improvements that could be introduced beyond the basic version of the game described above. Asteroids could break up on impact instead of being destroyed. Asteroids could be displayed as rock-like polygons instead of circles. Saucers could fire on the player’s ship, etc.

We encourage you to experiment with as many of these improvements as you want, but the version you submit should strictly match the specification above.

Acknowledgments

Nathan Sprague originally created this assignment. It was loosely inspired by the Asteroids project presented by Dan Leyzberg and Art Simon at the 2008 SIGCSE Nifty Assignments Session. It has since been updated by other JMU CS 159 Instructors.