The focus of this prep is to give you more experience writing interacting objects, and to include a click listener in the animation.
The program we will write produces an animation with a space ship and an asteroid. The ship can shoot rockets at the asteroid, and if a rocket hits the asteroid, the asteroid disappears.
Click here to try it out. You can move the blue ship with the arrow keys, and you can shoot a rocket at the asteroid by clicking somewhere in the window - the rocket will start at the middle of the ship and move toward the position of the click. You can have as many rockets as you like. Try to hit the asteroid!
To get started, create a new project in Eclipse called "prep5". Then create a new classes called "Prep5", "Ship", "Asteroid" and "Rocket". Make sure they all have the right imports and they all extend Animated.
Before starting it is a good idea to think about what each object is going to do. Clearly the Ship class will draw the ship, the Asteroid class will draw the asteroid, and the Rocket class will draw the rockets. It makes sense that the Prep5 class will create the Ship and the Rocket objects, since they are needed as soon as the animation starts up.
But where should the Rocket objects be created? Again, it seems to make sense that they be created by the Ship. This, of course, implies that the best place to react to clicks is in the Ship class - when a click happens the Ship will find out about it and create a Rocket object (and include it in the animation).
When the animation starts up we will have a Ship and an Asteroid object, and when a click happens we will get a Rocket object. Now we have to decide where the code that detects when the rocket hits the asteroid is going to be. Wherever it is, the code will have to know the position and radius of both the rocket and the asteroid. If we put that code in the Asteroid class, then the Asteroid class would have to know about every Rocket object, and we haven't covered how to do that in Java yet. Instead we will put the code to detect a hit in the Rocket class, and this means that the rocket will have be able to get the position of the asteroid, which means that the Rocket class will have to contain a reference to the Asteroid object. But then since the Ship class creates the Rocket objects, it will have to give the Rocket objects a reference to the Asteroid object, which implies that the Ship object will also have to contain a reference to the Asteroid object.
So to summarize:
So let's get started with the Prep5 class.
import animator.*;
import java.awt.*;
/**
* Start up a game with a ship, asteroid and rockets.
*/
public class Prep5 extends Animated {
/**
* Create the asteroid and ship and add them to the animation.
*/
public void startup() {
// create asteroid with a given initial position (100,100)
Asteroid asteroid = new Asteroid(100,100);
animator.include(asteroid);
// create ship
Ship ship = new Ship(asteroid);
animator.include(ship);
} // end of startup method
} // end of Prep5 class
|
Now let's get a start on the asteroid class. All we'll do right now is get it to move, which is nothing you haven't done already:
import animator.*;
import java.awt.*;
/**
* An asteroid in a simple game
*/
public class Asteroid extends Animated {
// named constants
private final int radius = 10;
private final int diameter = 2*radius;
// properties
private int x;
private int y;
private int xdir = 1;
private int ydir = 1;
/**
* Save the initial coordinates for the asteroid.
*
* @param initx The initial x-coordinate.
* @param inity The initial y-coordinate.
*/
public Asteroid(int initx, int inity) {
x = initx;
y = inity;
} // end of constructor
/**
* Draw the asteroid.
*/
public void draw() {
// draw the asteroid
screen.setColor(Color.black);
screen.fillOval(x-radius,y-radius,diameter,diameter);
// move it
x += xdir;
y += ydir;
// bounce if off the walls
if (x == radius) {
xdir = 1;
}
if (x == animator.getSceneWidth()-radius) {
xdir = -1;
}
if (y == radius) {
ydir = 1;
}
if (y == animator.getSceneHeight()-radius) {
ydir = -1;
}
} // end of draw method
} // end of Asteroid class
|
Next comes the Ship class. First let's just get it to move according to the arrow keys and to save a reference to the Asteroid object (via the constructor):
import animator.*;
import java.awt.*;
/**
* A spaceship in a simple game.
*/
public class Ship extends Animated {
// named constants
private final int radius = 20;
private final int diameter = 2*radius;
// properties
private Asteroid asteroid;
/**
* Save the reference to the asteroid.
*
* @param ast A reference to an asteroid.
*/
public Ship(Asteroid ast) {
asteroid = ast;
} // end of constructor
/**
* Draw the ship using arrow keys.
*
* @param x The x-coordinate from the animator.
* @param y The y-coordinate from the animator.
*/
public void draw(int x, int y) {
// draw the ship
screen.setColor(Color.blue);
screen.fillOval(x-radius,y-radius,diameter,diameter);
} // end of draw method
} // end of Ship class
|
You should run this and make sure that the asteroid and ship move correctly.
Now lets modify the Ship class to react to clicks. To do this, we need to make the Ship class "implement ClickListener" and have a "public void click(int x, int y)" method:
public class Ship extends Animated implements ClickListener {
// named constants
private final int radius = 20;
private final int diameter = 2*radius;
// properties
private Asteroid asteroid;
/**
* Save the reference to the asteroid.
*
* @param ast A reference to an asteroid.
*/
public Ship(Asteroid ast) {
asteroid = ast;
} // end of constructor
/**
* Draw the ship using arrow keys.
*
* @param x The x-coordinate from the animator.
* @param y The y-coordinate from the animator.
*/
public void draw(int x, int y) {
// draw the ship
screen.setColor(Color.blue);
screen.fillOval(x-radius,y-radius,diameter,diameter);
} // end of draw method
/**
* Respond to a click on the screen.
*
* @param x The x-coordinate of the click.
* @param y The y-coordinate of the click.
*/
public void click(int x, int y) {
} // end of click method
} // end of Ship class
|
We also need to modify the Prep5 class to tell the animator which object is the click listener. All that is needed is to add the line:
animator.setClickListener(ship);
to the end of the startup method.
Now we need to add code to the click method to create a Rocket and give it a reference to the Asteroid object. Creating the object is easy, but we also need to tell it where to start and in what direction to move (i.e., toward the click). This takes a little trig, show I'll just show you the code and you can figure it out.
public void click(int x, int y) {
// compute movement rates in both directions
double xdist = x - savex;
double ydist = y - savey;
double hyp = Math.sqrt(xdist*xdist + ydist*ydist);
double xrate = xdist/hyp;
double yrate = ydist/hyp;
// create a new rocket and add it to the animation
Rocket r = new Rocket(savex,savey,xrate,yrate,asteroid);
animator.include(r);
} // end of click method
} // end of Ship class
|
Note that the code uses variables "savex" and "savey". These are the coordinates of the center of the ship, and need to be saved by the draw method in new instance variables. I'll leave it as an exercise to put these in.
Here's the beginnings of the Rocket class:
import animator.*;
import java.awt.*;
/**
* A rocket ship in a simple game.
*/
public class Rocket extends Animated {
// named constants
private final int radius = 5;
private final int diameter = 2*radius;
// properties
private double x;
private double y;
private double xdir;
private double ydir;
private Asteroid asteroid;
/**
* Save initial coordinates, initial movement rates, and a reference to the
* asteroid.
*
* @param initx The initial x-coordinate.
* @param inity The initial y-coordinate.
* @param xd The amount to move each tick in the x direction.
* @param yd The amount to move each tick in the y direction.
* @param ast A reference to the asteroid.
*/
public Rocket(int initx, int inity, double xd, double yd, Asteroid ast) {
x = initx;
y = inity;
xdir = xd;
ydir = yd;
asteroid = ast;
} // end of constructor
/**
* Draw the rocket and check for hitting the asteroid.
*/
public void draw() {
// draw the rocket
screen.setColor(Color.red);
screen.fillOval((int)x-radius,(int)y-radius,diameter,diameter);
// move the rocket
x += xdir;
y += ydir;
} // end of draw method
} // end of Rocket class
|
Notice that we are keeping track of the position of the rocket using double's instead of int's. This is because the distance being moved on each tick could be less than 1 but not 0. For example, suppose it moves 0.5 in the x-direction on each tick. Then over several ticks the x-coordinate should change from 0.0 to 0.5 to 1.0 to 1.5 to 2.0, etc. If we used an int to keep track of the x-coordinate, then if we add 0.5 to it each time it will never change since we always have to truncate back to an int and the 0.5 goes away.
Now try this out and see if it works. You should now be able to click on the screen and have a rocket appear and start moving toward the click.
Finally we have to make the asteroid disappear when a rocket hits it. For this to happen the Rocket class needs to have code that checks for the hit, first getting the position and radius of the asteroid. Here's the new draw method in the Rocket class:
public void draw() {
// draw the rocket
screen.setColor(Color.red);
screen.fillOval((int)x-radius,(int)y-radius,diameter,diameter);
// see if it hits the asteroid
int ax = asteroid.getX();
int ay = asteroid.getY();
int ar = asteroid.getRadius();
double dist = Math.sqrt((x-ax)*(x-ax)+(y-ay)*(y-ay));
if (dist < radius+ar) {
// if so, tell the asteroid it has been hit
asteroid.hit();
}
// move the rocket
x += xdir;
y += ydir;
} // end of draw method
|
And the following methods are added to the Asteroid class:
/**
* Get the current x-coordinate of this asteroid.
*
* @return the x-coordinate.
*/
public int getX() {
return x;
} // end of getX method
/**
* Get the current y-coordinate of this asteroid.
*
* @return the y-coordinate.
*/
public int getY() {
return y;
} // end of getY method
/**
* Get the radius of this asteroid.
*
* @return the radius.
*/
public int getRadius() {
return radius;
} // end of getRadius method
/**
* Record that this asteroid has been hit.
*/
public void hit() {
alive = false;
} // end of hit method
|
All that needs to be done now is to add an instance variable to the Asteroid class:
private boolean alive = true;
and modify the draw method in the Asteroid class to only draw the asteroid if "alive" is true:
// draw the asteroid if still alive
screen.setColor(Color.black);
if (alive) {
screen.fillOval(x-radius,y-radius,diameter,diameter);
}
This should be all that is needed. Try it out and see if it works. If not, see if you can fix it. If you can't, get some help.
For practice before your lab, try make the changes necessary to:
That's it for this preparation. Good luck in your next lab.