* 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. *
* Possible ideas to extend this class: *
* 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. *
* Possible ideas to extend this class: *
length
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.
* * 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. *
* 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. *
* 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; } }