Filter3dTest.java Source code

Java tutorial

Introduction

Here is the source code for Filter3dTest.java

Source

/*
DEVELOPING GAME IN JAVA 
    
Caracteristiques
    
Editeur : NEW RIDERS 
Auteur : BRACKEEN 
Parution : 09 2003 
Pages : 972 
Isbn : 1-59273-005-1 
Reliure : Paperback 
Disponibilite : Disponible a la librairie 
*/

import java.awt.AWTException;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.DisplayMode;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Point;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;

/**
 * The Filter3dTest class demonstrates the Filter3d functionality. A fly buzzes
 * around the listener, and the closer the fly is, the louder it's heard.
 * 
 * @see Filter3d
 * @see SimpleSoundPlayer
 */
public class Filter3dTest extends GameCore {

    public static void main(String[] args) {
        new Filter3dTest().run();
    }

    private Sprite fly;

    private Sprite listener;

    private InputManager inputManager;

    private GameAction exit;

    private SimpleSoundPlayer bzzSound;

    private InputStream bzzSoundStream;

    public void init() {
        super.init();

        // set up input manager
        exit = new GameAction("exit", GameAction.DETECT_INITAL_PRESS_ONLY);
        inputManager = new InputManager(screen.getFullScreenWindow());
        inputManager.mapToKey(exit, KeyEvent.VK_ESCAPE);
        inputManager.setCursor(InputManager.INVISIBLE_CURSOR);

        createSprites();

        // load the sound
        bzzSound = new SimpleSoundPlayer("../sounds/fly-bzz.wav");

        // create the 3d filter
        Filter3d filter = new Filter3d(fly, listener, screen.getHeight());

        // create the filtered sound stream
        bzzSoundStream = new FilteredSoundStream(new LoopingByteInputStream(bzzSound.getSamples()), filter);

        // play the sound in a separate thread
        new Thread() {
            public void run() {
                bzzSound.play(bzzSoundStream);
            }
        }.start();
    }

    /**
     * Loads images and creates sprites.
     */
    private void createSprites() {
        // load images
        Image fly1 = loadImage("../images/fly1.png");
        Image fly2 = loadImage("../images/fly2.png");
        Image fly3 = loadImage("../images/fly3.png");
        Image ear = loadImage("../images/ear.png");

        // create "fly" sprite
        Animation anim = new Animation();
        anim.addFrame(fly1, 50);
        anim.addFrame(fly2, 50);
        anim.addFrame(fly3, 50);
        anim.addFrame(fly2, 50);

        fly = new Sprite(anim);

        // create the listener sprite
        anim = new Animation();
        anim.addFrame(ear, 0);
        listener = new Sprite(anim);
        listener.setX((screen.getWidth() - listener.getWidth()) / 2);
        listener.setY((screen.getHeight() - listener.getHeight()) / 2);
    }

    public void update(long elapsedTime) {
        if (exit.isPressed()) {
            stop();
        } else {
            listener.update(elapsedTime);
            fly.update(elapsedTime);
            fly.setX(inputManager.getMouseX());
            fly.setY(inputManager.getMouseY());
        }
    }

    public void stop() {
        super.stop();
        // stop the bzz sound
        try {
            bzzSoundStream.close();
        } catch (IOException ex) {
        }
    }

    public void draw(Graphics2D g) {

        // draw background
        g.setColor(new Color(0x33cc33));
        g.fillRect(0, 0, screen.getWidth(), screen.getHeight());

        // draw listener
        g.drawImage(listener.getImage(), Math.round(listener.getX()), Math.round(listener.getY()), null);

        // draw fly
        g.drawImage(fly.getImage(), Math.round(fly.getX()), Math.round(fly.getY()), null);
    }

}

/**
 * The SimpleSoundPlayer encapsulates a sound that can be opened from the file
 * system and later played.
 */

class SimpleSoundPlayer {

    public static void main(String[] args) {
        // load a sound
        SimpleSoundPlayer sound = new SimpleSoundPlayer("../sounds/voice.wav");

        // create the stream to play
        InputStream stream = new ByteArrayInputStream(sound.getSamples());

        // play the sound
        sound.play(stream);

        // exit
        System.exit(0);
    }

    private AudioFormat format;

    private byte[] samples;

    /**
     * Opens a sound from a file.
     */
    public SimpleSoundPlayer(String filename) {
        try {
            // open the audio input stream
            AudioInputStream stream = AudioSystem.getAudioInputStream(new File(filename));

            format = stream.getFormat();

            // get the audio samples
            samples = getSamples(stream);
        } catch (UnsupportedAudioFileException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Gets the samples of this sound as a byte array.
     */
    public byte[] getSamples() {
        return samples;
    }

    /**
     * Gets the samples from an AudioInputStream as an array of bytes.
     */
    private byte[] getSamples(AudioInputStream audioStream) {
        // get the number of bytes to read
        int length = (int) (audioStream.getFrameLength() * format.getFrameSize());

        // read the entire stream
        byte[] samples = new byte[length];
        DataInputStream is = new DataInputStream(audioStream);
        try {
            is.readFully(samples);
        } catch (IOException ex) {
            ex.printStackTrace();
        }

        // return the samples
        return samples;
    }

    /**
     * Plays a stream. This method blocks (doesn't return) until the sound is
     * finished playing.
     */
    public void play(InputStream source) {

        // use a short, 100ms (1/10th sec) buffer for real-time
        // change to the sound stream
        int bufferSize = format.getFrameSize() * Math.round(format.getSampleRate() / 10);
        byte[] buffer = new byte[bufferSize];

        // create a line to play to
        SourceDataLine line;
        try {
            DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
            line = (SourceDataLine) AudioSystem.getLine(info);
            line.open(format, bufferSize);
        } catch (LineUnavailableException ex) {
            ex.printStackTrace();
            return;
        }

        // start the line
        line.start();

        // copy data to the line
        try {
            int numBytesRead = 0;
            while (numBytesRead != -1) {
                numBytesRead = source.read(buffer, 0, buffer.length);
                if (numBytesRead != -1) {
                    line.write(buffer, 0, numBytesRead);
                }
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }

        // wait until all data is played, then close the line
        line.drain();
        line.close();

    }

}

/**
 * The InputManager manages input of key and mouse events. Events are mapped to
 * GameActions.
 */

class InputManager implements KeyListener, MouseListener, MouseMotionListener, MouseWheelListener {
    /**
     * An invisible cursor.
     */
    public static final Cursor INVISIBLE_CURSOR = Toolkit.getDefaultToolkit()
            .createCustomCursor(Toolkit.getDefaultToolkit().getImage(""), new Point(0, 0), "invisible");

    // mouse codes
    public static final int MOUSE_MOVE_LEFT = 0;

    public static final int MOUSE_MOVE_RIGHT = 1;

    public static final int MOUSE_MOVE_UP = 2;

    public static final int MOUSE_MOVE_DOWN = 3;

    public static final int MOUSE_WHEEL_UP = 4;

    public static final int MOUSE_WHEEL_DOWN = 5;

    public static final int MOUSE_BUTTON_1 = 6;

    public static final int MOUSE_BUTTON_2 = 7;

    public static final int MOUSE_BUTTON_3 = 8;

    private static final int NUM_MOUSE_CODES = 9;

    // key codes are defined in java.awt.KeyEvent.
    // most of the codes (except for some rare ones like
    // "alt graph") are less than 600.
    private static final int NUM_KEY_CODES = 600;

    private GameAction[] keyActions = new GameAction[NUM_KEY_CODES];

    private GameAction[] mouseActions = new GameAction[NUM_MOUSE_CODES];

    private Point mouseLocation;

    private Point centerLocation;

    private Component comp;

    private Robot robot;

    private boolean isRecentering;

    /**
     * Creates a new InputManager that listens to input from the specified
     * component.
     */
    public InputManager(Component comp) {
        this.comp = comp;
        mouseLocation = new Point();
        centerLocation = new Point();

        // register key and mouse listeners
        comp.addKeyListener(this);
        comp.addMouseListener(this);
        comp.addMouseMotionListener(this);
        comp.addMouseWheelListener(this);

        // allow input of the TAB key and other keys normally
        // used for focus traversal
        comp.setFocusTraversalKeysEnabled(false);
    }

    /**
     * Sets the cursor on this InputManager's input component.
     */
    public void setCursor(Cursor cursor) {
        comp.setCursor(cursor);
    }

    /**
     * Sets whether realtive mouse mode is on or not. For relative mouse mode,
     * the mouse is "locked" in the center of the screen, and only the changed
     * in mouse movement is measured. In normal mode, the mouse is free to move
     * about the screen.
     */
    public void setRelativeMouseMode(boolean mode) {
        if (mode == isRelativeMouseMode()) {
            return;
        }

        if (mode) {
            try {
                robot = new Robot();
                recenterMouse();
            } catch (AWTException ex) {
                // couldn't create robot!
                robot = null;
            }
        } else {
            robot = null;
        }
    }

    /**
     * Returns whether or not relative mouse mode is on.
     */
    public boolean isRelativeMouseMode() {
        return (robot != null);
    }

    /**
     * Maps a GameAction to a specific key. The key codes are defined in
     * java.awt.KeyEvent. If the key already has a GameAction mapped to it, the
     * new GameAction overwrites it.
     */
    public void mapToKey(GameAction gameAction, int keyCode) {
        keyActions[keyCode] = gameAction;
    }

    /**
     * Maps a GameAction to a specific mouse action. The mouse codes are defined
     * herer in InputManager (MOUSE_MOVE_LEFT, MOUSE_BUTTON_1, etc). If the
     * mouse action already has a GameAction mapped to it, the new GameAction
     * overwrites it.
     */
    public void mapToMouse(GameAction gameAction, int mouseCode) {
        mouseActions[mouseCode] = gameAction;
    }

    /**
     * Clears all mapped keys and mouse actions to this GameAction.
     */
    public void clearMap(GameAction gameAction) {
        for (int i = 0; i < keyActions.length; i++) {
            if (keyActions[i] == gameAction) {
                keyActions[i] = null;
            }
        }

        for (int i = 0; i < mouseActions.length; i++) {
            if (mouseActions[i] == gameAction) {
                mouseActions[i] = null;
            }
        }

        gameAction.reset();
    }

    /**
     * Gets a List of names of the keys and mouse actions mapped to this
     * GameAction. Each entry in the List is a String.
     */
    public List getMaps(GameAction gameCode) {
        ArrayList list = new ArrayList();

        for (int i = 0; i < keyActions.length; i++) {
            if (keyActions[i] == gameCode) {
                list.add(getKeyName(i));
            }
        }

        for (int i = 0; i < mouseActions.length; i++) {
            if (mouseActions[i] == gameCode) {
                list.add(getMouseName(i));
            }
        }
        return list;
    }

    /**
     * Resets all GameActions so they appear like they haven't been pressed.
     */
    public void resetAllGameActions() {
        for (int i = 0; i < keyActions.length; i++) {
            if (keyActions[i] != null) {
                keyActions[i].reset();
            }
        }

        for (int i = 0; i < mouseActions.length; i++) {
            if (mouseActions[i] != null) {
                mouseActions[i].reset();
            }
        }
    }

    /**
     * Gets the name of a key code.
     */
    public static String getKeyName(int keyCode) {
        return KeyEvent.getKeyText(keyCode);
    }

    /**
     * Gets the name of a mouse code.
     */
    public static String getMouseName(int mouseCode) {
        switch (mouseCode) {
        case MOUSE_MOVE_LEFT:
            return "Mouse Left";
        case MOUSE_MOVE_RIGHT:
            return "Mouse Right";
        case MOUSE_MOVE_UP:
            return "Mouse Up";
        case MOUSE_MOVE_DOWN:
            return "Mouse Down";
        case MOUSE_WHEEL_UP:
            return "Mouse Wheel Up";
        case MOUSE_WHEEL_DOWN:
            return "Mouse Wheel Down";
        case MOUSE_BUTTON_1:
            return "Mouse Button 1";
        case MOUSE_BUTTON_2:
            return "Mouse Button 2";
        case MOUSE_BUTTON_3:
            return "Mouse Button 3";
        default:
            return "Unknown mouse code " + mouseCode;
        }
    }

    /**
     * Gets the x position of the mouse.
     */
    public int getMouseX() {
        return mouseLocation.x;
    }

    /**
     * Gets the y position of the mouse.
     */
    public int getMouseY() {
        return mouseLocation.y;
    }

    /**
     * Uses the Robot class to try to postion the mouse in the center of the
     * screen.
     * <p>
     * Note that use of the Robot class may not be available on all platforms.
     */
    private synchronized void recenterMouse() {
        if (robot != null && comp.isShowing()) {
            centerLocation.x = comp.getWidth() / 2;
            centerLocation.y = comp.getHeight() / 2;
            SwingUtilities.convertPointToScreen(centerLocation, comp);
            isRecentering = true;
            robot.mouseMove(centerLocation.x, centerLocation.y);
        }
    }

    private GameAction getKeyAction(KeyEvent e) {
        int keyCode = e.getKeyCode();
        if (keyCode < keyActions.length) {
            return keyActions[keyCode];
        } else {
            return null;
        }
    }

    /**
     * Gets the mouse code for the button specified in this MouseEvent.
     */
    public static int getMouseButtonCode(MouseEvent e) {
        switch (e.getButton()) {
        case MouseEvent.BUTTON1:
            return MOUSE_BUTTON_1;
        case MouseEvent.BUTTON2:
            return MOUSE_BUTTON_2;
        case MouseEvent.BUTTON3:
            return MOUSE_BUTTON_3;
        default:
            return -1;
        }
    }

    private GameAction getMouseButtonAction(MouseEvent e) {
        int mouseCode = getMouseButtonCode(e);
        if (mouseCode != -1) {
            return mouseActions[mouseCode];
        } else {
            return null;
        }
    }

    // from the KeyListener interface
    public void keyPressed(KeyEvent e) {
        GameAction gameAction = getKeyAction(e);
        if (gameAction != null) {
            gameAction.press();
        }
        // make sure the key isn't processed for anything else
        e.consume();
    }

    // from the KeyListener interface
    public void keyReleased(KeyEvent e) {
        GameAction gameAction = getKeyAction(e);
        if (gameAction != null) {
            gameAction.release();
        }
        // make sure the key isn't processed for anything else
        e.consume();
    }

    // from the KeyListener interface
    public void keyTyped(KeyEvent e) {
        // make sure the key isn't processed for anything else
        e.consume();
    }

    // from the MouseListener interface
    public void mousePressed(MouseEvent e) {
        GameAction gameAction = getMouseButtonAction(e);
        if (gameAction != null) {
            gameAction.press();
        }
    }

    // from the MouseListener interface
    public void mouseReleased(MouseEvent e) {
        GameAction gameAction = getMouseButtonAction(e);
        if (gameAction != null) {
            gameAction.release();
        }
    }

    // from the MouseListener interface
    public void mouseClicked(MouseEvent e) {
        // do nothing
    }

    // from the MouseListener interface
    public void mouseEntered(MouseEvent e) {
        mouseMoved(e);
    }

    // from the MouseListener interface
    public void mouseExited(MouseEvent e) {
        mouseMoved(e);
    }

    // from the MouseMotionListener interface
    public void mouseDragged(MouseEvent e) {
        mouseMoved(e);
    }

    // from the MouseMotionListener interface
    public synchronized void mouseMoved(MouseEvent e) {
        // this event is from re-centering the mouse - ignore it
        if (isRecentering && centerLocation.x == e.getX() && centerLocation.y == e.getY()) {
            isRecentering = false;
        } else {
            int dx = e.getX() - mouseLocation.x;
            int dy = e.getY() - mouseLocation.y;
            mouseHelper(MOUSE_MOVE_LEFT, MOUSE_MOVE_RIGHT, dx);
            mouseHelper(MOUSE_MOVE_UP, MOUSE_MOVE_DOWN, dy);

            if (isRelativeMouseMode()) {
                recenterMouse();
            }
        }

        mouseLocation.x = e.getX();
        mouseLocation.y = e.getY();

    }

    // from the MouseWheelListener interface
    public void mouseWheelMoved(MouseWheelEvent e) {
        mouseHelper(MOUSE_WHEEL_UP, MOUSE_WHEEL_DOWN, e.getWheelRotation());
    }

    private void mouseHelper(int codeNeg, int codePos, int amount) {
        GameAction gameAction;
        if (amount < 0) {
            gameAction = mouseActions[codeNeg];
        } else {
            gameAction = mouseActions[codePos];
        }
        if (gameAction != null) {
            gameAction.press(Math.abs(amount));
            gameAction.release();
        }
    }

}

/**
 * Simple abstract class used for testing. Subclasses should implement the
 * draw() method.
 */

abstract class GameCore {

    protected static final int FONT_SIZE = 24;

    private static final DisplayMode POSSIBLE_MODES[] = { new DisplayMode(800, 600, 32, 0),
            new DisplayMode(800, 600, 24, 0), new DisplayMode(800, 600, 16, 0), new DisplayMode(640, 480, 32, 0),
            new DisplayMode(640, 480, 24, 0), new DisplayMode(640, 480, 16, 0) };

    private boolean isRunning;

    protected ScreenManager screen;

    /**
     * Signals the game loop that it's time to quit
     */
    public void stop() {
        isRunning = false;
    }

    /**
     * Calls init() and gameLoop()
     */
    public void run() {
        try {
            init();
            gameLoop();
        } finally {
            screen.restoreScreen();
            lazilyExit();
        }
    }

    /**
     * Exits the VM from a daemon thread. The daemon thread waits 2 seconds then
     * calls System.exit(0). Since the VM should exit when only daemon threads
     * are running, this makes sure System.exit(0) is only called if neccesary.
     * It's neccesary if the Java Sound system is running.
     */
    public void lazilyExit() {
        Thread thread = new Thread() {
            public void run() {
                // first, wait for the VM exit on its own.
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException ex) {
                }
                // system is still running, so force an exit
                System.exit(0);
            }
        };
        thread.setDaemon(true);
        thread.start();
    }

    /**
     * Sets full screen mode and initiates and objects.
     */
    public void init() {
        screen = new ScreenManager();
        DisplayMode displayMode = screen.findFirstCompatibleMode(POSSIBLE_MODES);
        screen.setFullScreen(displayMode);

        Window window = screen.getFullScreenWindow();
        window.setFont(new Font("Dialog", Font.PLAIN, FONT_SIZE));
        window.setBackground(Color.blue);
        window.setForeground(Color.white);

        isRunning = true;
    }

    public Image loadImage(String fileName) {
        return new ImageIcon(fileName).getImage();
    }

    /**
     * Runs through the game loop until stop() is called.
     */
    public void gameLoop() {
        long startTime = System.currentTimeMillis();
        long currTime = startTime;

        while (isRunning) {
            long elapsedTime = System.currentTimeMillis() - currTime;
            currTime += elapsedTime;

            // update
            update(elapsedTime);

            // draw the screen
            Graphics2D g = screen.getGraphics();
            draw(g);
            g.dispose();
            screen.update();

            // take a nap
            try {
                Thread.sleep(20);
            } catch (InterruptedException ex) {
            }
        }
    }

    /**
     * Updates the state of the game/animation based on the amount of elapsed
     * time that has passed.
     */
    public void update(long elapsedTime) {
        // do nothing
    }

    /**
     * Draws to the screen. Subclasses must override this method.
     */
    public abstract void draw(Graphics2D g);
}

/**
 * The GameAction class is an abstract to a user-initiated action, like jumping
 * or moving. GameActions can be mapped to keys or the mouse with the
 * InputManager.
 */

class GameAction {

    /**
     * Normal behavior. The isPressed() method returns true as long as the key
     * is held down.
     */
    public static final int NORMAL = 0;

    /**
     * Initial press behavior. The isPressed() method returns true only after
     * the key is first pressed, and not again until the key is released and
     * pressed again.
     */
    public static final int DETECT_INITAL_PRESS_ONLY = 1;

    private static final int STATE_RELEASED = 0;

    private static final int STATE_PRESSED = 1;

    private static final int STATE_WAITING_FOR_RELEASE = 2;

    private String name;

    private int behavior;

    private int amount;

    private int state;

    /**
     * Create a new GameAction with the NORMAL behavior.
     */
    public GameAction(String name) {
        this(name, NORMAL);
    }

    /**
     * Create a new GameAction with the specified behavior.
     */
    public GameAction(String name, int behavior) {
        this.name = name;
        this.behavior = behavior;
        reset();
    }

    /**
     * Gets the name of this GameAction.
     */
    public String getName() {
        return name;
    }

    /**
     * Resets this GameAction so that it appears like it hasn't been pressed.
     */
    public void reset() {
        state = STATE_RELEASED;
        amount = 0;
    }

    /**
     * Taps this GameAction. Same as calling press() followed by release().
     */
    public synchronized void tap() {
        press();
        release();
    }

    /**
     * Signals that the key was pressed.
     */
    public synchronized void press() {
        press(1);
    }

    /**
     * Signals that the key was pressed a specified number of times, or that the
     * mouse move a spcified distance.
     */
    public synchronized void press(int amount) {
        if (state != STATE_WAITING_FOR_RELEASE) {
            this.amount += amount;
            state = STATE_PRESSED;
        }

    }

    /**
     * Signals that the key was released
     */
    public synchronized void release() {
        state = STATE_RELEASED;
    }

    /**
     * Returns whether the key was pressed or not since last checked.
     */
    public synchronized boolean isPressed() {
        return (getAmount() != 0);
    }

    /**
     * For keys, this is the number of times the key was pressed since it was
     * last checked. For mouse movement, this is the distance moved.
     */
    public synchronized int getAmount() {
        int retVal = amount;
        if (retVal != 0) {
            if (state == STATE_RELEASED) {
                amount = 0;
            } else if (behavior == DETECT_INITAL_PRESS_ONLY) {
                state = STATE_WAITING_FOR_RELEASE;
                amount = 0;
            }
        }
        return retVal;
    }
}

class Sprite {

    private Animation anim;

    // position (pixels)
    private float x;

    private float y;

    // velocity (pixels per millisecond)
    private float dx;

    private float dy;

    /**
     * Creates a new Sprite object with the specified Animation.
     */
    public Sprite(Animation anim) {
        this.anim = anim;
    }

    /**
     * Updates this Sprite's Animation and its position based on the velocity.
     */
    public void update(long elapsedTime) {
        x += dx * elapsedTime;
        y += dy * elapsedTime;
        anim.update(elapsedTime);
    }

    /**
     * Gets this Sprite's current x position.
     */
    public float getX() {
        return x;
    }

    /**
     * Gets this Sprite's current y position.
     */
    public float getY() {
        return y;
    }

    /**
     * Sets this Sprite's current x position.
     */
    public void setX(float x) {
        this.x = x;
    }

    /**
     * Sets this Sprite's current y position.
     */
    public void setY(float y) {
        this.y = y;
    }

    /**
     * Gets this Sprite's width, based on the size of the current image.
     */
    public int getWidth() {
        return anim.getImage().getWidth(null);
    }

    /**
     * Gets this Sprite's height, based on the size of the current image.
     */
    public int getHeight() {
        return anim.getImage().getHeight(null);
    }

    /**
     * Gets the horizontal velocity of this Sprite in pixels per millisecond.
     */
    public float getVelocityX() {
        return dx;
    }

    /**
     * Gets the vertical velocity of this Sprite in pixels per millisecond.
     */
    public float getVelocityY() {
        return dy;
    }

    /**
     * Sets the horizontal velocity of this Sprite in pixels per millisecond.
     */
    public void setVelocityX(float dx) {
        this.dx = dx;
    }

    /**
     * Sets the vertical velocity of this Sprite in pixels per millisecond.
     */
    public void setVelocityY(float dy) {
        this.dy = dy;
    }

    /**
     * Gets this Sprite's current image.
     */
    public Image getImage() {
        return anim.getImage();
    }

}

/**
 * The Filter3d class is a SoundFilter that creates a 3d sound effect. The sound
 * is filtered so that it is quiter the farther away the sound source is from
 * the listener.
 * <p>
 * Possible ideas to extend this class:
 * <ul>
 * <li>pan the sound to the left and right speakers
 * </ul>
 * 
 * @see FilteredSoundStream
 */

class Filter3d extends SoundFilter {

    // number of samples to shift when changing the volume.
    private static final int NUM_SHIFTING_SAMPLES = 500;

    private Sprite source;

    private Sprite listener;

    private int maxDistance;

    private float lastVolume;

    /**
     * Creates a new Filter3d object with the specified source and listener
     * Sprites. The Sprite's position can be changed while this filter is
     * running.
     * <p>
     * The maxDistance parameter is the maximum distance that the sound can be
     * heard.
     */
    public Filter3d(Sprite source, Sprite listener, int maxDistance) {
        this.source = source;
        this.listener = listener;
        this.maxDistance = maxDistance;
        this.lastVolume = 0.0f;
    }

    /**
     * Filters the sound so that it gets more quiet with distance.
     */
    public void filter(byte[] samples, int offset, int length) {

        if (source == null || listener == null) {
            // nothing to filter - return
            return;
        }

        // calculate the listener's distance from the sound source
        float dx = (source.getX() - listener.getX());
        float dy = (source.getY() - listener.getY());
        float distance = (float) Math.sqrt(dx * dx + dy * dy);

        // set volume from 0 (no sound) to 1
        float newVolume = (maxDistance - distance) / maxDistance;
        if (newVolume <= 0) {
            newVolume = 0;
        }

        // set the volume of the sample
        int shift = 0;
        for (int i = offset; i < offset + length; i += 2) {

            float volume = newVolume;

            // shift from the last volume to the new volume
            if (shift < NUM_SHIFTING_SAMPLES) {
                volume = lastVolume + (newVolume - lastVolume) * shift / NUM_SHIFTING_SAMPLES;
                shift++;
            }

            // change the volume of the sample
            short oldSample = getSample(samples, i);
            short newSample = (short) (oldSample * volume);
            setSample(samples, i, newSample);
        }

        lastVolume = newVolume;
    }

}

/**
 * The FilteredSoundStream class is a FilterInputStream that applies a
 * SoundFilter to the underlying input stream.
 * 
 * @see SoundFilter
 */

class FilteredSoundStream extends FilterInputStream {

    private static final int REMAINING_SIZE_UNKNOWN = -1;

    private SoundFilter soundFilter;

    private int remainingSize;

    /**
     * Creates a new FilteredSoundStream object with the specified InputStream
     * and SoundFilter.
     */
    public FilteredSoundStream(InputStream in, SoundFilter soundFilter) {
        super(in);
        this.soundFilter = soundFilter;
        remainingSize = REMAINING_SIZE_UNKNOWN;
    }

    /**
     * Overrides the FilterInputStream method to apply this filter whenever
     * bytes are read
     */
    public int read(byte[] samples, int offset, int length) throws IOException {
        // read and filter the sound samples in the stream
        int bytesRead = super.read(samples, offset, length);
        if (bytesRead > 0) {
            soundFilter.filter(samples, offset, bytesRead);
            return bytesRead;
        }

        // if there are no remaining bytes in the sound stream,
        // check if the filter has any remaining bytes ("echoes").
        if (remainingSize == REMAINING_SIZE_UNKNOWN) {
            remainingSize = soundFilter.getRemainingSize();
            // round down to nearest multiple of 4
            // (typical frame size)
            remainingSize = remainingSize / 4 * 4;
        }
        if (remainingSize > 0) {
            length = Math.min(length, remainingSize);

            // clear the buffer
            for (int i = offset; i < offset + length; i++) {
                samples[i] = 0;
            }

            // filter the remaining bytes
            soundFilter.filter(samples, offset, length);
            remainingSize -= length;

            // return
            return length;
        } else {
            // end of stream
            return -1;
        }
    }

}

/**
 * The Animation class manages a series of images (frames) and the amount of
 * time to display each frame.
 */

class Animation {

    private ArrayList frames;

    private int currFrameIndex;

    private long animTime;

    private long totalDuration;

    /**
     * Creates a new, empty Animation.
     */
    public Animation() {
        frames = new ArrayList();
        totalDuration = 0;
        start();
    }

    /**
     * Adds an image to the animation with the specified duration (time to
     * display the image).
     */
    public synchronized void addFrame(Image image, long duration) {
        totalDuration += duration;
        frames.add(new AnimFrame(image, totalDuration));
    }

    /**
     * Starts this animation over from the beginning.
     */
    public synchronized void start() {
        animTime = 0;
        currFrameIndex = 0;
    }

    /**
     * Updates this animation's current image (frame), if neccesary.
     */
    public synchronized void update(long elapsedTime) {
        if (frames.size() > 1) {
            animTime += elapsedTime;

            if (animTime >= totalDuration) {
                animTime = animTime % totalDuration;
                currFrameIndex = 0;
            }

            while (animTime > getFrame(currFrameIndex).endTime) {
                currFrameIndex++;
            }
        }
    }

    /**
     * Gets this Animation's current image. Returns null if this animation has
     * no images.
     */
    public synchronized Image getImage() {
        if (frames.size() == 0) {
            return null;
        } else {
            return getFrame(currFrameIndex).image;
        }
    }

    private AnimFrame getFrame(int i) {
        return (AnimFrame) frames.get(i);
    }

    private class AnimFrame {

        Image image;

        long endTime;

        public AnimFrame(Image image, long endTime) {
            this.image = image;
            this.endTime = endTime;
        }
    }
}

/**
 * The LoopingByteInputStream is a ByteArrayInputStream that loops indefinitly.
 * The looping stops when the close() method is called.
 * <p>
 * Possible ideas to extend this class:
 * <ul>
 * <li>Add an option to only loop a certain number of times.
 * </ul>
 */

class LoopingByteInputStream extends ByteArrayInputStream {

    private boolean closed;

    /**
     * Creates a new LoopingByteInputStream with the specified byte array. The
     * array is not copied.
     */
    public LoopingByteInputStream(byte[] buffer) {
        super(buffer);
        closed = false;
    }

    /**
     * Reads <code>length</code> bytes from the array. If the end of the array
     * is reached, the reading starts over from the beginning of the array.
     * Returns -1 if the array has been closed.
     */
    public int read(byte[] buffer, int offset, int length) {
        if (closed) {
            return -1;
        }
        int totalBytesRead = 0;

        while (totalBytesRead < length) {
            int numBytesRead = super.read(buffer, offset + totalBytesRead, length - totalBytesRead);

            if (numBytesRead > 0) {
                totalBytesRead += numBytesRead;
            } else {
                reset();
            }
        }
        return totalBytesRead;
    }

    /**
     * Closes the stream. Future calls to the read() methods will return 1.
     */
    public void close() throws IOException {
        super.close();
        closed = true;
    }

}

/**
 * A abstract class designed to filter sound samples. Since SoundFilters may use
 * internal buffering of samples, a new SoundFilter object should be created for
 * every sound played. However, SoundFilters can be reused after they are
 * finished by called the reset() method.
 * <p>
 * Assumes all samples are 16-bit, signed, little-endian format.
 * 
 * @see FilteredSoundStream
 */

abstract class SoundFilter {

    /**
     * Resets this SoundFilter. Does nothing by default.
     */
    public void reset() {
        // do nothing
    }

    /**
     * Gets the remaining size, in bytes, that this filter plays after the sound
     * is finished. An example would be an echo that plays longer than it's
     * original sound. This method returns 0 by default.
     */
    public int getRemainingSize() {
        return 0;
    }

    /**
     * Filters an array of samples. Samples should be in 16-bit, signed,
     * little-endian format.
     */
    public void filter(byte[] samples) {
        filter(samples, 0, samples.length);
    }

    /**
     * Filters an array of samples. Samples should be in 16-bit, signed,
     * little-endian format. This method should be implemented by subclasses.
     */
    public abstract void filter(byte[] samples, int offset, int length);

    /**
     * Convenience method for getting a 16-bit sample from a byte array. Samples
     * should be in 16-bit, signed, little-endian format.
     */
    public static short getSample(byte[] buffer, int position) {
        return (short) (((buffer[position + 1] & 0xff) << 8) | (buffer[position] & 0xff));
    }

    /**
     * Convenience method for setting a 16-bit sample in a byte array. Samples
     * should be in 16-bit, signed, little-endian format.
     */
    public static void setSample(byte[] buffer, int position, short sample) {
        buffer[position] = (byte) (sample & 0xff);
        buffer[position + 1] = (byte) ((sample >> 8) & 0xff);
    }

}

/**
 * The ScreenManager class manages initializing and displaying full screen
 * graphics modes.
 */

class ScreenManager {

    private GraphicsDevice device;

    /**
     * Creates a new ScreenManager object.
     */
    public ScreenManager() {
        GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment();
        device = environment.getDefaultScreenDevice();
    }

    /**
     * Returns a list of compatible display modes for the default device on the
     * system.
     */
    public DisplayMode[] getCompatibleDisplayModes() {
        return device.getDisplayModes();
    }

    /**
     * Returns the first compatible mode in a list of modes. Returns null if no
     * modes are compatible.
     */
    public DisplayMode findFirstCompatibleMode(DisplayMode modes[]) {
        DisplayMode goodModes[] = device.getDisplayModes();
        for (int i = 0; i < modes.length; i++) {
            for (int j = 0; j < goodModes.length; j++) {
                if (displayModesMatch(modes[i], goodModes[j])) {
                    return modes[i];
                }
            }

        }

        return null;
    }

    /**
     * Returns the current display mode.
     */
    public DisplayMode getCurrentDisplayMode() {
        return device.getDisplayMode();
    }

    /**
     * Determines if two display modes "match". Two display modes match if they
     * have the same resolution, bit depth, and refresh rate. The bit depth is
     * ignored if one of the modes has a bit depth of
     * DisplayMode.BIT_DEPTH_MULTI. Likewise, the refresh rate is ignored if one
     * of the modes has a refresh rate of DisplayMode.REFRESH_RATE_UNKNOWN.
     */
    public boolean displayModesMatch(DisplayMode mode1, DisplayMode mode2)

    {
        if (mode1.getWidth() != mode2.getWidth() || mode1.getHeight() != mode2.getHeight()) {
            return false;
        }

        if (mode1.getBitDepth() != DisplayMode.BIT_DEPTH_MULTI && mode2.getBitDepth() != DisplayMode.BIT_DEPTH_MULTI
                && mode1.getBitDepth() != mode2.getBitDepth()) {
            return false;
        }

        if (mode1.getRefreshRate() != DisplayMode.REFRESH_RATE_UNKNOWN
                && mode2.getRefreshRate() != DisplayMode.REFRESH_RATE_UNKNOWN
                && mode1.getRefreshRate() != mode2.getRefreshRate()) {
            return false;
        }

        return true;
    }

    /**
     * Enters full screen mode and changes the display mode. If the specified
     * display mode is null or not compatible with this device, or if the
     * display mode cannot be changed on this system, the current display mode
     * is used.
     * <p>
     * The display uses a BufferStrategy with 2 buffers.
     */
    public void setFullScreen(DisplayMode displayMode) {
        final JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setUndecorated(true);
        frame.setIgnoreRepaint(true);
        frame.setResizable(false);

        device.setFullScreenWindow(frame);

        if (displayMode != null && device.isDisplayChangeSupported()) {
            try {
                device.setDisplayMode(displayMode);
            } catch (IllegalArgumentException ex) {
            }
            // fix for mac os x
            frame.setSize(displayMode.getWidth(), displayMode.getHeight());
        }
        // avoid potential deadlock in 1.4.1_02
        try {
            EventQueue.invokeAndWait(new Runnable() {
                public void run() {
                    frame.createBufferStrategy(2);
                }
            });
        } catch (InterruptedException ex) {
            // ignore
        } catch (InvocationTargetException ex) {
            // ignore
        }

    }

    /**
     * Gets the graphics context for the display. The ScreenManager uses double
     * buffering, so applications must call update() to show any graphics drawn.
     * <p>
     * The application must dispose of the graphics object.
     */
    public Graphics2D getGraphics() {
        Window window = device.getFullScreenWindow();
        if (window != null) {
            BufferStrategy strategy = window.getBufferStrategy();
            return (Graphics2D) strategy.getDrawGraphics();
        } else {
            return null;
        }
    }

    /**
     * Updates the display.
     */
    public void update() {
        Window window = device.getFullScreenWindow();
        if (window != null) {
            BufferStrategy strategy = window.getBufferStrategy();
            if (!strategy.contentsLost()) {
                strategy.show();
            }
        }
        // Sync the display on some systems.
        // (on Linux, this fixes event queue problems)
        Toolkit.getDefaultToolkit().sync();
    }

    /**
     * Returns the window currently used in full screen mode. Returns null if
     * the device is not in full screen mode.
     */
    public JFrame getFullScreenWindow() {
        return (JFrame) device.getFullScreenWindow();
    }

    /**
     * Returns the width of the window currently used in full screen mode.
     * Returns 0 if the device is not in full screen mode.
     */
    public int getWidth() {
        Window window = device.getFullScreenWindow();
        if (window != null) {
            return window.getWidth();
        } else {
            return 0;
        }
    }

    /**
     * Returns the height of the window currently used in full screen mode.
     * Returns 0 if the device is not in full screen mode.
     */
    public int getHeight() {
        Window window = device.getFullScreenWindow();
        if (window != null) {
            return window.getHeight();
        } else {
            return 0;
        }
    }

    /**
     * Restores the screen's display mode.
     */
    public void restoreScreen() {
        Window window = device.getFullScreenWindow();
        if (window != null) {
            window.dispose();
        }
        device.setFullScreenWindow(null);
    }

    /**
     * Creates an image compatible with the current display.
     */
    public BufferedImage createCompatibleImage(int w, int h, int transparancy) {
        Window window = device.getFullScreenWindow();
        if (window != null) {
            GraphicsConfiguration gc = window.getGraphicsConfiguration();
            return gc.createCompatibleImage(w, h, transparancy);
        }
        return null;
    }
}