import java.util.*; import java.net.*; import java.awt.*; import java.awt.event.*; import java.applet.*; import javax.swing.*; class CONSTANTS { public static final double SleepTime = 25; // measured in milliseconds } /////////////////////////////////////////////////////////////////////////////// // Class: GameObject // This class captures everything we need to work with an object within the game. // Specifically, it handles drawing the object and moving the object. /////////////////////////////////////////////////////////////////////////////// class GameObject { // properties double x, y; // position double direction; // where it's going (an angle in radians) double speed; // how fast it's going Applet applet; // the applet this object is a part of Image graphic; // image used draw the object int graphicWidth, graphicHeight; // the dimensions of the image // Method: The "default" constructor (no arguments). // This is called anytime we see: // GameObject foo = new GameObject(); public GameObject () { // set all of the properties to reasonable defaults x = 0; y = 0; direction = 0; speed = 0; graphic = null; applet = null; } // Method: A constructor with arguments. // This is called anytime we see: // GameObject foo = new GameObject(x,y,direction,speed,image,scene); public GameObject(double posX, double posY, double newDir, double newSpeed, Image img, Applet a) { // simply set all of the properties to the arguments provided x = posX; y = posY; direction = Math.toRadians(newDir); speed = newSpeed; applet = a; graphic = img; // extract the width and height of the image and set those properites graphicWidth = img.getWidth(a); graphicHeight = img.getHeight(a); } // Method: Used to update the location of the object void updateLocation() { if (speed > 0) { x = x + Math.cos(direction) * (speed * CONSTANTS.SleepTime / 1000.0); y = y + Math.sin(direction) * (speed * CONSTANTS.SleepTime / 1000.0); } } int getDamageAbility() { return 0; } void receiveDamage(int damage) { } boolean isDestroyed() { return false; } boolean canBeDamaged() { return false; } // Method: Used to actually draw the object to a given Graphics space. void draw(Graphics g) { // if dimensions of image has been updated, change them if (graphicWidth != graphic.getWidth(applet) || graphicHeight != graphic.getHeight(applet)) { graphicWidth = graphic.getWidth(applet); graphicHeight = graphic.getHeight(applet); } // draw the actual image int middleX = (int) x - (graphicWidth / 2); int middleY = (int) y - (graphicHeight / 2); g.drawImage(graphic, middleX, middleY, applet); } } /////////////////////////////////////////////////////////////////////////////// // class: CrossHair // This class is a special type of GameObject (hence, it extends the GameObject // class), where instead of drawing an image, it draws a "crosshair" (a circle // and two lines). As such, we "override" the draw method of the GameObject // class to, instead of drawing an image, to draw a crosshair. /////////////////////////////////////////////////////////////////////////////// class CrossHair extends GameObject { // Method: Used to draw the crosshair void draw(Graphics g) { // note: the x,y will be the upper-left corner. to draw a 40x40 crosshair, // we thus need the following... int radius = 20; // Draw the circle and then the two lines that make up the crosshair g.setColor(Color.black); g.drawOval((int) x - radius, (int) y - radius, 2 * radius, 2 * radius); g.drawLine((int) x, (int) y - radius, (int) x, (int) y + radius); g.drawLine((int) x - radius, (int) y, (int) x + radius, (int) y); } } class Weapon extends GameObject { int damagePoints; public Weapon(double posX, double posY, double newDir, double newSpeed, int damage, Image img, Applet a) { super(posX, posY, newDir, newSpeed, img, a); damagePoints = damage; } int getDamageAbility() { return damagePoints; } } class Enemy extends GameObject { int hitCapacity; // how many "hits" the object *can* take int hits = 0; // how many "hits" the object *has* taken public Enemy(double posX, double posY, double newDir, double newSpeed, int maxHits, Image img, Applet a) { super(posX, posY, newDir, newSpeed, img, a); hitCapacity = maxHits; } void receiveDamage(int damage) { hits = hits + damage; } boolean isDestroyed() { return (hits >= hitCapacity); } boolean canBeDamaged() { return true; } void updateLocation() { super.updateLocation(); if (x < 0) { x = -1 * x; direction = Math.PI - direction; } if (x > applet.getWidth()) { x = applet.getWidth() - (x - applet.getWidth()); direction = Math.PI - direction; } if (y < 0) { y = -1 * y; direction = -1 * direction; } if (y > applet.getHeight()) { y = applet.getHeight() - (y - applet.getHeight()); direction = -1 * direction; } } void draw(Graphics g) { // Call the GameObject.draw() method to draw the image super.draw(g); // Draw the health bar atop the image, indicating how many hits the // enemy has taken and how many more it can take int rectangleHeight = 5; int middleX = (int) x - (graphicWidth / 2); int middleY = (int) y - (graphicHeight / 2); g.setColor(Color.black); g.drawRect(middleX, middleY, graphicWidth + 1, rectangleHeight + 1); g.setColor(Color.red); g.fillRect(middleX + 1, middleY + 1, graphicWidth, rectangleHeight); g.setColor(Color.green); double healthPercent = (double) (hitCapacity - hits) / (double) hitCapacity; g.fillRect(middleX + 1, middleY + 1, (int) (graphicWidth * healthPercent), rectangleHeight); } } /////////////////////////////////////////////////////////////////////////////// // Class: GameScene // This class is responsible for storing a collection of drawable objects and // rendering all of them when needed. /////////////////////////////////////////////////////////////////////////////// class GameScene extends JPanel { // properties... ArrayList knownObjects; // list of all game objects in the scene int width = 0, height = 0; // dimensions of the scene Applet applet; // the applet this scene is a part of // Method: A constructor that takes the width and height of the scene public GameScene(Applet a, int w, int h) { knownObjects = new ArrayList(); applet = a; width = w; height = h; } // Method: Used to retrieve the number of game objects int getNumberObjects() { return knownObjects.size(); } // Method: Used to retrieve a specific game object (by it's index in the array // of known objects). GameObject getObject(int index) { return knownObjects.get(index); } // Method: Used to add a new game object to the scene synchronized void add(GameObject thing) { knownObjects.add(thing); } // Method: Used to draw all of the game objects public void paintComponent(Graphics g) { // clear everything from the screen g.setColor(Color.white); g.fillRect(0, 0, width, height); // display all of the objects for (int i = 0; i < knownObjects.size(); i++) knownObjects.get(i).draw(g); } // Method: Used to update the locations of all the game objects synchronized void updateLocations() { for (int i = 0; i < knownObjects.size(); i++) { // get the game object at index i... GameObject foo = knownObjects.get(i); // update it's location foo.updateLocation(); // if it has moved off the screen, remove it from the scene if (foo.x < 0 || foo.x > width || foo.y < 0 || foo.y > height) { knownObjects.remove(i); } else { // check to see if this object (foo) collides with any other // object (bar)... for (int j = i + 1; j < knownObjects.size(); j++) { GameObject bar = knownObjects.get(j); if ((foo.canBeDamaged() && bar.getDamageAbility() > 0) || (bar.canBeDamaged() && foo.getDamageAbility() > 0)) { double diffX = foo.x - bar.x; double diffY = foo.y - bar.y; double distance = Math.sqrt((diffX * diffX) + (diffY * diffY)); // If the following is true, then object foo collides with // object bar. if (distance <= 40) { if (foo.canBeDamaged()) { foo.receiveDamage(bar.getDamageAbility()); if (foo.isDestroyed()) knownObjects.remove(i); knownObjects.remove(j); } else { bar.receiveDamage(foo.getDamageAbility()); if (bar.isDestroyed()) knownObjects.remove(j); knownObjects.remove(i); } } } } } } } } /////////////////////////////////////////////////////////////////////////////// // Main Applet Class // This is the class that's loaded and controls the program. /////////////////////////////////////////////////////////////////////////////// public class AngryHomer extends Applet implements KeyListener, MouseListener, MouseMotionListener, Runnable { // properties... GameScene scene; GameObject homer, donut; CrossHair cross; Enemy ned, burns, abe; Image imgHomer, imgDonut, imgNed, imgBurns, imgAbe; // This is the thread we use to auto-magically execute the "run" method // side-by-side with the rest of the applet. Thread runner = null; // Method: Used to load all of the images we'll use void loadImages() { try { imgHomer = Toolkit.getDefaultToolkit().getImage(new URL(getCodeBase(), "homer.gif")); imgDonut = Toolkit.getDefaultToolkit().getImage(new URL(getCodeBase(), "donut-small.gif")); imgNed = Toolkit.getDefaultToolkit().getImage(new URL(getCodeBase(), "ned.gif")); imgBurns = Toolkit.getDefaultToolkit().getImage(new URL(getCodeBase(), "burns.gif")); imgAbe = Toolkit.getDefaultToolkit().getImage(new URL(getCodeBase(), "abe.gif")); } catch (Exception e) { } } // Method: Given two points, a source x,y and a destination x,y, this will // compute and return the angle (in degrees) from source to destination. double getDirection(int sx, int sy, int dx, int dy) { double diffX = dx - sx; double diffY = dy - sy; double hypotenuse = Math.sqrt((diffX * diffX) + (diffY * diffY)); double direction = Math.toDegrees(Math.asin(diffY / hypotenuse)); if (sx > dx && sy < dy) direction = 180 - direction; if (sx > dx && sy > dy) direction = -180 - direction; return direction; } // Method: init (called when applet is first loaded by browser) public void init() { // Create space area and add it to the display scene = new GameScene(this, getSize().width, getSize().height); setLayout(new BorderLayout()); add("Center", scene); // Register all of the event listeners we use addKeyListener(this); addMouseListener(this); addMouseMotionListener(this); this.setFocusable(true); // start building the scene... // load all images loadImages(); // add homer homer = new GameObject(scene.width / 2, scene.height / 2, 0, 0, imgHomer, this); scene.add(homer); // add the crosshair cross = new CrossHair(); scene.add(cross); // create ned, burns, abe, and add them to the scene ned = new Enemy(50, 200, 45, 200, 20, imgNed, this); burns = new Enemy(350, 20, 100, 120, 10, imgBurns, this); //abe = new Enemy(650, 150, 194, 80, 5, imgAbe, this); scene.add(ned); scene.add(burns); //scene.add(abe); // Create the thread try { runner = new Thread(this); runner.start(); } catch (Exception e) { System.out.println("failure to create thread"); } } /////////////////////////////////////////////////////////////////////////// // Handlers for the "MouseListener" interface. These are the methods that // are called anytime the user clicks the mouse. /////////////////////////////////////////////////////////////////////////// public void run() { int count = 0; while (true) { // Sleep for 10 milliseconds (1/100 of a second) try { runner.sleep((int)CONSTANTS.SleepTime); } catch (Exception e) { } // Update the position of all game objects and re-draw the scene scene.updateLocations(); scene.repaint(); // NOTE: the repaint() method actually calls the "paintComponent" // method of the GameScene class. // debug: every second output how many objects are in the game scene // to the console if (count++ % 100 == 0) { System.out.println("num objects: " + scene.getNumberObjects()); } } } /////////////////////////////////////////////////////////////////////////// // Handlers for the "KeyListener" interface. These are the methods that // are called anytime the user presses any keys on the keyboard. /////////////////////////////////////////////////////////////////////////// // Method: this is called whenever the user hits a key on the keyboard public synchronized void keyTyped(KeyEvent e) { // handle the keybindings appropriately by updating homer's position switch (e.getKeyChar()) { case 's': homer.y += 10; break; case 'w': homer.y -= 10; break; case 'a': homer.x -= 10; break; case 'd': homer.x += 10; break; } // Keep homer in the scene if (homer.x < 0) homer.x = 0; if (homer.y < 0) homer.y = 0; if (homer.x > scene.width) homer.x = scene.width; if (homer.y > scene.height) homer.y = scene.height; } // Although we don't use these, we *must* implement them b/c of the // KeyListener interface that we "implement". We don't want to actually // do anything for these events, so we leave them empty. public void keyReleased(KeyEvent e) { } public void keyPressed(KeyEvent e) { } /////////////////////////////////////////////////////////////////////////// // Handlers for the "MouseMotionListener" interface. These are the methods that // are called anytime the user moves the mouse. /////////////////////////////////////////////////////////////////////////// // Method: this is called whenever the user moves the mouse. public synchronized void mouseMoved(MouseEvent e) { // Update the location of the crosshair cross.x = e.getX(); cross.y = e.getY(); // Make sure we stay within the scene if (cross.x < 0) cross.x = 0; if (cross.y < 0) cross.y = 0; if (cross.x > scene.width) cross.x = scene.width; if (cross.y > scene.height) cross.y = scene.height; } // We don't use this method, but we must implement it b/c of the // MouseMostionListener interface that we implement. public void mouseDragged(MouseEvent e) { } /////////////////////////////////////////////////////////////////////////// // Handlers for the "MouseListener" interface. These are the methods that // are called anytime the user clicks the mouse. /////////////////////////////////////////////////////////////////////////// // Method: this is called anytime the user clicks one of the mouse // buttons. public synchronized void mouseClicked(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1) { // This is a left-click... // Compute the direction from homer to the click (i.e. where to shoot // the donut). double direction = getDirection((int)homer.x, (int)homer.y, e.getX(), e.getY()); // Add the donut to the scene Weapon donut = new Weapon(homer.x, homer.y, direction, 250, 1, imgDonut, this); scene.add(donut); } else if (e.getButton() == MouseEvent.BUTTON2) { // This is a middle-click } else if (e.getButton() == MouseEvent.BUTTON3) { // This is a right-click } } // Although we don't use these, we *must* implement them b/c of the // MouseListener interface that we "implement". We don't want to actually // do anything for these events, so we leave them empty. public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } public void mousePressed(MouseEvent e) { } public void mouseReleased(MouseEvent e) { } }