Code is also available in an archive.
GameApplet.java
/*
<APPLET CODE="GameApplet.class" WIDTH=600 HEIGHT=600></APPLET>
*/
import javax.swing.*;
import java.awt.*;
/**
* An applet that displays a game.
*
* @author Jim Glenn
* @version 0.1 from LiberationApplet of 1/8/2009
*/
public class GameApplet extends JApplet
{
public void init()
{
GameModel model = new GameModel();
GameView view = new GameView(model);
GameControl control = new GameControl(model);
view.addKeyListener(control);
getContentPane().setLayout(new BorderLayout());
getContentPane().add(view, BorderLayout.CENTER);
}
}
GameControl.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
/**
* The controller for a game. The controller handles the time and
* the player's input.
*
* @author Jim Glenn
* @version 0.1 from LiberationGame of 1/8/2009 (from CalaverasCrossingControl)
*/
public class GameControl extends KeyAdapter
{
/**
* The game model this controller is connected to.
*/
protected GameModel model;
/**
* Creates a controller connected to the given game model and view.
*
* @param g a game
*/
public GameControl(GameModel m)
{
model = m;
Timer t = new Timer(1000 / 20,
new ActionListener() {
public void actionPerformed(ActionEvent e)
{
timerTick();
}
});
t.start();
}
/**
* Handles timer events.
*/
public synchronized void timerTick()
{
model.update();
}
}
GameModel.java
import java.util.*;
/**
* The current state of a game.
*
* @author Jim Glenn
* @version 0.1 1/19/2009 from LiberationGame of 1/8/2009
*/
public class GameModel extends Observable
{
/**
* The list of sprites in this game.
*/
private List< Sprite > sprites;
/**
* The list of sprites to be removed at the next update. This is
* necessary because as we iterate through sprites they may want to
* remove other sprites, but we can't modify the list of sprites
* as we iterate through it.
*/
private Set< Sprite > toRemove;
/**
* The time of the last update to this game.
*/
private long lastUpdate;
/**
* The number of milliseconds in a second.
*/
private static final double MILLIS_PER_SECOND = 1000.0;
/**
* Creates a new game.
*/
public GameModel()
{
sprites = new ArrayList< Sprite >();
toRemove = new HashSet< Sprite >();
lastUpdate = -1;
}
/**
* Returns a list of the active sprites in this game.
*
* @return a list of sprites in this game
*/
public List< Sprite > getSprites()
{
List< Sprite > l = new LinkedList< Sprite >(sprites);
l.removeAll(toRemove);
return l;
}
/**
* Adds the given sprite to this game.
*
* @param s the sprite to add
*/
public void addSprite(Sprite s)
{
sprites.add(s);
}
/**
* Removes the given sprite from this game.
*
* @param s the sprite to remove
*/
public void removeSprite(Sprite s)
{
toRemove.add(s);
}
/**
* Returns the width of the playable area of this game.
*
* @return the width of this game
*/
public double getWidth()
{
return 1.0;
}
/**
* Returns the height of the playable area of this game.
*
* @return the height of this game
*/
public double getHeight()
{
return 1.0;
}
/**
* Removes sprites that are currently on the to-be-removed list.
*/
private void processRemovedSprites()
{
for (Sprite s : toRemove)
{
sprites.remove(s);
}
toRemove.clear();
}
/**
* Updates all the sprites in this world.
*/
public void update()
{
// update sprites
long currTime = System.currentTimeMillis();
if (lastUpdate != -1)
{
for (Sprite s : sprites)
{
s.update((currTime - lastUpdate) / MILLIS_PER_SECOND,
this);
}
}
lastUpdate = currTime;
// check for and handle collisions
checkCollisions();
// process removed list
processRemovedSprites();
// notify view that is presumably waiting for updates from this model
setChanged();
notifyObservers();
}
/**
* Checks for and handles collisions between sprites. Collision
* handling between a pair of sprites is delegated to the
* <CODE>handleCollisions</CODE> method; subclasses should
* override that method for actions appropriate to the particular
* game.
*/
protected void checkCollisions()
{
long start = System.currentTimeMillis();
// iterate over the n(n-1)/2 distinct pairs of sprites
for (int i = 0; i < sprites.size(); i++)
{
Sprite s1 = sprites.get(i);
if (!toRemove.contains(s1))
{
for (int j = i + 1; j < sprites.size(); j++)
{
Sprite s2 = sprites.get(j);
if (!toRemove.contains(s2)
&& close(s1, s2)
&& s1.collidesWith(s2))
{
handleCollision(s1, s2);
}
}
}
}
long end = System.currentTimeMillis();
System.out.println("checkCollisions: " + (end - start) + "ms");
}
/**
* Checks if the two given sprites are close enough to collide.
* This check is performed using the bounding radius: if the
* distance between the control points of the sprites is less than
* the sum of their radii then the sprites are close enough to
* collide. Because this is a very rough collision check, false
* positives are possible: the return value may be <CODE>false</CODE>
* even though the sprites are not in collision. Therefore
* a more precise collision test is necessary.
*
*
* @param s1 a sprite
* @param s2 a sprite
* @return false if the sprites are not in collision
*/
private boolean close(Sprite s1, Sprite s2)
{
double sumRadii = s1.getBoundingRadius() + s2.getBoundingRadius();
return (Math.sqrt(Math.pow(s1.getX() - s2.getX(), 2)
+ Math.pow(s1.getY() - s2.getY(), 2)) < sumRadii);
}
/**
* Handles a collision between the given sprites. This implementation
* does nothing; subclasses should override this method to implement
* the rules of a game.
*
* @param s1 a sprite
* @param s2 a sprite that has collided with <CODE>s2</CODE>
*/
protected void handleCollision(Sprite s1, Sprite s2)
{
System.out.println("Oof: " + s1 + " " + s2);
}
}
GameView.java
import java.util.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import javax.swing.*;
/**
* A panel that displays a game.
*
* @author Jim Glenn
* @version 0.1 from LiberationPanel of 1/8/2009
*/
public class GameView extends JPanel implements Observer
{
/**
* The game this view displays.
*/
private GameModel model;
/**
* Creates a new view that displays the given game.
*
* @param g the game to display in the new view
*/
public GameView(GameModel m)
{
model = m;
model.addObserver(this);
// get this panel the focus so its KeyListeners will get events
setFocusable(true);
addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e)
{
requestFocusInWindow();
}
}
);
}
/**
* Automatically invoked when the model invokes its notifyObservers
* method.
*/
public void update(Observable m, Object args)
{
repaint();
}
/**
* Draws this view.
*
* @param g the graphics context to draw on.
*/
public void paint(Graphics g)
{
g.clearRect(0, 0, getWidth(), getHeight());
// we will draw the whole playable area in this panel
// determine the scale factor to translate from game coordinates
// to view coordinates
double scale = Math.min(getWidth() / model.getWidth(),
getHeight() / model.getHeight());
for (Sprite s : model.getSprites())
{
drawSprite(g, s, scale);
}
}
/**
* Draws the given sprite in this view.
*
* @param g the graphics context to draw in
* @param s the sprite to draw
* @param scale the scaling factor that defines the transformation
* from game coordinates to pixel coordinates
*/
private void drawSprite(Graphics g, Sprite s, double scale)
{
// translate the outline of the sprite from a GeneralPath in
// game coordinates to a Polygon in pixel coordinates
Polygon p = new Polygon();
GeneralPath outline = s.getOutline();
PathIterator i = outline.getPathIterator(new AffineTransform());
double[] coords = new double[6];
while (!i.isDone())
{
if (i.currentSegment(coords) != PathIterator.SEG_CLOSE)
{
p.addPoint((int)(coords[0] * scale),
(int)(coords[1] * scale));
}
i.next();
}
// draw the polygon
g.setColor(s.getColor());
g.fillPolygon(p);
}
}
GameWindow.java
import javax.swing.*;
import java.awt.*;
/**
* A window that displays a game.
*
* @author Jim Glenn
* @version 0.1 from LiberationApplet of 1/8/2009
*/
public class GameWindow extends JFrame
{
public GameWindow()
{
GameModel model = new GameModel();
GameView view = new GameView(model);
GameControl control = new GameControl(model);
view.addKeyListener(control);
getContentPane().setLayout(new BorderLayout());
getContentPane().add(view, BorderLayout.CENTER);
}
public static void main(String[] args)
{
JFrame win = new GameWindow();
win.setSize(600, 600);
win.setVisible(true);
win.setDefaultCloseOperation(EXIT_ON_CLOSE);
}
}
Sprite.java
import java.util.*;
import java.awt.Color;
import java.awt.geom.*;
/**
* A moving object in a game. Sprites have positions, velocities, and
* sizes. The size is given as the radius of a circle that surrounds the
* sprite. Such a size is intended only for an approximate determination
* if two sprites collide.
*
* @author Jim Glenn
* @version 0.1 1/19/2009 from Liberation's Sprite.java of 1/8/2009
*/
public class Sprite
{
/**
* The x-coordinate of this sprite.
*/
protected double x;
/**
* The y-coordinate of this sprite.
*/
protected double y;
/**
* The x-component of this sprite's velocity.
*/
protected double vx;
/**
* The y-component of this sprite's velocity.
*/
protected double vy;
/**
* The size of a circle that encloses this sprite.
*/
protected double radius;
/**
* The color of this sprite.
*/
protected Color color;
/**
* The state of this sprite.
*/
protected int state;
/**
* The normal state of a sprite. This constant is defined to be 0.
*/
public static final int STATE_NORMAL = 0;
/**
* The amount of time this sprite has spent in its current state.
* Measured in seconds.
*/
private double stateTime;
/**
* Creates a new sprite at the given position.
* The sprite will be black and stationary.
*
* @param initX the x-coordinate of the new sprite
* @param initY the y-coordinate of the new sprite
* @param r the radius of a circle that bounds the new sprite
*/
public Sprite(double initX, double initY, double r)
{
this(initX, initY, r, 0.0, 0.0, Color.BLACK);
}
/**
* Creates a new sprite at the given position.
* The sprite will be stationary.
*
* @param initX the x-coordinate of the new sprite
* @param initY the y-coordinate of the new sprite
* @param r the radius of a circle that bounds the new sprite
* @param c the color of the new sprite
*/
public Sprite(double initX, double initY, double r, Color c)
{
this(initX, initY, r, 0.0, 0.0, c);
}
/**
* Creates a new sprite at the given position.
*
* @param initX the x-coordinate of the new sprite
* @param initY the y-coordinate of the new sprite
* @param r the radius of a circle that bounds the new sprite
* @param initVx the x-component of the velocity of the new sprite
* @param initVy the y-component of the velocity of the new sprite
* @param c the color of the new sprite
*/
public Sprite(double initX, double initY, double r, double initVx, double initVy, Color c)
{
x = initX;
y = initY;
radius = r;
vx = initVx;
vy = initVy;
color = c;
}
/**
* Returns the x-coordinate of this sprite.
*
* @return the x-coordinate of this sprite
*/
public double getX()
{
return x;
}
/**
* Returns the y-coordinate of this sprite.
*
* @return the y-coordinate of this sprite
*/
public double getY()
{
return y;
}
/**
* Returns the color of this sprite.
*
* @return the color of this sprite
*/
public Color getColor()
{
return color;
}
/**
* Returns the maximum speed of this sprite. This implementation
* returns positive infinity, indicating no limit. Subclasses should
* override this method so that <CODE>update</CODE> will take
* a maximum speed into account.
*/
public double getMaximumSpeed()
{
return Double.POSITIVE_INFINITY;
}
/**
* Sets the velocity of this sprite. If the given velocity exceeds the
* maximum, it will be reduced so that it equals the maximum. In such
* a case the direction will remain the same.
*
* @param vx the new velocity in the horizontal direction
* @param vy the new velocity in the vertical direction
*/
public void setVelocity(double vxNew, double vyNew)
{
vx = vxNew;
vy = vyNew;
// limit speed
double v = Math.sqrt(vx * vx + vy * vy);
double maxV = getMaximumSpeed();
if (v > maxV)
{
vx /= v / maxV;
vy /= v / maxV;
}
}
/**
* Returns the radius of a circle that encloses this sprite.
*
* @return the bounding radius of this sprite
*/
public double getBoundingRadius()
{
return radius;
}
/**
* Returns the polygonal outline of this Sprite. This implementation
* returns the square inscribed in the bounding circle as defined
* by <CODE>getBoundingRadius</CODE> and that has sides parallel to the
* axes of the coordinate system. Subclasses should override this
* method for sprites of other shapes.
*
* @return the outline of this sprite
*/
public GeneralPath getOutline()
{
GeneralPath outline = new GeneralPath();
outline.moveTo((float)(x + Math.cos(Math.PI / 4) * getBoundingRadius()),
(float)(y + Math.sin(Math.PI / 4) * getBoundingRadius()));
outline.lineTo((float)(x + Math.cos(Math.PI / 4) * getBoundingRadius()),
(float)(y - Math.sin(Math.PI / 4) * getBoundingRadius()));
outline.lineTo((float)(x - Math.cos(Math.PI / 4) * getBoundingRadius()),
(float)(y - Math.sin(Math.PI / 4) * getBoundingRadius()));
outline.lineTo((float)(x - Math.cos(Math.PI / 4) * getBoundingRadius()),
(float)(y + Math.sin(Math.PI / 4) * getBoundingRadius()));
outline.closePath();
return outline;
}
/**
* Determines if this sprite collides with the given sprite.
* Two sprites are considered in collision if their outlines intersect.
* (Note that this ignores the special case of one sprite completely
* inside the other.)
*
* @return true if and only if the two sprites are in collision
*/
public boolean collidesWith(Sprite s)
{
// first, get the points and outlines
List< Point2D > p1 = getPointsList();
List< Point2D > p2 = s.getPointsList();
GeneralPath out1 = getOutline();
GeneralPath out2 = s.getOutline();
// iterate over each segment in this sprite's outline
for (int i = 0; i < p1.size() - 1; i++)
{
// test points for being inside or part of
// a segment than intersects
for (int j = 0; j < p2.size() - 1; j++)
{
if (Line2D.linesIntersect(p1.get(i).getX(),
p1.get(i).getY(),
p1.get(i + 1).getX(),
p1.get(i + 1).getY(),
p2.get(j).getX(),
p2.get(j).getY(),
p2.get(j + 1).getX(),
p2.get(j + 1).getY())
)
return true;
}
}
// we also need to test whether points of one polygon are
// inside the other; we assume here that updates are often
// enough so that there is an edge collision detected above
// before such a case (but suppose the peanut randomly appears
// inside the player or the elephant...)
return false;
}
/**
* Returns the list of points on the outline of this sprite. The
* starting point is on the list at the start and at the end.
* This implementation currently returns an <CODE>ArrayList</CODE>
* in order to facilitate random access to the points.
*
* @return a list of points on the outline of this sprite
*/
private List< Point2D > getPointsList()
{
List< Point2D > l = new ArrayList< Point2D >();
double[] coords = new double[6]; // for getting coords from iterator
PathIterator i = getOutline().getPathIterator(new AffineTransform());
while (!i.isDone())
{
i.currentSegment(coords);
l.add(new Point2D.Double(coords[0], coords[1]));
i.next();
}
// add first point as last
l.add(l.get(0));
return l;
}
/**
* Updates this sprite's position and velocity. The velocity will be
* adjusted so that it does not exceed the maximum speed as specified by
* the <CODE>getMaximumSpeed</CODE> method.
*
* @param t the time since the last update, in seconds
* @param w the world this sprite belongs to
*/
public void update(double t, GameModel m)
{
stateTime += t;
// update position
x += vx * t;
y += vy * t;
}
/**
* Returns the state of this sprite.
*
* @return the current state
*/
public int getState()
{
return state;
}
/**
* Sets the state of this sprite. The state timer is reset to 0.
*
* @param s the new state
*/
public void setState(int s)
{
state = s;
stateTime = 0.0;
}
/**
* Returns the amount of time this sprite has spent in its current state.
*
* @return the time in the current state
*/
public double getStateTime()
{
return stateTime;
}
}
This code can also be downloaded from the files
GameApplet.java,
GameControl.java,
GameModel.java,
GameView.java,
GameWindow.java,
and Sprite.java.