/*
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.Transparency;
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.geom.AffineTransform;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MetaEventListener;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;
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.Mixer;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
/**
* GameManager manages all parts of the game.
*/
public class TitleGame extends GameCore {
public static void main(String[] args) {
new TitleGame().run();
}
// uncompressed, 44100Hz, 16-bit, mono, signed, little-endian
private static final AudioFormat PLAYBACK_FORMAT = new AudioFormat(44100,
16, 1, true, false);
private static final int DRUM_TRACK = 1;
public static final float GRAVITY = 0.002f;
private Point pointCache = new Point();
private TileMap map;
private MidiPlayer midiPlayer;
private SoundManager soundManager;
private ResourceManager resourceManager;
private Sound prizeSound;
private Sound boopSound;
private InputManager inputManager;
private TileMapRenderer renderer;
private GameAction moveLeft;
private GameAction moveRight;
private GameAction jump;
private GameAction exit;
public void init() {
super.init();
// set up input manager
initInput();
// start resource manager
resourceManager = new ResourceManager(screen.getFullScreenWindow()
.getGraphicsConfiguration());
// load resources
renderer = new TileMapRenderer();
renderer.setBackground(resourceManager.loadImage("background.png"));
// load first map
map = resourceManager.loadNextMap();
// load sounds
soundManager = new SoundManager(PLAYBACK_FORMAT);
prizeSound = soundManager.getSound("sounds/prize.wav");
boopSound = soundManager.getSound("sounds/boop2.wav");
// start music
midiPlayer = new MidiPlayer();
Sequence sequence = midiPlayer.getSequence("sounds/music.midi");
midiPlayer.play(sequence, true);
toggleDrumPlayback();
}
/**
* Closes any resurces used by the GameManager.
*/
public void stop() {
super.stop();
midiPlayer.close();
soundManager.close();
}
private void initInput() {
moveLeft = new GameAction("moveLeft");
moveRight = new GameAction("moveRight");
jump = new GameAction("jump", GameAction.DETECT_INITAL_PRESS_ONLY);
exit = new GameAction("exit", GameAction.DETECT_INITAL_PRESS_ONLY);
inputManager = new InputManager(screen.getFullScreenWindow());
inputManager.setCursor(InputManager.INVISIBLE_CURSOR);
inputManager.mapToKey(moveLeft, KeyEvent.VK_LEFT);
inputManager.mapToKey(moveRight, KeyEvent.VK_RIGHT);
inputManager.mapToKey(jump, KeyEvent.VK_SPACE);
inputManager.mapToKey(exit, KeyEvent.VK_ESCAPE);
}
private void checkInput(long elapsedTime) {
if (exit.isPressed()) {
stop();
}
Player player = (Player) map.getPlayer();
if (player.isAlive()) {
float velocityX = 0;
if (moveLeft.isPressed()) {
velocityX -= player.getMaxSpeed();
}
if (moveRight.isPressed()) {
velocityX += player.getMaxSpeed();
}
if (jump.isPressed()) {
player.jump(false);
}
player.setVelocityX(velocityX);
}
}
public void draw(Graphics2D g) {
renderer.draw(g, map, screen.getWidth(), screen.getHeight());
}
/**
* Gets the current map.
*/
public TileMap getMap() {
return map;
}
/**
* Turns on/off drum playback in the midi music (track 1).
*/
public void toggleDrumPlayback() {
Sequencer sequencer = midiPlayer.getSequencer();
if (sequencer != null) {
sequencer.setTrackMute(DRUM_TRACK, !sequencer
.getTrackMute(DRUM_TRACK));
}
}
/**
* Gets the tile that a Sprites collides with. Only the Sprite's X or Y
* should be changed, not both. Returns null if no collision is detected.
*/
public Point getTileCollision(Sprite sprite, float newX, float newY) {
float fromX = Math.min(sprite.getX(), newX);
float fromY = Math.min(sprite.getY(), newY);
float toX = Math.max(sprite.getX(), newX);
float toY = Math.max(sprite.getY(), newY);
// get the tile locations
int fromTileX = TileMapRenderer.pixelsToTiles(fromX);
int fromTileY = TileMapRenderer.pixelsToTiles(fromY);
int toTileX = TileMapRenderer
.pixelsToTiles(toX + sprite.getWidth() - 1);
int toTileY = TileMapRenderer.pixelsToTiles(toY + sprite.getHeight()
- 1);
// check each tile for a collision
for (int x = fromTileX; x <= toTileX; x++) {
for (int y = fromTileY; y <= toTileY; y++) {
if (x < 0 || x >= map.getWidth() || map.getTile(x, y) != null) {
// collision found, return the tile
pointCache.setLocation(x, y);
return pointCache;
}
}
}
// no collision found
return null;
}
/**
* Checks if two Sprites collide with one another. Returns false if the two
* Sprites are the same. Returns false if one of the Sprites is a Creature
* that is not alive.
*/
public boolean isCollision(Sprite s1, Sprite s2) {
// if the Sprites are the same, return false
if (s1 == s2) {
return false;
}
// if one of the Sprites is a dead Creature, return false
if (s1 instanceof Creature && !((Creature) s1).isAlive()) {
return false;
}
if (s2 instanceof Creature && !((Creature) s2).isAlive()) {
return false;
}
// get the pixel location of the Sprites
int s1x = Math.round(s1.getX());
int s1y = Math.round(s1.getY());
int s2x = Math.round(s2.getX());
int s2y = Math.round(s2.getY());
// check if the two sprites' boundaries intersect
return (s1x < s2x + s2.getWidth() && s2x < s1x + s1.getWidth()
&& s1y < s2y + s2.getHeight() && s2y < s1y + s1.getHeight());
}
/**
* Gets the Sprite that collides with the specified Sprite, or null if no
* Sprite collides with the specified Sprite.
*/
public Sprite getSpriteCollision(Sprite sprite) {
// run through the list of Sprites
Iterator i = map.getSprites();
while (i.hasNext()) {
Sprite otherSprite = (Sprite) i.next();
if (isCollision(sprite, otherSprite)) {
// collision found, return the Sprite
return otherSprite;
}
}
// no collision found
return null;
}
/**
* Updates Animation, position, and velocity of all Sprites in the current
* map.
*/
public void update(long elapsedTime) {
Creature player = (Creature) map.getPlayer();
// player is dead! start map over
if (player.getState() == Creature.STATE_DEAD) {
map = resourceManager.reloadMap();
return;
}
// get keyboard/mouse input
checkInput(elapsedTime);
// update player
updateCreature(player, elapsedTime);
player.update(elapsedTime);
// update other sprites
Iterator i = map.getSprites();
while (i.hasNext()) {
Sprite sprite = (Sprite) i.next();
if (sprite instanceof Creature) {
Creature creature = (Creature) sprite;
if (creature.getState() == Creature.STATE_DEAD) {
i.remove();
} else {
updateCreature(creature, elapsedTime);
}
}
// normal update
sprite.update(elapsedTime);
}
}
/**
* Updates the creature, applying gravity for creatures that aren't flying,
* and checks collisions.
*/
private void updateCreature(Creature creature, long elapsedTime) {
// apply gravity
if (!creature.isFlying()) {
creature.setVelocityY(creature.getVelocityY() + GRAVITY
* elapsedTime);
}
// change x
float dx = creature.getVelocityX();
float oldX = creature.getX();
float newX = oldX + dx * elapsedTime;
Point tile = getTileCollision(creature, newX, creature.getY());
if (tile == null) {
creature.setX(newX);
} else {
// line up with the tile boundary
if (dx > 0) {
creature.setX(TileMapRenderer.tilesToPixels(tile.x)
- creature.getWidth());
} else if (dx < 0) {
creature.setX(TileMapRenderer.tilesToPixels(tile.x + 1));
}
creature.collideHorizontal();
}
if (creature instanceof Player) {
checkPlayerCollision((Player) creature, false);
}
// change y
float dy = creature.getVelocityY();
float oldY = creature.getY();
float newY = oldY + dy * elapsedTime;
tile = getTileCollision(creature, creature.getX(), newY);
if (tile == null) {
creature.setY(newY);
} else {
// line up with the tile boundary
if (dy > 0) {
creature.setY(TileMapRenderer.tilesToPixels(tile.y)
- creature.getHeight());
} else if (dy < 0) {
creature.setY(TileMapRenderer.tilesToPixels(tile.y + 1));
}
creature.collideVertical();
}
if (creature instanceof Player) {
boolean canKill = (oldY < creature.getY());
checkPlayerCollision((Player) creature, canKill);
}
}
/**
* Checks for Player collision with other Sprites. If canKill is true,
* collisions with Creatures will kill them.
*/
public void checkPlayerCollision(Player player, boolean canKill) {
if (!player.isAlive()) {
return;
}
// check for player collision with other sprites
Sprite collisionSprite = getSpriteCollision(player);
if (collisionSprite instanceof PowerUp) {
acquirePowerUp((PowerUp) collisionSprite);
} else if (collisionSprite instanceof Creature) {
Creature badguy = (Creature) collisionSprite;
if (canKill) {
// kill the badguy and make player bounce
soundManager.play(boopSound);
badguy.setState(Creature.STATE_DYING);
player.setY(badguy.getY() - player.getHeight());
player.jump(true);
} else {
// player dies!
player.setState(Creature.STATE_DYING);
}
}
}
/**
* Gives the player the speicifed power up and removes it from the map.
*/
public void acquirePowerUp(PowerUp powerUp) {
// remove it from the map
map.removeSprite(powerUp);
if (powerUp instanceof PowerUp.Star) {
// do something here, like give the player points
soundManager.play(prizeSound);
} else if (powerUp instanceof PowerUp.Music) {
// change the music
soundManager.play(prizeSound);
toggleDrumPlayback();
} else if (powerUp instanceof PowerUp.Goal) {
// advance to next map
soundManager.play(prizeSound, new EchoFilter(2000, .7f), false);
map = resourceManager.loadNextMap();
}
}
}
/**
* The EchoFilter class is a SoundFilter that emulates an echo.
*
* @see FilteredSoundStream
*/
class EchoFilter extends SoundFilter {
private short[] delayBuffer;
private int delayBufferPos;
private float decay;
/**
* Creates an EchoFilter with the specified number of delay samples and the
* specified decay rate.
* <p>
* The number of delay samples specifies how long before the echo is
* initially heard. For a 1 second echo with mono, 44100Hz sound, use 44100
* delay samples.
* <p>
* The decay value is how much the echo has decayed from the source. A decay
* value of .5 means the echo heard is half as loud as the source.
*/
public EchoFilter(int numDelaySamples, float decay) {
delayBuffer = new short[numDelaySamples];
this.decay = decay;
}
/**
* Gets the remaining size, in bytes, of samples that this filter can echo
* after the sound is done playing. Ensures that the sound will have decayed
* to below 1% of maximum volume (amplitude).
*/
public int getRemainingSize() {
float finalDecay = 0.01f;
// derived from Math.pow(decay,x) <= finalDecay
int numRemainingBuffers = (int) Math.ceil(Math.log(finalDecay)
/ Math.log(decay));
int bufferSize = delayBuffer.length * 2;
return bufferSize * numRemainingBuffers;
}
/**
* Clears this EchoFilter's internal delay buffer.
*/
public void reset() {
for (int i = 0; i < delayBuffer.length; i++) {
delayBuffer[i] = 0;
}
delayBufferPos = 0;
}
/**
* Filters the sound samples to add an echo. The samples played are added to
* the sound in the delay buffer multipied by the decay rate. The result is
* then stored in the delay buffer, so multiple echoes are heard.
*/
public void filter(byte[] samples, int offset, int length) {
for (int i = offset; i < offset + length; i += 2) {
// update the sample
short oldSample = getSample(samples, i);
short newSample = (short) (oldSample + decay
* delayBuffer[delayBufferPos]);
setSample(samples, i, newSample);
// update the delay buffer
delayBuffer[delayBufferPos] = newSample;
delayBufferPos++;
if (delayBufferPos == delayBuffer.length) {
delayBufferPos = 0;
}
}
}
}
/**
* 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);
}
}
/**
* A Creature is a Sprite that is affected by gravity and can die. It has four
* Animations: moving left, moving right, dying on the left, and dying on the
* right.
*/
abstract class Creature extends Sprite {
/**
* Amount of time to go from STATE_DYING to STATE_DEAD.
*/
private static final int DIE_TIME = 1000;
public static final int STATE_NORMAL = 0;
public static final int STATE_DYING = 1;
public static final int STATE_DEAD = 2;
private Animation left;
private Animation right;
private Animation deadLeft;
private Animation deadRight;
private int state;
private long stateTime;
/**
* Creates a new Creature with the specified Animations.
*/
public Creature(Animation left, Animation right, Animation deadLeft,
Animation deadRight) {
super(right);
this.left = left;
this.right = right;
this.deadLeft = deadLeft;
this.deadRight = deadRight;
state = STATE_NORMAL;
}
public Object clone() {
// use reflection to create the correct subclass
Constructor constructor = getClass().getConstructors()[0];
try {
return constructor
.newInstance(new Object[] { (Animation) left.clone(),
(Animation) right.clone(),
(Animation) deadLeft.clone(),
(Animation) deadRight.clone() });
} catch (Exception ex) {
// should never happen
ex.printStackTrace();
return null;
}
}
/**
* Gets the maximum speed of this Creature.
*/
public float getMaxSpeed() {
return 0;
}
/**
* Wakes up the creature when the Creature first appears on screen.
* Normally, the creature starts moving left.
*/
public void wakeUp() {
if (getState() == STATE_NORMAL && getVelocityX() == 0) {
setVelocityX(-getMaxSpeed());
}
}
/**
* Gets the state of this Creature. The state is either STATE_NORMAL,
* STATE_DYING, or STATE_DEAD.
*/
public int getState() {
return state;
}
/**
* Sets the state of this Creature to STATE_NORMAL, STATE_DYING, or
* STATE_DEAD.
*/
public void setState(int state) {
if (this.state != state) {
this.state = state;
stateTime = 0;
if (state == STATE_DYING) {
setVelocityX(0);
setVelocityY(0);
}
}
}
/**
* Checks if this creature is alive.
*/
public boolean isAlive() {
return (state == STATE_NORMAL);
}
/**
* Checks if this creature is flying.
*/
public boolean isFlying() {
return false;
}
/**
* Called before update() if the creature collided with a tile horizontally.
*/
public void collideHorizontal() {
setVelocityX(-getVelocityX());
}
/**
* Called before update() if the creature collided with a tile vertically.
*/
public void collideVertical() {
setVelocityY(0);
}
/**
* Updates the animaton for this creature.
*/
public void update(long elapsedTime) {
// select the correct Animation
Animation newAnim = anim;
if (getVelocityX() < 0) {
newAnim = left;
} else if (getVelocityX() > 0) {
newAnim = right;
}
if (state == STATE_DYING && newAnim == left) {
newAnim = deadLeft;
} else if (state == STATE_DYING && newAnim == right) {
newAnim = deadRight;
}
// update the Animation
if (anim != newAnim) {
anim = newAnim;
anim.start();
} else {
anim.update(elapsedTime);
}
// update to "dead" state
stateTime += elapsedTime;
if (state == STATE_DYING && stateTime >= DIE_TIME) {
setState(STATE_DEAD);
}
}
}
/**
* 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() {
this(new ArrayList(), 0);
}
private Animation(ArrayList frames, long totalDuration) {
this.frames = frames;
this.totalDuration = totalDuration;
start();
}
/**
* Creates a duplicate of this animation. The list of frames are shared
* between the two Animations, but each Animation can be animated
* independently.
*/
public Object clone() {
return new Animation(frames, totalDuration);
}
/**
* 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;
}
}
}
/**
* A thread pool is a group of a limited number of threads that are used to
* execute tasks.
*/
class ThreadPool extends ThreadGroup {
private boolean isAlive;
private LinkedList taskQueue;
private int threadID;
private static int threadPoolID;
/**
* Creates a new ThreadPool.
*
* @param numThreads
* The number of threads in the pool.
*/
public ThreadPool(int numThreads) {
super("ThreadPool-" + (threadPoolID++));
setDaemon(true);
isAlive = true;
taskQueue = new LinkedList();
for (int i = 0; i < numThreads; i++) {
new PooledThread().start();
}
}
/**
* Requests a new task to run. This method returns immediately, and the task
* executes on the next available idle thread in this ThreadPool.
* <p>
* Tasks start execution in the order they are received.
*
* @param task
* The task to run. If null, no action is taken.
* @throws IllegalStateException
* if this ThreadPool is already closed.
*/
public synchronized void runTask(Runnable task) {
if (!isAlive) {
throw new IllegalStateException();
}
if (task != null) {
taskQueue.add(task);
notify();
}
}
protected synchronized Runnable getTask() throws InterruptedException {
while (taskQueue.size() == 0) {
if (!isAlive) {
return null;
}
wait();
}
return (Runnable) taskQueue.removeFirst();
}
/**
* Closes this ThreadPool and returns immediately. All threads are stopped,
* and any waiting tasks are not executed. Once a ThreadPool is closed, no
* more tasks can be run on this ThreadPool.
*/
public synchronized void close() {
if (isAlive) {
isAlive = false;
taskQueue.clear();
interrupt();
}
}
/**
* Closes this ThreadPool and waits for all running threads to finish. Any
* waiting tasks are executed.
*/
public void join() {
// notify all waiting threads that this ThreadPool is no
// longer alive
synchronized (this) {
isAlive = false;
notifyAll();
}
// wait for all threads to finish
Thread[] threads = new Thread[activeCount()];
int count = enumerate(threads);
for (int i = 0; i < count; i++) {
try {
threads[i].join();
} catch (InterruptedException ex) {
}
}
}
/**
* Signals that a PooledThread has started. This method does nothing by
* default; subclasses should override to do any thread-specific startup
* tasks.
*/
protected void threadStarted() {
// do nothing
}
/**
* Signals that a PooledThread has stopped. This method does nothing by
* default; subclasses should override to do any thread-specific cleanup
* tasks.
*/
protected void threadStopped() {
// do nothing
}
/**
* A PooledThread is a Thread in a ThreadPool group, designed to run tasks
* (Runnables).
*/
private class PooledThread extends Thread {
public PooledThread() {
super(ThreadPool.this, "PooledThread-" + (threadID++));
}
public void run() {
// signal that this thread has started
threadStarted();
while (!isInterrupted()) {
// get a task to run
Runnable task = null;
try {
task = getTask();
} catch (InterruptedException ex) {
}
// if getTask() returned null or was interrupted,
// close this thread.
if (task == null) {
break;
}
// run the task, and eat any exceptions it throws
try {
task.run();
} catch (Throwable t) {
uncaughtException(this, t);
}
}
// signal that this thread has stopped
threadStopped();
}
}
}
/**
* The Player.
*/
class Player extends Creature {
private static final float JUMP_SPEED = -.95f;
private boolean onGround;
public Player(Animation left, Animation right, Animation deadLeft,
Animation deadRight) {
super(left, right, deadLeft, deadRight);
}
public void collideHorizontal() {
setVelocityX(0);
}
public void collideVertical() {
// check if collided with ground
if (getVelocityY() > 0) {
onGround = true;
}
setVelocityY(0);
}
public void setY(float y) {
// check if falling
if (Math.round(y) > Math.round(getY())) {
onGround = false;
}
super.setY(y);
}
public void wakeUp() {
// do nothing
}
/**
* Makes the player jump if the player is on the ground or if forceJump is
* true.
*/
public void jump(boolean forceJump) {
if (onGround || forceJump) {
onGround = false;
setVelocityY(JUMP_SPEED);
}
}
public float getMaxSpeed() {
return 0.5f;
}
}
/**
* 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, 16, 0), new DisplayMode(800, 600, 32, 0),
new DisplayMode(800, 600, 24, 0), new DisplayMode(640, 480, 16, 0),
new DisplayMode(640, 480, 32, 0), new DisplayMode(640, 480, 24, 0),
new DisplayMode(1024, 768, 16, 0),
new DisplayMode(1024, 768, 32, 0),
new DisplayMode(1024, 768, 24, 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();
// don't take a nap! run as fast as possible
/*
* 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 ResourceManager class loads and manages tile Images and "host" Sprites
* used in the game. Game Sprites are cloned from "host" Sprites.
*/
class ResourceManager {
private ArrayList tiles;
private int currentMap;
private GraphicsConfiguration gc;
// host sprites used for cloning
private Sprite playerSprite;
private Sprite musicSprite;
private Sprite coinSprite;
private Sprite goalSprite;
private Sprite grubSprite;
private Sprite flySprite;
/**
* Creates a new ResourceManager with the specified GraphicsConfiguration.
*/
public ResourceManager(GraphicsConfiguration gc) {
this.gc = gc;
loadTileImages();
loadCreatureSprites();
loadPowerUpSprites();
}
/**
* Gets an image from the images/ directory.
*/
public Image loadImage(String name) {
String filename = "images/" + name;
return new ImageIcon(filename).getImage();
}
public Image getMirrorImage(Image image) {
return getScaledImage(image, -1, 1);
}
public Image getFlippedImage(Image image) {
return getScaledImage(image, 1, -1);
}
private Image getScaledImage(Image image, float x, float y) {
// set up the transform
AffineTransform transform = new AffineTransform();
transform.scale(x, y);
transform.translate((x - 1) * image.getWidth(null) / 2, (y - 1)
* image.getHeight(null) / 2);
// create a transparent (not translucent) image
Image newImage = gc.createCompatibleImage(image.getWidth(null), image
.getHeight(null), Transparency.BITMASK);
// draw the transformed image
Graphics2D g = (Graphics2D) newImage.getGraphics();
g.drawImage(image, transform, null);
g.dispose();
return newImage;
}
public TileMap loadNextMap() {
TileMap map = null;
while (map == null) {
currentMap++;
try {
map = loadMap("maps/map" + currentMap + ".txt");
} catch (IOException ex) {
if (currentMap == 1) {
// no maps to load!
return null;
}
currentMap = 0;
map = null;
}
}
return map;
}
public TileMap reloadMap() {
try {
return loadMap("maps/map" + currentMap + ".txt");
} catch (IOException ex) {
ex.printStackTrace();
return null;
}
}
private TileMap loadMap(String filename) throws IOException {
ArrayList lines = new ArrayList();
int width = 0;
int height = 0;
// read every line in the text file into the list
BufferedReader reader = new BufferedReader(new FileReader(filename));
while (true) {
String line = reader.readLine();
// no more lines to read
if (line == null) {
reader.close();
break;
}
// add every line except for comments
if (!line.startsWith("#")) {
lines.add(line);
width = Math.max(width, line.length());
}
}
// parse the lines to create a TileEngine
height = lines.size();
TileMap newMap = new TileMap(width, height);
for (int y = 0; y < height; y++) {
String line = (String) lines.get(y);
for (int x = 0; x < line.length(); x++) {
char ch = line.charAt(x);
// check if the char represents tile A, B, C etc.
int tile = ch - 'A';
if (tile >= 0 && tile < tiles.size()) {
newMap.setTile(x, y, (Image) tiles.get(tile));
}
// check if the char represents a sprite
else if (ch == 'o') {
addSprite(newMap, coinSprite, x, y);
} else if (ch == '!') {
addSprite(newMap, musicSprite, x, y);
} else if (ch == '*') {
addSprite(newMap, goalSprite, x, y);
} else if (ch == '1') {
addSprite(newMap, grubSprite, x, y);
} else if (ch == '2') {
addSprite(newMap, flySprite, x, y);
}
}
}
// add the player to the map
Sprite player = (Sprite) playerSprite.clone();
player.setX(TileMapRenderer.tilesToPixels(3));
player.setY(0);
newMap.setPlayer(player);
return newMap;
}
private void addSprite(TileMap map, Sprite hostSprite, int tileX, int tileY) {
if (hostSprite != null) {
// clone the sprite from the "host"
Sprite sprite = (Sprite) hostSprite.clone();
// center the sprite
sprite.setX(TileMapRenderer.tilesToPixels(tileX)
+ (TileMapRenderer.tilesToPixels(1) - sprite.getWidth())
/ 2);
// bottom-justify the sprite
sprite.setY(TileMapRenderer.tilesToPixels(tileY + 1)
- sprite.getHeight());
// add it to the map
map.addSprite(sprite);
}
}
// -----------------------------------------------------------
// code for loading sprites and images
// -----------------------------------------------------------
public void loadTileImages() {
// keep looking for tile A,B,C, etc. this makes it
// easy to drop new tiles in the images/ directory
tiles = new ArrayList();
char ch = 'A';
while (true) {
String name = "tile_" + ch + ".png";
File file = new File("images/" + name);
if (!file.exists()) {
break;
}
tiles.add(loadImage(name));
ch++;
}
}
public void loadCreatureSprites() {
Image[][] images = new Image[4][];
// load left-facing images
images[0] = new Image[] { loadImage("player1.png"),
loadImage("player2.png"), loadImage("player3.png"),
loadImage("fly1.png"), loadImage("fly2.png"),
loadImage("fly3.png"), loadImage("grub1.png"),
loadImage("grub2.png"), };
images[1] = new Image[images[0].length];
images[2] = new Image[images[0].length];
images[3] = new Image[images[0].length];
for (int i = 0; i < images[0].length; i++) {
// right-facing images
images[1][i] = getMirrorImage(images[0][i]);
// left-facing "dead" images
images[2][i] = getFlippedImage(images[0][i]);
// right-facing "dead" images
images[3][i] = getFlippedImage(images[1][i]);
}
// create creature animations
Animation[] playerAnim = new Animation[4];
Animation[] flyAnim = new Animation[4];
Animation[] grubAnim = new Animation[4];
for (int i = 0; i < 4; i++) {
playerAnim[i] = createPlayerAnim(images[i][0], images[i][1],
images[i][2]);
flyAnim[i] = createFlyAnim(images[i][3], images[i][4], images[i][5]);
grubAnim[i] = createGrubAnim(images[i][6], images[i][7]);
}
// create creature sprites
playerSprite = new Player(playerAnim[0], playerAnim[1], playerAnim[2],
playerAnim[3]);
flySprite = new Fly(flyAnim[0], flyAnim[1], flyAnim[2], flyAnim[3]);
grubSprite = new Grub(grubAnim[0], grubAnim[1], grubAnim[2],
grubAnim[3]);
}
private Animation createPlayerAnim(Image player1, Image player2,
Image player3) {
Animation anim = new Animation();
anim.addFrame(player1, 250);
anim.addFrame(player2, 150);
anim.addFrame(player1, 150);
anim.addFrame(player2, 150);
anim.addFrame(player3, 200);
anim.addFrame(player2, 150);
return anim;
}
private Animation createFlyAnim(Image img1, Image img2, Image img3) {
Animation anim = new Animation();
anim.addFrame(img1, 50);
anim.addFrame(img2, 50);
anim.addFrame(img3, 50);
anim.addFrame(img2, 50);
return anim;
}
private Animation createGrubAnim(Image img1, Image img2) {
Animation anim = new Animation();
anim.addFrame(img1, 250);
anim.addFrame(img2, 250);
return anim;
}
private void loadPowerUpSprites() {
// create "goal" sprite
Animation anim = new Animation();
anim.addFrame(loadImage("heart1.png"), 150);
anim.addFrame(loadImage("heart2.png"), 150);
anim.addFrame(loadImage("heart3.png"), 150);
anim.addFrame(loadImage("heart2.png"), 150);
goalSprite = new PowerUp.Goal(anim);
// create "star" sprite
anim = new Animation();
anim.addFrame(loadImage("star1.png"), 100);
anim.addFrame(loadImage("star2.png"), 100);
anim.addFrame(loadImage("star3.png"), 100);
anim.addFrame(loadImage("star4.png"), 100);
coinSprite = new PowerUp.Star(anim);
// create "music" sprite
anim = new Animation();
anim.addFrame(loadImage("music1.png"), 150);
anim.addFrame(loadImage("music2.png"), 150);
anim.addFrame(loadImage("music3.png"), 150);
anim.addFrame(loadImage("music2.png"), 150);
musicSprite = new PowerUp.Music(anim);
}
}
/**
* A Grub is a Creature that moves slowly on the ground.
*/
class Grub extends Creature {
public Grub(Animation left, Animation right, Animation deadLeft,
Animation deadRight) {
super(left, right, deadLeft, deadRight);
}
public float getMaxSpeed() {
return 0.05f;
}
}
/**
* A PowerUp class is a Sprite that the player can pick up.
*/
abstract class PowerUp extends Sprite {
public PowerUp(Animation anim) {
super(anim);
}
public Object clone() {
// use reflection to create the correct subclass
Constructor constructor = getClass().getConstructors()[0];
try {
return constructor.newInstance(new Object[] { (Animation) anim
.clone() });
} catch (Exception ex) {
// should never happen
ex.printStackTrace();
return null;
}
}
/**
* A Star PowerUp. Gives the player points.
*/
public static class Star extends PowerUp {
public Star(Animation anim) {
super(anim);
}
}
/**
* A Music PowerUp. Changes the game music.
*/
public static class Music extends PowerUp {
public Music(Animation anim) {
super(anim);
}
}
/**
* A Goal PowerUp. Advances to the next map.
*/
public static class Goal extends PowerUp {
public Goal(Animation anim) {
super(anim);
}
}
}
/**
* A Fly is a Creature that fly slowly in the air.
*/
class Fly extends Creature {
public Fly(Animation left, Animation right, Animation deadLeft,
Animation deadRight) {
super(left, right, deadLeft, deadRight);
}
public float getMaxSpeed() {
return 0.2f;
}
public boolean isFlying() {
return isAlive();
}
}
/**
* 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;
}
}
/**
* The TileMapRenderer class draws a TileMap on the screen. It draws all tiles,
* sprites, and an optional background image centered around the position of the
* player.
*
* <p>
* If the width of background image is smaller the width of the tile map, the
* background image will appear to move slowly, creating a parallax background
* effect.
*
* <p>
* Also, three static methods are provided to convert pixels to tile positions,
* and vice-versa.
*
* <p>
* This TileMapRender uses a tile size of 64.
*/
class TileMapRenderer {
private static final int TILE_SIZE = 64;
// the size in bits of the tile
// Math.pow(2, TILE_SIZE_BITS) == TILE_SIZE
private static final int TILE_SIZE_BITS = 6;
private Image background;
/**
* Converts a pixel position to a tile position.
*/
public static int pixelsToTiles(float pixels) {
return pixelsToTiles(Math.round(pixels));
}
/**
* Converts a pixel position to a tile position.
*/
public static int pixelsToTiles(int pixels) {
// use shifting to get correct values for negative pixels
return pixels >> TILE_SIZE_BITS;
// or, for tile sizes that aren't a power of two,
// use the floor function:
//return (int)Math.floor((float)pixels / TILE_SIZE);
}
/**
* Converts a tile position to a pixel position.
*/
public static int tilesToPixels(int numTiles) {
// no real reason to use shifting here.
// it's slighty faster, but doesn't add up to much
// on modern processors.
return numTiles << TILE_SIZE_BITS;
// use this if the tile size isn't a power of 2:
//return numTiles * TILE_SIZE;
}
/**
* Sets the background to draw.
*/
public void setBackground(Image background) {
this.background = background;
}
/**
* Draws the specified TileMap.
*/
public void draw(Graphics2D g, TileMap map, int screenWidth,
int screenHeight) {
Sprite player = map.getPlayer();
int mapWidth = tilesToPixels(map.getWidth());
// get the scrolling position of the map
// based on player's position
int offsetX = screenWidth / 2 - Math.round(player.getX()) - TILE_SIZE;
offsetX = Math.min(offsetX, 0);
offsetX = Math.max(offsetX, screenWidth - mapWidth);
// get the y offset to draw all sprites and tiles
int offsetY = screenHeight - tilesToPixels(map.getHeight());
// draw black background, if needed
if (background == null || screenHeight > background.getHeight(null)) {
g.setColor(Color.black);
g.fillRect(0, 0, screenWidth, screenHeight);
}
// draw parallax background image
if (background != null) {
int x = offsetX * (screenWidth - background.getWidth(null))
/ (screenWidth - mapWidth);
int y = screenHeight - background.getHeight(null);
g.drawImage(background, x, y, null);
}
// draw the visible tiles
int firstTileX = pixelsToTiles(-offsetX);
int lastTileX = firstTileX + pixelsToTiles(screenWidth) + 1;
for (int y = 0; y < map.getHeight(); y++) {
for (int x = firstTileX; x <= lastTileX; x++) {
Image image = map.getTile(x, y);
if (image != null) {
g.drawImage(image, tilesToPixels(x) + offsetX,
tilesToPixels(y) + offsetY, null);
}
}
}
// draw player
g.drawImage(player.getImage(), Math.round(player.getX()) + offsetX,
Math.round(player.getY()) + offsetY, null);
// draw sprites
Iterator i = map.getSprites();
while (i.hasNext()) {
Sprite sprite = (Sprite) i.next();
int x = Math.round(sprite.getX()) + offsetX;
int y = Math.round(sprite.getY()) + offsetY;
g.drawImage(sprite.getImage(), x, y, null);
// wake up the creature when it's on screen
if (sprite instanceof Creature && x >= 0 && x < screenWidth) {
((Creature) sprite).wakeUp();
}
}
}
}
/**
* 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();
}
}
}
/**
* The SoundManager class manages sound playback. The SoundManager is a
* ThreadPool, with each thread playing back one sound at a time. This allows
* the SoundManager to easily limit the number of simultaneous sounds being
* played.
* <p>
* Possible ideas to extend this class:
* <ul>
* <li>add a setMasterVolume() method, which uses Controls to set the volume
* for each line.
* <li>don't play a sound if more than, say, 500ms has passed since the request
* to play
* </ul>
*/
class SoundManager extends ThreadPool {
private AudioFormat playbackFormat;
private ThreadLocal localLine;
private ThreadLocal localBuffer;
private Object pausedLock;
private boolean paused;
/**
* Creates a new SoundManager using the maximum number of simultaneous
* sounds.
*/
public SoundManager(AudioFormat playbackFormat) {
this(playbackFormat, getMaxSimultaneousSounds(playbackFormat));
}
/**
* Creates a new SoundManager with the specified maximum number of
* simultaneous sounds.
*/
public SoundManager(AudioFormat playbackFormat, int maxSimultaneousSounds) {
super(Math.min(maxSimultaneousSounds,
getMaxSimultaneousSounds(playbackFormat)));
this.playbackFormat = playbackFormat;
localLine = new ThreadLocal();
localBuffer = new ThreadLocal();
pausedLock = new Object();
// notify threads in pool it's ok to start
synchronized (this) {
notifyAll();
}
}
/**
* Gets the maximum number of simultaneous sounds with the specified
* AudioFormat that the default mixer can play.
*/
public static int getMaxSimultaneousSounds(AudioFormat playbackFormat) {
DataLine.Info lineInfo = new DataLine.Info(SourceDataLine.class,
playbackFormat);
Mixer mixer = AudioSystem.getMixer(null);
return mixer.getMaxLines(lineInfo);
}
/**
* Does any clean up before closing.
*/
protected void cleanUp() {
// signal to unpause
setPaused(false);
// close the mixer (stops any running sounds)
Mixer mixer = AudioSystem.getMixer(null);
if (mixer.isOpen()) {
mixer.close();
}
}
public void close() {
cleanUp();
super.close();
}
public void join() {
cleanUp();
super.join();
}
/**
* Sets the paused state. Sounds may not pause immediately.
*/
public void setPaused(boolean paused) {
if (this.paused != paused) {
synchronized (pausedLock) {
this.paused = paused;
if (!paused) {
// restart sounds
pausedLock.notifyAll();
}
}
}
}
/**
* Returns the paused state.
*/
public boolean isPaused() {
return paused;
}
/**
* Loads a Sound from the file system. Returns null if an error occurs.
*/
public Sound getSound(String filename) {
return getSound(getAudioInputStream(filename));
}
/**
* Loads a Sound from an input stream. Returns null if an error occurs.
*/
public Sound getSound(InputStream is) {
return getSound(getAudioInputStream(is));
}
/**
* Loads a Sound from an AudioInputStream.
*/
public Sound getSound(AudioInputStream audioStream) {
if (audioStream == null) {
return null;
}
// get the number of bytes to read
int length = (int) (audioStream.getFrameLength() * audioStream
.getFormat().getFrameSize());
// read the entire stream
byte[] samples = new byte[length];
DataInputStream is = new DataInputStream(audioStream);
try {
is.readFully(samples);
is.close();
} catch (IOException ex) {
ex.printStackTrace();
}
// return the samples
return new Sound(samples);
}
/**
* Creates an AudioInputStream from a sound from the file system.
*/
public AudioInputStream getAudioInputStream(String filename) {
try {
return getAudioInputStream(new FileInputStream(filename));
} catch (IOException ex) {
ex.printStackTrace();
return null;
}
}
/**
* Creates an AudioInputStream from a sound from an input stream
*/
public AudioInputStream getAudioInputStream(InputStream is) {
try {
if (!is.markSupported()) {
is = new BufferedInputStream(is);
}
// open the source stream
AudioInputStream source = AudioSystem.getAudioInputStream(is);
// convert to playback format
return AudioSystem.getAudioInputStream(playbackFormat, source);
} catch (UnsupportedAudioFileException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
} catch (IllegalArgumentException ex) {
ex.printStackTrace();
}
return null;
}
/**
* Plays a sound. This method returns immediately.
*/
public InputStream play(Sound sound) {
return play(sound, null, false);
}
/**
* Plays a sound with an optional SoundFilter, and optionally looping. This
* method returns immediately.
*/
public InputStream play(Sound sound, SoundFilter filter, boolean loop) {
InputStream is;
if (sound != null) {
if (loop) {
is = new LoopingByteInputStream(sound.getSamples());
} else {
is = new ByteArrayInputStream(sound.getSamples());
}
return play(is, filter);
}
return null;
}
/**
* Plays a sound from an InputStream. This method returns immediately.
*/
public InputStream play(InputStream is) {
return play(is, null);
}
/**
* Plays a sound from an InputStream with an optional sound filter. This
* method returns immediately.
*/
public InputStream play(InputStream is, SoundFilter filter) {
if (is != null) {
if (filter != null) {
is = new FilteredSoundStream(is, filter);
}
runTask(new SoundPlayer(is));
}
return is;
}
/**
* Signals that a PooledThread has started. Creates the Thread's line and
* buffer.
*/
protected void threadStarted() {
// wait for the SoundManager constructor to finish
synchronized (this) {
try {
wait();
} catch (InterruptedException ex) {
}
}
// use a short, 100ms (1/10th sec) buffer for filters that
// change in real-time
int bufferSize = playbackFormat.getFrameSize()
* Math.round(playbackFormat.getSampleRate() / 10);
// create, open, and start the line
SourceDataLine line;
DataLine.Info lineInfo = new DataLine.Info(SourceDataLine.class,
playbackFormat);
try {
line = (SourceDataLine) AudioSystem.getLine(lineInfo);
line.open(playbackFormat, bufferSize);
} catch (LineUnavailableException ex) {
// the line is unavailable - signal to end this thread
Thread.currentThread().interrupt();
return;
}
line.start();
// create the buffer
byte[] buffer = new byte[bufferSize];
// set this thread's locals
localLine.set(line);
localBuffer.set(buffer);
}
/**
* Signals that a PooledThread has stopped. Drains and closes the Thread's
* Line.
*/
protected void threadStopped() {
SourceDataLine line = (SourceDataLine) localLine.get();
if (line != null) {
line.drain();
line.close();
}
}
/**
* The SoundPlayer class is a task for the PooledThreads to run. It receives
* the threads's Line and byte buffer from the ThreadLocal variables and
* plays a sound from an InputStream.
* <p>
* This class only works when called from a PooledThread.
*/
protected class SoundPlayer implements Runnable {
private InputStream source;
public SoundPlayer(InputStream source) {
this.source = source;
}
public void run() {
// get line and buffer from ThreadLocals
SourceDataLine line = (SourceDataLine) localLine.get();
byte[] buffer = (byte[]) localBuffer.get();
if (line == null || buffer == null) {
// the line is unavailable
return;
}
// copy data to the line
try {
int numBytesRead = 0;
while (numBytesRead != -1) {
// if paused, wait until unpaused
synchronized (pausedLock) {
if (paused) {
try {
pausedLock.wait();
} catch (InterruptedException ex) {
return;
}
}
}
// copy data
numBytesRead = source.read(buffer, 0, buffer.length);
if (numBytesRead != -1) {
line.write(buffer, 0, numBytesRead);
}
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
/**
* 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;
}
}
/**
* 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 Sound class is a container for sound samples. The sound samples are
* format-agnostic and are stored as a byte array.
*/
class Sound {
private byte[] samples;
/**
* Create a new Sound object with the specified byte array. The array is not
* copied.
*/
public Sound(byte[] samples) {
this.samples = samples;
}
/**
* Returns this Sound's objects samples as a byte array.
*/
public byte[] getSamples() {
return samples;
}
}
/**
* 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;
}
}
class MidiPlayer implements MetaEventListener {
// Midi meta event
public static final int END_OF_TRACK_MESSAGE = 47;
private Sequencer sequencer;
private boolean loop;
private boolean paused;
/**
* Creates a new MidiPlayer object.
*/
public MidiPlayer() {
try {
sequencer = MidiSystem.getSequencer();
sequencer.open();
sequencer.addMetaEventListener(this);
} catch (MidiUnavailableException ex) {
sequencer = null;
}
}
/**
* Loads a sequence from the file system. Returns null if an error occurs.
*/
public Sequence getSequence(String filename) {
try {
return getSequence(new FileInputStream(filename));
} catch (IOException ex) {
ex.printStackTrace();
return null;
}
}
/**
* Loads a sequence from an input stream. Returns null if an error occurs.
*/
public Sequence getSequence(InputStream is) {
try {
if (!is.markSupported()) {
is = new BufferedInputStream(is);
}
Sequence s = MidiSystem.getSequence(is);
is.close();
return s;
} catch (InvalidMidiDataException ex) {
ex.printStackTrace();
return null;
} catch (IOException ex) {
ex.printStackTrace();
return null;
}
}
/**
* Plays a sequence, optionally looping. This method returns immediately.
* The sequence is not played if it is invalid.
*/
public void play(Sequence sequence, boolean loop) {
if (sequencer != null && sequence != null && sequencer.isOpen()) {
try {
sequencer.setSequence(sequence);
sequencer.start();
this.loop = loop;
} catch (InvalidMidiDataException ex) {
ex.printStackTrace();
}
}
}
/**
* This method is called by the sound system when a meta event occurs. In
* this case, when the end-of-track meta event is received, the sequence is
* restarted if looping is on.
*/
public void meta(MetaMessage event) {
if (event.getType() == END_OF_TRACK_MESSAGE) {
if (sequencer != null && sequencer.isOpen() && loop) {
sequencer.start();
}
}
}
/**
* Stops the sequencer and resets its position to 0.
*/
public void stop() {
if (sequencer != null && sequencer.isOpen()) {
sequencer.stop();
sequencer.setMicrosecondPosition(0);
}
}
/**
* Closes the sequencer.
*/
public void close() {
if (sequencer != null && sequencer.isOpen()) {
sequencer.close();
}
}
/**
* Gets the sequencer.
*/
public Sequencer getSequencer() {
return sequencer;
}
/**
* Sets the paused state. Music may not immediately pause.
*/
public void setPaused(boolean paused) {
if (this.paused != paused && sequencer != null && sequencer.isOpen()) {
this.paused = paused;
if (paused) {
sequencer.stop();
} else {
sequencer.start();
}
}
}
/**
* Returns the paused state.
*/
public boolean isPaused() {
return paused;
}
}
/**
* The TileMap class contains the data for a tile-based map, including Sprites.
* Each tile is a reference to an Image. Of course, Images are used multiple
* times in the tile map.
*/
class TileMap {
private Image[][] tiles;
private LinkedList sprites;
private Sprite player;
/**
* Creates a new TileMap with the specified width and height (in number of
* tiles) of the map.
*/
public TileMap(int width, int height) {
tiles = new Image[width][height];
sprites = new LinkedList();
}
/**
* Gets the width of this TileMap (number of tiles across).
*/
public int getWidth() {
return tiles.length;
}
/**
* Gets the height of this TileMap (number of tiles down).
*/
public int getHeight() {
return tiles[0].length;
}
/**
* Gets the tile at the specified location. Returns null if no tile is at
* the location or if the location is out of bounds.
*/
public Image getTile(int x, int y) {
if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) {
return null;
} else {
return tiles[x][y];
}
}
/**
* Sets the tile at the specified location.
*/
public void setTile(int x, int y, Image tile) {
tiles[x][y] = tile;
}
/**
* Gets the player Sprite.
*/
public Sprite getPlayer() {
return player;
}
/**
* Sets the player Sprite.
*/
public void setPlayer(Sprite player) {
this.player = player;
}
/**
* Adds a Sprite object to this map.
*/
public void addSprite(Sprite sprite) {
sprites.add(sprite);
}
/**
* Removes a Sprite object from this map.
*/
public void removeSprite(Sprite sprite) {
sprites.remove(sprite);
}
/**
* Gets an Iterator of all the Sprites in this map, excluding the player
* Sprite.
*/
public Iterator getSprites() {
return sprites.iterator();
}
}
class Sprite {
protected 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();
}
/**
* Clones this Sprite. Does not clone position or velocity info.
*/
public Object clone() {
return new Sprite(anim);
}
}
|