CS 489.02 - Computers and Games - Spring 2009
Game Skeleton


Loyola College > Department of Computer Science > Dr. James Glenn > CS 489.02 > Examples and Lecture Notes > Game Skeleton

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.