Exercises - Interfaces, Inner Classes and Events

Some of the exercises below provide sample runs to better clarify what the program in question is supposed to do. In these sample runs, text given in green has been typed by the user, while white text has been output by the program. Additionally, the "$" symbol indicates the command prompt, while the "↵" symbol indicates the user has pressed the return key.

Exercises identified by the ACM logo (shown at left) require one or more of the following libraries:
acm.jar, acm.breadboards.jar, or acm.toys.jar.

  1. Create a class named DraggableGOval that extends the GOval class in such a way that once an instance of this class has been added to the canvas, this instance can repeatedly be dragged with the mouse to some new location.

    The creation and use of objects of this type should be virtually identical to that of GRect objects, as the below code suggests:

    
    import java.awt.Color;
    
    import acm.breadboards.OneButtonBreadboard;
    
    public class DraggableGOvalTest extends OneButtonBreadboard {
        
        public void run() {
            DraggableGOval oval = new DraggableGOval(100,100);
            oval.setLocation(200,200);
            oval.setFilled(true);
            oval.setFillColor(Color.RED);
            this.add(oval);
        }
    }
    
     
    
    import java.awt.Color;
    import java.awt.event.MouseEvent;
    import java.awt.event.MouseListener;
    import java.awt.event.MouseMotionListener;
    
    import acm.graphics.GOval;
    
    public class DraggableGOval extends GOval {
        
      // instance variable representing the difference in x-coordinates between   
      // where this object was clicked and where it's top left corner is..
      private double xOffset;  
    
      // instance variable representing the difference in y-coordinates between   
      // where this object was clicked and where it's top left corner is..
      private double yOffset;  
        
      public DraggableGOval(double width, double height) {
        super(width, height);
            
        this.addMouseListener(new MouseListener() {
    
          public void mouseClicked(MouseEvent e) {}
    
          public void mousePressed(MouseEvent e) {
            if (DraggableGOval.this.contains(e.getX(),e.getY())) {
              DraggableGOval.this.xOffset = e.getX()-DraggableGOval.this.getX();
              DraggableGOval.this.yOffset = e.getY()-DraggableGOval.this.getY();
            }                   
          }
    
          public void mouseReleased(MouseEvent e) {
            isBeingDragged = false;                 
          }
    
          public void mouseEntered(MouseEvent e) {}
          public void mouseExited(MouseEvent e) {}
          });
            
        this.addMouseMotionListener(new MouseMotionListener() {
    
          @Override
          public void mouseDragged(MouseEvent e) {
            double newX = e.getX()-DraggableGOval.this.xOffset;
            double newY = e.getY()-DraggableGOval.this.yOffset;
            DraggableGOval.this.setLocation(newX, newY);
          }
    
          @Override
          public void mouseMoved(MouseEvent e) {}
          });
      }
    }
    

  2. Write a class named SpinningBall that extends GCompound that when added to the canvas of any GraphicsProgram immediately shows itself spinning with no additional code. The following file provides the individual frames of the ball that should be used to accomplish the animation: ball_frames.zip

    The following code can be used to test the SpinningBall class. Upon running this code you should see the ball at right, spinning.

    
    import acm.program.GraphicsProgram;
    
    public class SpinningBallFun extends GraphicsProgram {
        
        public void run() {
            this.setSize(500,500);
            SpinningBall spinningBall = new SpinningBall();
            spinningBall.setLocation(120,120);
            this.add(spinningBall);
        }
    }
    
    
     
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    
    import javax.swing.Timer;
    
    import acm.graphics.GCompound;
    import acm.graphics.GImage;
    
    public class SpinningBall extends GCompound {
        
        private final int numFrames = 59; 
        private final int timerDelay = 50;
        
        private int currentFrame;
        private GImage[] frames;
        private Timer timer;
        
        
        public SpinningBall() {
            currentFrame = 0;
            
            frames = new GImage[numFrames];
            for (int i = 0; i < numFrames; i++) {
                frames[i] = new GImage("ball_frames/" + frameName(i+1));
            }
            this.add(frames[0]);
            
            timer = new Timer(timerDelay, new ActionListener() {
                public void actionPerformed(ActionEvent arg0) {
                    advanceFrame();
                }});
            timer.start();
        }
        
        private void advanceFrame() {
            this.remove(frames[currentFrame]);
            currentFrame = (currentFrame + 1) % numFrames;
            this.add(frames[currentFrame]);
        }
        
        private String frameName(int i) {
            // assuming no more than 9999 frames, returns
            // a string representing a filename of the form
            // ####.png where the digits preceding ".png"
            // are determined by the value of i, with leading 
            // zeros (examples: 0001.png, 0043.png, 7302.png)
            
            if (i < 10) return "000" + i + ".png";
            else if (i < 100) return "00" + i + ".png";
            else if (i < 1000) return "0" + i + ".png";
            else return i + ".png";
        }
    }
    

  3. Write classes named ShootingRange, Pidgeon, and Bullet in a package named skeetshooter, so that ShootingRange extends GraphicsProgram

    When ShootingRange is run the result should be a simulation of a shooting range, where one fires a rifle at clay pidgeons periodically launched up into the air.

    The up-arrow key on the keyboard should launch a clay pidgeon, while the spacebar should fire a bullet from the rifle. Only one bullet and/or pidgeon should be on the screen at any one time, so if the user strikes either key before its related element has left the screen, that key event should be ignored. When the pidgeon is launched, it should travel in a parbolic arc, traveling left, over the rifle. The rifle should fire at a slight leftward-pointing angle.

    The rifle should be in a fixed position, so success in hitting the pidgeon with the bullet will be a matter of timing, not aiming.

    Both the pidgeon and the bullet should get slightly smaller as they remain in the air, to give the illusion that they are getting farther away (although this effect will be more noticeable with the pidgeon, than it will with the bullet, as the bullet already starts out fairly small).

    The images linked to below can be used for the in-game graphics.


     
    package skeetshooter;
    
    import java.awt.Color;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.awt.event.KeyEvent;
    import java.awt.event.KeyListener;
    
    import javax.swing.Timer;
    
    import acm.graphics.GImage;
    import acm.graphics.GRect;
    import acm.program.GraphicsProgram;
    
    public class ShootingRange extends GraphicsProgram {
        
        private final int TIMER_DELAY = 2;
        private final int SPACEBAR = 32;
        private final int UP_ARROW = 38;
        private final int WINDOW_WIDTH = 800;
        private final int WINDOW_HEIGHT = 500;
        
        private Pidgeon pidgeon;
        private Bullet bullet;
        private Timer timer;
        private GImage rifleImage;
        private GImage meadowImage;
        private GRect sky;
        
        public void init() {
            sky = new GRect(WINDOW_WIDTH, WINDOW_HEIGHT);
            sky.setFilled(true);
            sky.setFillColor(new Color(220,220,255));
            this.add(sky);
            
            meadowImage = new GImage("meadow.png");
            meadowImage.setLocation(0,WINDOW_HEIGHT - meadowImage.getHeight());
            this.add(meadowImage);
            
            rifleImage = new GImage("rifle.png");
            rifleImage.setLocation(WINDOW_WIDTH/2,
                                   WINDOW_HEIGHT 
                                     - rifleImage.getHeight()
                                     + this.getRegionPanel(SOUTH).getHeight());
            this.add(rifleImage);
            
            timer = new Timer(TIMER_DELAY, new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    if (pidgeon != null) {
                        pidgeon.movePerTick();
                        if (pidgeon.getY() > WINDOW_HEIGHT) {
                            ShootingRange.this.remove(pidgeon);
                            pidgeon = null;
                        }
                    }
                    if (bullet != null) {
                        bullet.movePerTick();
                        if (bullet.getY() < 0) {
                            ShootingRange.this.remove(bullet);
                            bullet = null;
                        }
                    }
                    if ((bullet != null) && (pidgeon != null)) {
                        if (bullet.getBounds().intersects(
                                               pidgeon.getBounds())) {
                            pidgeon.explode();
                        }
                    }
                }});
            timer.start();
        }
        
        public void launchPidgeon() {
            pidgeon = new Pidgeon(WINDOW_WIDTH,WINDOW_HEIGHT - 30);
            pidgeon.setVelocity(-2, -2.9);
            this.add(pidgeon);
        }
        
        public void shoot() {
            bullet = new Bullet(WINDOW_WIDTH / 2, 
                                WINDOW_HEIGHT - rifleImage.getHeight());
            bullet.setVelocity(-2.6, -6);
            this.add(bullet);
        }
        
        public void run() {
            this.setSize(WINDOW_WIDTH,WINDOW_HEIGHT);
            this.addKeyListeners(new KeyListener() {
                public void keyPressed(KeyEvent e) {
                    switch (e.getKeyCode()) {
                    case SPACEBAR : if (bullet == null)
                                       shoot(); 
                                    break;
                    case UP_ARROW : if (pidgeon == null)
                                       launchPidgeon(); 
                                    break; 
                    }
                }
                public void keyReleased(KeyEvent e) {}
                public void keyTyped(KeyEvent e) {}
            });
            this.getGCanvas().requestFocus();
        }
    }
    
    
    package skeetshooter;
    
    import java.awt.Color;
    
    import acm.graphics.GCompound;
    import acm.graphics.GImage;
    import acm.graphics.GOval;
    
    public class Pidgeon extends GCompound {
        
        private final double PIDGEON_WIDTH = 40;
        private final double SHRINKAGE_RATE = 0.997;
     
        private GOval bottom;
        private GOval top;
        private GImage explosion;
        private double xVelocity;
        private double yVelocity;
        private double yAcceleration = 0.01;
        
        public Pidgeon(double x, double y) {
            this.bottom = new GOval(0,0);
            this.bottom.setFilled(true);
            this.bottom.setFillColor(Color.RED);
            
            this.top = new GOval(0,0);
            this.top.setFilled(true);
            this.top.setFillColor(Color.ORANGE);
            
            this.setWidth(PIDGEON_WIDTH);
            this.add(bottom);
            this.add(top);
            this.setLocation(x,y);
            
            this.explosion = new GImage("explosion.png"); 
            this.explosion.setLocation(-explosion.getWidth()/2,
                                       -explosion.getHeight()/2);
        }
        
        public double getWidth() {
            return bottom.getWidth();
        }
        
        public void setWidth(double width) {
            this.bottom.setSize(width,width/2);
            this.top.setSize(width,width/2);
            this.bottom.setLocation(-width/2,-width/6+width/20);
            this.top.setLocation(-width/2,-width/6-width/20);
        }
        
        public void setVelocity(double vx, double vy) {
            this.xVelocity = vx;
            this.yVelocity = vy;
        }
        
        public void explode() {
            this.add(explosion);
        }
        
        public void movePerTick() {
            this.move(xVelocity, yVelocity);
            this.yVelocity += yAcceleration;
            this.setWidth(SHRINKAGE_RATE * this.getWidth());
        }
    
    }
    
    
    package skeetshooter;
    
    import java.awt.Color;
    
    import acm.graphics.GCompound;
    import acm.graphics.GOval;
    
    public class Bullet extends GCompound {
        
        private final double ACCELERATION_DUE_TO_GRAVITY = 0.01;
        private double SHRINKAGE_RATE = 0.995;
        private int DIAMETER= 10;
        
        private double xVelocity;
        private double yVelocity;
        
        private GOval oval;
        
        public Bullet(double x, double y) {
            this.oval = new GOval(50,50);
            this.oval.setFilled(true);
            this.oval.setFillColor(Color.BLACK);
            this.add(oval);
            this.setDiameter(DIAMETER);
            this.setLocation(x,y);
        }
        
        public double getDiameter() {
            return oval.getWidth();
        }
        
        public void setDiameter(double diameter) {
            this.oval.setSize(diameter,diameter);
            this.oval.setLocation(-diameter/2,-diameter/2);
        }
    
        public void setVelocity(double vx, double vy) {
            this.xVelocity = vx;
            this.yVelocity = vy;
        }
        
        public void movePerTick() {
            this.move(xVelocity, yVelocity);
            this.yVelocity += ACCELERATION_DUE_TO_GRAVITY;
            this.setDiameter(SHRINKAGE_RATE * this.getDiameter()); 
        }
    }