Link Search Menu Expand Document

CS 142 Project 3: Asteroids

  1. Starter code
  2. Guide to the classes
  3. Guide to the math
  4. Guide to computer graphics animations
  5. How to develop the project
    1. The Polygon class
    2. The Asteroid class
    3. The RocketShip class
    4. The ShrinkRay class
    5. The AsteroidsGame class, Part 1
    6. Finishing the Asteroid class (collisions)
    7. The AsteroidsGame class, Part 2
  6. Coding style and citations
  7. Getting help: Humans vs. AI
  8. What to turn in
  9. Challenges

For this project, you will write a program that allows you to play a game called Asteroids.
The game features a rocket ship that you control, a collection of asteroids that bounce around the screen, and a “shrink ray” that the ship can fire to shrink the asteroids. When you hit an asteroid with the shrink ray three times, it is destroyed (disappears). Destroy all the asteroids to win the game. But you automatically lose if you crash your rocket into an asteroid!

Starter code

Make sure you create the project in a place on your computer where you can find it! I suggest making a new subfolder in your CS 142 projects folder.

Link to download starter code: proj3-starter.zip

Guide to the classes

In this project, you will work with several classes:

  • Polygon: A polygon represents a shape drawn with a sequence of connected line segments. Each polygon is defined by a sequence of (x, y) points, and the boundary of the polygon is formed by connecting the points in sequence. A Polygon also knows what color it is so it can be drawn on the canvas. Polygons can be translated (moved), rotated, and scaled (grown or shrunk). Both the asteroids and the rocket ship are drawn as polygons.

  • Asteroid: An asteroid is a rock that floats around the screen. It is drawn as an 8-sided polygon with a slightly irregular shape. Asteroids drift in a random direction and slowly rotate. When hit by a shrink ray, an asteroid shrinks. After being shrunk three times, it is destroyed and vanishes from the screen.

  • RocketShip: The rocket ship is the player’s ship. It is drawn as a red triangle. The player can rotate it left/right and move it forward/backward. It can also fire shrink rays in the direction it is pointing.

  • ShrinkRay: A shrink ray is a small green circle that the rocket ship fires. It travels in a straight line in the direction the ship was pointing when it was fired.

  • AsteroidsGame: This class runs the game. It creates the asteroids, the ship, and manages the game loop: moving objects, checking for collisions, handling keyboard input, and drawing everything on the screen.

  • AsteroidsRunner: This class does nothing more than hold the main() method for the game. All it does is create a new instance of the AsteroidsGame class and call runGame().

Guide to the math

This project requires some understanding of translations, scaling, and rotations in geometry and how they are applied to a polygon.

Translations are straightforward. A translation means moving every point in a polygon a constant distance in the \(x\) direction, and a constant distance in the \(y\) direction. The distances in the \(x\) and \(y\) directions don’t have to be the same, and they may be zero (for instance, to shift the polygon perfectly left, right, up or down).

Mathematically, just add the translation’s \(x\) distance and \(y\) distance to each point in the polygon. Remember that in computer graphics, the y-axis is “flipped,” so a translation of 10 in the \(y\)-direction increases all the \(y\) coordinates in the polygon by 10, and therefore moves the polygon down the screen.

Scaling means growing or shrinking a polygon by a certain factor, relative to its center. To scale a polygon by a factor \(f\) around its center \((c_x, c_y)\):

  • For each point \((x, y)\) in the polygon:
    • Subtract the center: \(x_1 = x - c_x\), \(y_1 = y - c_y\)
    • Multiply by the factor: \(x_2 = x_1 \cdot f\), \(y_2 = y_1 \cdot f\)
    • Add the center back: the new point is \((x_2 + c_x, y_2 + c_y)\)

Rotations are more complicated. Our game always rotates a polygon around its center point, which we will denote by \((c_x, c_y)\). To perform the rotation by an angle \(d\) in degrees:

  • Convert the angle to radians: \(r = d \cdot \pi/180\).
  • For each point \((x, y)\) in the polygon:
    • Subtract the center: \(x_1 = x - c_x\), \(y_1 = y - c_y\)
    • Apply the rotation: \(x_2 = x_1 \cdot \cos(r) - y_1 \cdot \sin(r)\), \(y_2 = x_1 \cdot \sin(r) + y_1 \cdot \cos(r)\)
    • Add the center back: the new point is \((x_2 + c_x, y_2 + c_y)\)

Moving by a speed and angle: The rocket ship and the shrink ray keep track of the speed they are moving as well as the angle they are pointing on the screen (where directly “up” on the screen is 0 degrees). When these objects move during the game, we can calculate how far they move in the \(x\) and \(y\) directions based on this algorithm:

  • Convert the angle to radians: if the angle in degrees is \(d\), then set \(r = d\cdot \pi/180\).
  •  \(\text{change}_x = \text{speed} \cdot \sin(r)\)
  •  \(\text{change}_y = -\text{speed} \cdot \cos(r)\)

The negative sign on \(\text{change}_y\) is because the y-axis is flipped.

You can now apply the regular math for a translation to move the rocket ship or shrink ray.

Guide to computer graphics animations

Animations on a computer screen — whether in games, movies, or simulations — work the same way as a flipbook: by showing a series of still images in rapid succession, each slightly different from the last. If the images change fast enough, our eyes perceive smooth, continuous motion.

In most computer games, we produce this effect with a game loop: a while loop that repeats over and over, many times per second, until the game ends. Each time through the loop is called a frame, and every frame, the game does the same sequence of steps:

  1. Move all the objects a small amount (asteroids drift a few pixels, the ship responds to controls, any shrink ray travels forward). This step modifies the internal state of the game (changing variables and objects), but does not update the screen.
  2. Handle events: Did the shrink ray hit an asteroid? Did the ship crash into one? Did the player press a key and therefore we need to adjust the ship?
  3. Draw the entire screen from scratch: clear the canvas, then redraw every object in its new position.
  4. Pause for a short time before starting the next frame.

The pause at the end of each frame is critical. Without it, the loop would run as fast as the computer can execute it — potentially thousands of times per second. Each object would only move a few pixels per frame, but those frames would fly by so fast that the objects would appear to teleport across the screen. By pausing for 20 milliseconds at the end of each frame, we limit the game to approximately 50 frames per second (since 1000 ms / 20 ms = 50). This is similar to the frame rate of television or video, and it gives the animation a smooth, consistent feel that doesn’t depend on how fast the computer is.

You can see this structure in the animation testing methods in the PolygonTests class. You will write longer and more complicated versions in the full game.

How to develop the project

This project is organized around writing one class at a time and testing it. You do not need to modify SimpleCanvas or AsteroidsRunner — those classes are complete and ready to use as-is.

The Polygon class

This class represents a generic polygon, represented as two arrays of points (one for the \(x\)-coordinates and one for the \(y\)’s), along with a color it will be drawn in. A polygon is created using syntax like this:

Polygon rectangle = new Polygon(4, Color.RED);  // number of points and color
rectangle.addPoint(100, 100);
rectangle.addPoint(200, 100);
rectangle.addPoint(200, 300);
rectangle.addPoint(100, 300);

This class already has some methods written for you: the constructor, addPoint(), drawOn(), and toString(). You need to write the rest of the methods.

Before writing any methods, familiarize yourself with the instance variables of the class and understand how the existing methods work with them.

Note: The points of a polygon are represented here as doubles, even though we will only ever use integer coordinates. The reason for this is that when we start animating the polygons, it is possible that the translation and rotation math will require some polygons to temporarily be placed at non-integer coordinates, so therefore we just store all points as doubles.

Suggested order of writing and testing methods

  • getColor(): Write this first; it’s literally one line of code.

  • getCenterX() and getCenterY(): Write these next. These methods should compute and return the x (or y) coordinate of the center of the polygon. The center is simply the average of all the x coordinates (or y coordinates) of the polygon’s points. Use a loop to calculate these values.

  • getAverageRadius(): Write this next. This method should compute and return the average distance from the center of the polygon to each of its vertices. You can use getCenterX() and getCenterY() to find the center. Hint: use Math.sqrt() to compute square roots.

  • Stop and test. In the PolygonTests class, there is code to test the four functions you wrote. You will need to uncomment the call to testPolygons() in main() to run it. Make sure the output makes sense. You will need to add another polygon to the code and test it as well.

  • translate(): Write this next. This method takes two doubles, distx and disty, and should shift every point in the polygon by adding distx to each x coordinate and disty to each y coordinate.

  • Stop and test. In PolygonTests, run testTranslation() as well as testTranslationAnimation(). Note how the testTranslationAnimation() test code runs the animation loop described above. Make sure you understand this.

  • scale(): Write this next. This method takes a double factor and scales the polygon around its center. Follow the scaling math described above.

  • Stop and test. In PolygonTests, you will need to write testScaling() as well as run testScalingAnimation(). You should invent some appropriate tests that follow what testTranslation() does.

  • rotate(): Write this last. This method takes an angle in degrees and rotates the polygon around its center. Follow the rotation math described above.

  • Stop and test. In PolygonTests, you will need to write testRotation() as well as testRotationAnimation(). You should invent some appropriate tests that follow what testTranslation() does.

Polygon is now done!

The Asteroid class

This class represents a single asteroid. Every Asteroid keeps track of five instance variables, which each have a comment at the top of the class stating what they do. The constructor is already written for you; it sets up the 8-sided polygon and initializes the direction the asteroid is floating (xSpeed, ySpeed) and the rotation speed, all to random values. Take note of the size instance variable: notice that the constructor sets this to 3, indicating the asteroid is “large.” As it gets hit by a shrink ray, it will decrease in size, and we keep track of this through the size variable.

Suggested order of writing methods

We will write some of the methods now and some later. Note that all of these methods you are writing now are very short. If you find yourself writing more than 5 lines of code, you are probably on the wrong track.

  • getCenterX() and getCenterY(): Write these first. These will be very short; all these methods do is return the x and y coordinates for the center of the polygon. Hint: you do not need to calculate these from scratch like you did in Polygon; you should just call the same method on the Polygon object you already have in Asteroid.

  • toString(): This method should return a String containing information about the Asteroid, including its position, size, speed, and rotation speed.

  • Stop and test. In AsteroidTests, run the test code to verify toString works.

  • move(): This method should move the asteroid one step. Do this by translating the polygon by xSpeed and ySpeed, and rotating it by rotationSpeed.

  • Stop and test. In AsteroidTests, call move() on your asteroid and verify it works.

  • reverseXSpeed() and reverseYSpeed(): These methods should reverse the direction of the asteroid’s horizontal or vertical speed. These are used for bouncing off the walls. All you need to do is keep the speed variable the same magnitude, but change it from positive to negative or vice-versa.

  • shrink(): This method should decrease size by 1 and scale the polygon to 75% of its current dimensions.

  • isDestroyed(): This method should return true if size is 0.

  • Stop and test. In AsteroidTests, make a few calls to the methods you just wrote to verify they work.

Note that you should not write the checkForHit methods yet.

Asteroid is now partially done, enough to write the first version of the whole game.

The RocketShip class

This class represents the player’s rocket ship. Every RocketShip keeps track of two instance variables: a Polygon (the red triangle that is drawn on screen) and an angle (the direction the ship is pointing, in degrees, where 0 means “up”). The class also has two constants: SPEED, which controls how many pixels the ship moves per update, and ROTATE_ANGLE, which controls how many degrees the ship rotates when the player presses left or right.

Note: You’ll notice that some instance variables are declared with the keyword final, like private final int SPEED = 3. The final keyword means the variable’s value is set once and cannot be changed while the program runs: it is a constant. By convention, we name constants in all capital letters to distinguish them from regular variables. You should use these constants in your code rather than typing the numbers directly (e.g., write SPEED instead of 3), so that if you ever want to tweak a value, you only have to change it in one place.

Suggested order of writing methods

These methods are all fairly short. If you find yourself writing more than about 5 lines of code for any of them, you are probably on the wrong track.

The constructor is already written for you. It creates a 3-point red polygon shaped like a triangle with its nose at the given center coordinates, and sets the initial angle to 0.

  • getCenterX() and getCenterY(): Write these first. Like the Asteroid class, these are very short — just return the center coordinates from the polygon.

  • getAngle(): Return the value of angle.

  • rotateLeft(): Decrease angle by ROTATE_ANGLE, and then rotate the polygon by the negative of ROTATE_ANGLE.

  • rotateRight(): Increase angle by ROTATE_ANGLE, and then rotate the polygon by ROTATE_ANGLE.

  • moveForward(): Move the ship forward in the direction it is pointing. Use the “moving by a speed and angle” math from the math section above to calculate how far to move in the \(x\) and \(y\) directions, and then translate the polygon by those amounts. Remember to convert the angle to radians first.

  • moveBackward(): Move the ship backward (opposite of the direction it is pointing). This is the same as moveForward(), but with the translation going in the opposite direction. Hint: you can negate both the \(x\) and \(y\) changes.

  • toString(): Return a String containing information about the ship, including its center coordinates and angle.

  • Stop and test. In RocketShipTests, there is code that creates a ship, rotates and moves it, and prints the center coordinates after each operation. Run the test and verify that your output matches the expected values shown in the comments.

RocketShip is now done!

The ShrinkRay class

This class represents a shrink ray (laser beam) that the ship can fire. A ShrinkRay keeps track of three instance variables: x and y (its current position on the screen) and angle (the direction it is traveling, in degrees, where 0 means “up”). There are also three constants: RADIUS (3), SPEED (10), and COLOR (green). You should use these constants in your code rather than typing the numbers directly.

Unlike the other classes, nothing is written for you in this class except the instance variables and constants. You need to write the constructor and all methods yourself. This is a good exercise in writing a class from scratch given only the instance variables.

Methods you need to write

  • A constructor ShrinkRay(double x, double y, double angle): Store the given starting position and angle into the instance variables.

  • drawOn(SimpleCanvas canvas): Draw the shrink ray on the canvas. The shrink ray is drawn as a small filled circle at position (x, y) with the radius and color given by the constants. Hint: look at the SimpleCanvas class’s methods for how to draw a filled circle.

  • move(): Move the shrink ray one step forward. Use the same “moving by a speed and angle” math you used in RocketShip, but applied to the x and y variables directly. (Since the shrink ray is just a circle, not a polygon, you update x and y instead of translating a polygon.)

  • getCenterX() and getCenterY(): Return x and y, respectively.

  • toString(): Return a String containing information about the shrink ray, including its position and angle.

  • Stop and test. In ShrinkRayTests, write code to create a shrink ray, print it out, call move() a few times, and print it again. Verify that the position changes make sense for the angle you chose.

ShrinkRay is now done!

The AsteroidsGame class, Part 1

This is the main class that brings everything together. It creates the game objects, runs the game loop, and handles all the game logic. You should begin by reading the entire class. Take note of the instance variables that hold the canvas, the array of asteroids, the ship, the shrink ray, the score, and the boolean variables that will keep track of the game’s progress.

There are three methods that you will add code to:

  • draw(): Draws the current state of the game on the screen.
  • handleKeyboard(): Reacts to the user pressing keys on the keyboard to move the ship or fire the shrink ray.
  • runGame(): Contains the main game loop — a while loop that repeats every frame until the game ends, calling draw() and handleKeyboard() each time through.

There are lots of comments throughout to help you understand the logic.

You will write the code in stages so you can test as you go.

Step 1: Have the draw() method draw the ship and asteroids

This method draws the current state of the game on the canvas. The canvas is already cleared for you at the top, and canvas.update() is already called at the bottom. In between, you need to draw the asteroids (skip any that have been destroyed) and the rocket ship. We will add in the shrink ray and score later.

Stop and test. Run the game. You should see the ship and asteroids!

Step 2: Have the handleKeyboard() method react to the keyboard

This method checks which keys are currently pressed and responds accordingly. The if statements checking for each key are already provided. Fill in the body of each one so that the arrow keys move and rotate the ship. Each one of these is one line of code; you are just calling a method you’ve already written.

Stop and test. Run the game again. You should be able to fly the ship around with the arrow keys.

Step 3: Asteroid movement and wall bouncing

Now let’s bring the asteroids to life. Inside the while loop in runGame(), add code before the existing handleKeyboard() and draw() calls to move each non-destroyed asteroid one step. Remember, you’ve written methods to do these things!

After moving an asteroid, check whether it has drifted beyond any of the four edges of the screen, and if so, “bounce” the asteroid off the edge of the screen so it doesn’t disappear off the side. You can do this with if statements, one for each edge of the screen: get the center of the asteroid and check if its \(x\) and \(y\) components indicate that it’s off the screen (too far left/right or too far top/bottom), and then call the speed reversal methods on the asteroid as appropriate to achieve the bouncing effect. Note: you can get the width and height of the canvas with the getWidth() and getHeight() methods of the canvas object, or you can use the constants from the top of the file.

Stop and test. Run the game again. The asteroids should now drift around the screen and bounce off the walls.

Step 4: Adding the shrink ray

Now let’s get the shrink ray working. First, an important design detail: unlike the asteroids, where there are NUM_ASTER of them stored in an array, there is only ever one shrink ray on the screen at a time. Look at the instance variable ray in AsteroidsGame — notice that it is initialized to null. Remember that in Java, null is a special value that means “this variable doesn’t refer to any object right now.” We use null to represent the fact that no shrink ray is currently on screen. When the player presses the spacebar, we create a new ShrinkRay object and store it in ray. If the player presses the spacebar again before the ray has left the screen, we simply replace it with a new one (the old ray disappears and a new one fires from wherever the ship is now). Unlike the asteroids, the shrink ray does not bounce off the edges of the screen — it just flies off into the distance and that’s fine.

Because ray might be null at any given moment, you need to check whether it is null before doing anything with it (drawing it, moving it, etc.). If you try to call a method on a null variable, Java will crash with a NullPointerException.

With that in mind, there are three things to add in the code:

  • In handleKeyboard(), fill in the spacebar case so that pressing space creates a new ShrinkRay at the ship’s current position and angle. You wrote methods to access these values, so now you can call them.
  • In draw(), add code to draw the shrink ray (if one exists).
  • In the game loop, add code to move the shrink ray each frame (if one exists).

Stop and test. Run the game. You should be able to fire shrink rays with the spacebar and see them fly across the screen. Try firing again before the first one leaves the screen — you should see the old ray disappear and a new one fire from the ship. Collisions still won’t work; that’s coming next.

Finishing the Asteroid class (collisions)

Before we can add collision detection to the game, we need to go back to the Asteroid class and write the two checkForHit methods. These methods detect whether the asteroid has been hit by a shrink ray or by the ship.

Both methods work the same way: calculate the distance from the center of the asteroid to the center of the other object using the standard distance formula (\(\sqrt{(x_1-x_2)^2 + (y_1-y_2)^2}\)), and return true if that distance is less than the asteroid’s average radius. Remember, you have methods to access all of this information.

Write the following:

  • checkForHit(ShrinkRay ray): Check if the ray has hit this asteroid.

  • checkForHit(RocketShip ship): Check if the ship has hit this asteroid.

Notice that these two methods have the same name but different parameter types. This is called method overloading: Java can tell which version to call based on whether you pass a ShrinkRay or a RocketShip as the argument.

Asteroid is now fully done!

The AsteroidsGame class, Part 2

Now we can wire up the collision detection and end-of-game logic in runGame(). Add the following inside the while loop, after the movement code you wrote earlier but before handleKeyboard().

Step 5: Shrink ray vs asteroid collisions. If a shrink ray is on screen, check whether it has hit any non-destroyed asteroid (use a loop). If it has, shrink that asteroid, award 10 points, and remove the ray from the game (set it to null). A single ray can only hit one asteroid, so stop checking once you find a hit.

Step 6: Ship vs asteroid collisions. Check whether the ship has crashed into any non-destroyed asteroid (use a loop). If so, the game is over — update the appropriate boolean variable.

Step 7: Win condition. Determine whether every asteroid has been destroyed (use a loop). If so, update the appropriate boolean variable. Think about how to check that all elements of an array satisfy a condition.

Step 8: Display the score. In draw(), add code to display the score on the screen. Take a look at canvas.drawString() — it takes an x position, a y position, the text to display, and a font size.

Step 9: End-of-game message. After the while loop ends, the game is over. Display a “You Win!” or “Game Over!” message (along with the final score) on the screen, depending on how the game ended.

Stop and test. The game should now be fully playable! Asteroids should bounce around, the shrink ray should shrink them on contact, and the game should end when you destroy all asteroids or crash into one.

Coding style and citations

  • You should use good programming style when writing your program. All other style guidelines, including proper indentation and comments, should be followed.

  • Projects are intended to have you practice coding concepts covered in class; the goal of completing the project is to demonstrate your understanding and knowledge of the material that we’ve covered, not to find the shortest path to a working program. For that reason, while I always encourage learning things outside the class and using external resources to help you, you should do your best to stick to using coding techniques learned in class.

  • If you are unsure whether a particular feature or approach is permitted, ask me before submitting.

  • If you do use an idea that you got from an outside source (a person, a website, or AI), you should acknowledge that source in a comment in the code, explaining where the idea came from, including the person’s name, complete website URL, or name of the AI. Not doing so is a serious academic violation.

Getting help: Humans vs. AI

Why work without AI for now? This project is designed to build your problem-solving muscles. When you struggle with a bug or concept, that struggle is where the learning happens. Here’s why human help is better at this stage:

  • Professor Kirlin and the tutors can meet you where you are. We can ask questions to understand your thought process, not just fix the code.
  • We help you learn to debug. AI will often just give you corrected code. The tutors and I can teach you how to find bugs yourself, which is a skill you’ll use forever.
  • We can explain the “why.” Understanding why a piece of code works the way it does is more valuable than having working code you don’t understand.
  • Office hours are for YOU. Seriously, I love talking through these problems. My general rule of thumb is that if you’ve been stuck on something for 30 minutes or more, ask for help.

What to turn in

  • Through Canvas, turn in all your .java files.
  • Additionally, upload a text file answering the following questions:
    • What bugs and conceptual difficulties did you encounter? How did you overcome them? What did you learn?
    • Describe any serious problems you encountered while writing the program (larger things than the previous question).
    • Describe whatever help (if any) that you received. Don’t include readings, lectures, and exercises, but do include any help from other sources, such as websites or people (including classmates and friends) and attribute them by name.
    • Did you do any of the challenges (see below)? If so, explain what you did.
    • List any other feedback you have. Feel free to provide any feedback on how much you learned from doing the assignment, and whether you enjoyed doing it.

Challenges

  • Add a life system: the player gets 3 lives and respawns at the center of the screen after being hit.

  • Add the ability for there to be multiple shrink rays present on the screen at once.

  • Make the asteroids split into two smaller asteroids when hit, instead of just shrinking in place.

  • Add screen wrapping: objects that leave one edge of the screen reappear on the opposite edge.

  • Add a visual effect when an asteroid is destroyed (e.g., a brief flash or explosion).