All this time we've been talking about objects as actors but we've only built fairly boring things like lamps and simple dividers. Now that we know something about how Java works, it's time to begin to see what Java can really do.
Let's take Java out for a spin and build a game.
Adding Some Actors
Let's build a ball. For the time being, our ball will just be a simple filled circle. Each ball has a size and a position. First though, this actor needs some supporting actors to help it do its job.
public class Tuple
{
/*
Define an object to hold two double values.
This object could hold the horizontal and vertical
components of a displayable object's screen positions
or velocities.
*/
//this tuple has two double values
private double x, y;
public Tuple(double x, double y)
{
/*
Initialize this tuple with the given values.
*/
this.x = x; this.y = y;
}
public final double getX()
{
/*
Report this tuple's x-value.
*/
return x;
}
public final void setX(double x)
{
/*
Alter this tuple's x-value.
*/
this.x = x;
}
public final double getY()
{
/*
Report this tuple's y-value.
*/
return y;
}
public final void setY(double y)
{
/*
Alter this tuple's y-value.
*/
this.y = y;
}
}
public class Dimensions
{
/*
Define an object to hold two int values.
This object could hold a rectangle's
width and height.
*/
//this dimensions object has a width and a height
private int width, height;
public Dimensions(int width, int height)
{
/*
Initialize this dimensions' given values.
*/
this.width = width; this.height = height;
}
public final int getWidth()
{
/*
Report this dimensions' width.
*/
return width;
}
public final void setWidth(int width)
{
/*
Alter this dimensions' width.
*/
this.width = width;
}
public final int getHeight()
{
/*
Report this dimensions' height.
*/
return height;
}
public final void setHeight(int height)
{
/*
Alter this dimensions' height.
*/
this.height = height;
}
}
import Dimensions;
import java.awt.Graphics;
import java.applet.Applet;
import java.applet.AudioClip;
import java.net.URL;
import java.net.MalformedURLException;
public abstract class Actor
{
/*
Actors must have a act() method
to change their appearance or position
and a paint() method to display themselves
on a given smartTablet.
*/
//this actor exists in a world
//and that world has dimensions
private int worldWidth, worldHeight;
//this actor may have an associated sound
private AudioClip bumpSound;
public Actor(Dimensions worldDimensions)
{
/*
Initialize this actor with the given values.
*/
worldWidth = worldDimensions.getWidth();
worldHeight = worldDimensions.getHeight();
}
public Actor(Dimensions worldDimensions, String soundFilename)
{
/*
Initialize this actor with the given values.
*/
this(worldDimensions);
bumpSound = loadSound(soundFilename);
if (bumpSound == null)
System.out.println("Warning: sound not loaded");
}
public abstract void act();
public abstract void paint(Graphics smartTablet);
protected final int getWorldWidth()
{
/*
Report the world's width.
*/
return worldWidth;
}
protected final int getWorldHeight()
{
/*
Report the world's height.
*/
return worldHeight;
}
private final AudioClip loadSound(String soundFilename)
{
/*
Try to load the given sound from the 'sounds' subdirectory.
*/
//get the current directory and local file separator
String directory = System.getProperty("user.dir");
String separator = System.getProperty("file.separator");
//try to fetch the given sound file
URL soundFileURL = null;
try
{
soundFileURL = new URL("file:" + directory + separator +
"sounds" + separator + soundFilename);
}
catch (MalformedURLException ignored) {}
return Applet.newAudioClip(soundFileURL);
}
protected final void playSound()
{
/*
Play a sound.
*/
if (bumpSound != null)
bumpSound.play();
}
}
import Tuple;
import Circle;
import Actor;
import java.awt.Color;
import java.awt.Graphics;
public class Ball extends Actor
{
/*
Define an actor that can represent a moving ball.
*/
//this ball looks like a circle,
//and it has a velocity, and a color
private Circle circle;
private Tuple velocity;
private Color color;
public Ball(Dimensions dimensions, Circle circle,
Tuple velocity, Color color)
{
/*
Initialize this ball with the given values.
*/
super(dimensions);
this.circle =
new Circle(circle.getCenter(), circle.getRadius());
this.velocity =
new Tuple(velocity.getX(), velocity.getY());
this.color = color;
}
public Ball(Dimensions dimensions, Circle circle,
Tuple velocity, Color color, String soundFilename)
{
/*
Initialize this ball with the given values.
*/
super(dimensions, soundFilename);
this.circle =
new Circle(circle.getCenter(), circle.getRadius());
this.velocity =
new Tuple(velocity.getX(), velocity.getY());
this.color = color;
}
public void act()
{
/*
Move this ball by its current velocity.
*/
double xCenter = circle.getCenter().getX();
double yCenter = circle.getCenter().getY();
double newXCenter = xCenter + velocity.getX();
double newYCenter = yCenter + velocity.getY();
moveTo(new Tuple(newXCenter, newYCenter));
bounceOffWalls();
}
public void paint(Graphics smartTablet)
{
/*
Paint this ball on the given smartTablet.
*/
int radius = (int) circle.getRadius();
int leftEdge = (int) circle.getCenter().getX() - radius;
int topEdge = (int) circle.getCenter().getY() - radius;
int diameter = 2 * radius;
smartTablet.setColor(color);
smartTablet.fillOval(leftEdge, topEdge, diameter, diameter);
}
private final void moveTo(Tuple center)
{
/*
Move this ball to the given location.
*/
circle.setCenter(center);
}
private void bounceOffWalls()
{
/*
Bounce this ball off its walls.
*/
int worldWidth = getWorldWidth();
int worldHeight = getWorldHeight();
//get this ball's radius and center
double radius = circle.getRadius();
double diameter = 2 * radius;
double xCenter = circle.getCenter().getX();
double yCenter = circle.getCenter().getY();
//calculate this ball's current position
double leftEdge = xCenter - radius;
double rightEdge = xCenter + radius;
double topEdge = yCenter - radius;
double bottomEdge = yCenter + radius;
//remember whether this ball hits a wall or not
boolean hasHitAWall = false;
//has this ball hit the left wall while going left?
if ((leftEdge <= 0) && (velocity.getX() < 0))
{
velocity.setX(-velocity.getX());
xCenter = xCenter - 2 * leftEdge;
hasHitAWall = true;
}
//has this ball hit the right wall while going right?
if ((rightEdge >= worldWidth) && (velocity.getX() > 0))
{
velocity.setX(-velocity.getX());
xCenter = xCenter - 2 * (rightEdge - worldWidth);
hasHitAWall = true;
}
//has this ball hit the top wall while going up?
if ((topEdge <= 0) && (velocity.getY() < 0))
{
velocity.setY(-velocity.getY());
yCenter = yCenter - 2 * topEdge;
hasHitAWall = true;
}
//has this ball hit the bottom wall while going down?
if ((bottomEdge >= worldHeight) && (velocity.getY() > 0))
{
velocity.setY(-velocity.getY());
yCenter = yCenter - 2 * (bottomEdge - worldHeight);
hasHitAWall = true;
}
//if this ball has hit a wall, update its position
//and play a sound
if (hasHitAWall)
{
moveTo(new Tuple(xCenter, yCenter));
playSound();
}
}
}
import Actor;
import World;
import Dimensions;
import java.awt.Graphics;
import java.awt.Color;
import javax.swing.JPanel;
import javax.swing.border.EtchedBorder;
public class Stage extends JPanel
{
/*
Define an object that can represent a rectangular stage
for actors to paint themselves on.
*/
//this stage exists in a world
private World world;
//this stage has dimensions
private Dimensions dimensions;
public Stage(final World world, final Dimensions dimensions)
{
/*
Initialize this stage with the given values.
*/
this.world = world;
this.dimensions = dimensions;
this.setSize(dimensions.getWidth(), dimensions.getHeight());
this.setBackground(Color.black);
this.setBorder(new EtchedBorder(EtchedBorder.RAISED));
this.setDoubleBuffered(true);
this.setVisible(true);
}
public Dimensions getDimensions()
{
/*
Report this stage's dimensions.
*/
int width = dimensions.getWidth();
int height = dimensions.getHeight();
return new Dimensions(width, height);
}
public void paintComponent(Graphics smartTablet)
{
/*
Update this stage.
*/
super.paintComponent(smartTablet);
Actor[] actors = world.getActorArray();
for (int index = 0; index < actors.length; index++)
actors[index].paint(smartTablet);
}
}
import java.lang.Runnable;
import java.lang.Thread;
public class Clock implements Runnable
{
/*
Define an object that regularly executes
its creator's tick() method.
*/
//this clock was created by some clock observable
private ClockObservable creator;
//this clock uses a thread to tick
private Thread thread;
//this clock ticks some number of times per second;
//to do so it pauses for some number of milliseconds
//every second then tick()s its creator
private final int ticksPerSecond;
private final int pauseTimeInMilliseconds;
//all clocks have a default number of ticks per second
private final static int DEFAULT_TICKS_PER_SECOND = 30;
public Clock(ClockObservable creator)
{
/*
Initialize this clock with the given value.
*/
this(creator, Clock.DEFAULT_TICKS_PER_SECOND);
}
public Clock(ClockObservable creator, int ticksPerSecond)
{
/*
Initialize this clock with the given values.
*/
this.creator = creator;
this.ticksPerSecond = ticksPerSecond;
pauseTimeInMilliseconds = (1000 / ticksPerSecond);
thread = new Thread(this);
thread.start();
}
public void run()
{
/*
Regularly ask this clock's creator
to execute its tick() method.
*/
while (true)
{
creator.tick();
pause(pauseTimeInMilliseconds);
}
}
private final void pause(final int pauseTimeInMilliseconds)
{
/*
Pause this clock for the given time.
*/
try {Thread.sleep(pauseTimeInMilliseconds);}
catch (InterruptedException exception) {}
}
}
public interface ClockObservable
{
/*
ClockObservables must have a tick() method.
The clock they're observing will ask them
to execute it on each clock tick.
*/
public abstract void tick();
}
import Tuple;
import Circle;
import Ball;
import Clock;
import ClockObservable;
import Actor;
import Stage;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.BorderLayout;
import javax.swing.JFrame;
public class World extends JFrame implements ClockObservable
{
/*
Define an object that can represent a rectangular stage
with some actors and a clock.
*/
//this world has a stage, a set of actors, and a clock
private Stage stage;
private Actor[] actors;
private Clock clock;
public World(final Dimensions dimensions)
{
/*
Initialize this world with the given values.
*/
int width = dimensions.getWidth();
int height = dimensions.getHeight();
Dimensions stageDimensions =
new Dimensions(width - 10, height - 30);
stage = new Stage(this, stageDimensions);
actors = new Actor[1];
actors[0] = new Ball(stageDimensions,
new Circle(new Tuple(50, 25), 5),
new Tuple(3, 5), Color.red, "ip.au");
this.setSize(width, height);
this.getContentPane().setLayout(new BorderLayout());
this.getContentPane().add(stage, BorderLayout.CENTER);
this.setVisible(true);
clock = new Clock(this, 30);
}
public void tick()
{
/*
Update this world.
*/
//let the actors act
for (int index = 0; index < actors.length; index++)
actors[index].act();
//draw the actors in their new positions
//this.repaint();
stage.repaint();
}
public final Dimensions getDimensions()
{
/*
Report this world's dimensions.
*/
int width = stage.getDimensions().getWidth();
int height = stage.getDimensions().getHeight();
return new Dimensions(width, height);
}
public Actor[] getActorArray()
{
/*
Report this world's array of actors.
//note flaw: outside objects can change actors!
*/
return actors;
}
public static void main(final String[] parameters)
{
/*
Create a world.
*/
new World(new Dimensions(400, 400));
}
}
Animating the Game
The simplest element of any active game is regular motion, which means we need a clock. Our clock's sole purpose is to ask its creator to execute its tick() method so many times a second. Here's the code:
This class uses several elements of Java we haven't yet seen.
For the moment, they will all remain mysterious.
The important thing right now is that objects of this class
ask their creator to execute its tick()
method once per second.
They are all Clock
s.
Building a Stage
Now let's create a drawing surface to display the ball on.
This will be our actors' stage.
Starting the Play
Threads
Forcing Preemptive Scheduling
package com.knownspace.tools;
public final class ThreadScheduler extends Thread
{
/*
Implement a simple thread scheduler
that forces preemptive scheduling for
NORM_PRIORITY threads (i.e. threads with default priority)
even if the JVM this is executing on
uses cooperative scheduling.
To use this class: create a new instance
of ThreadScheduler (with whatever timeSlice
and priority you wish---or use the defaults).
It will start itself.
There is a small cost when using this
in a JVM that is already preemptive,
but it's well worth using it always
since we never have to worry about scheduling
ever again and the extra cost is very small.
*/
//amount of milliseconds
//to give to each NORM_PRIORITY thread
private int timeSlice;
//if a timeSlice isn't specified,
//use this as the default (in milliseconds)
private final static int DEFAULT_TIMESLICE = 100;
public ThreadScheduler()
{
this(DEFAULT_TIMESLICE);
}
public ThreadScheduler(int timeSlice)
{
this(timeSlice, Thread.NORM_PRIORITY + 1);
}
public ThreadScheduler(int timeSlice, int priority)
{
this.timeSlice = timeSlice;
//make sure the given priority is legal
if (priority < Thread.MIN_PRIORITY)
priority = Thread.MIN_PRIORITY;
if (priority > Thread.MAX_PRIORITY)
priority = Thread.MAX_PRIORITY;
this.setPriority(priority);
//if this is ever the only thread left,
//the JVM should just die
this.setDaemon(true);
this.setName("ThreadScheduler");
this.start();
}
public void run()
{
while (true)
{
//wake up just long enough
//to reset the current NORM_PRIORITY thread
//to the next one in the queue,
//then go right back to sleep
try { this.sleep(timeSlice); }
catch (InterruptedException ignored) {}
}
}
}
Exceptions
Design Style Crimes
Once our programs become more complex we find many more complex ways to commit
style crime.
Instead of simple things like poor or inconsistent indentation
we now have to worry about issues that can affect that whole effort.
For example, should a Ball
have a Circle
or should it be a Circle
?
It seems so much easier to extend Ball
from Circle
rather than to let each Ball
have a Circle
as one of its variables, but that way leads to style crime.
Simply saving a little typing now
would lead us into all kinds of problems in future.
Another design problem: when two or more objects have to work together, who should have which role? For example, the stage has a ball and a window. The ball has to appear in the window or we can't play the game. Should the ball control the stage, should the stage control the ball, or should the window control the ball and the stage? If no one controls another, how is action mediated?
A third issue: since only the stage creates the clock,
why not make the type of the clock's creator World
(or
Frame
) instead of ClockObserver
?
That way we could get rid of ClockObserver
as well.
We could save lots of typing!
But it's a bad style crime to do so.
Keeping the exportable type as
ClockObserver
means that the same clock code can be used with
any class, once it implements
ClockObserver
, which means it simply has a tick()
method.
A fourth issue: since only the ball is being controlled by the clock, why not fold the clock into the stage entirely and forget about having to create another class and an interface to boot? that would also save us some typing but would clutter up the responsibilities of the various objects. The point is that it isn't the window's business how (or if) the ball's position gets updated.
The whole stupid paint()
and Graphics
issue.
Involving the Audience
So far our game is fairly interesting, but it becomes much more so when we
can play it.
So now let's let the game pay attention to the user.
First, we'll give the user the ability to control the paddle.
Detecting Events
import java.awt.Point;
import java.awt.Graphics;
import java.awt.Color;
public class Eyes
{
/*
Define objects that paint a pair of cartoon eyes
at a fixed position but with moveable pupils.
constructors:
-------------
Eyes()
Eyes(midpoint)
Eyes(midpoint, eyeradius, pupilradius)
Eyes(midpoint, eyeradius, pupilradius, eyecolor, pupilcolor)
Eyes(midpoint, eyeradius, pupilradius, eyecolor, pupilcolor,
eyeseparation)
Public Methods:
---------------
track(graphics, point) -- track point by moving my pupils
*/
protected int eyeRadius = 30;
protected int pupilRadius = 10;
protected Color eyeColor = Color.white;
protected Color pupilColor = Color.black;
protected int eyeSeparation = 3;
protected Point leftEyeCenter, rightEyeCenter;
protected Point leftPupilCenter, rightPupilCenter;
public Eyes()
{
/*
Create a pair of eyes
with the default eyeRadius, pupilRadius,
eyeColor, pupilColor, eyeSeparation,
leftEyeCenter, and rightEyeCenter.
*/
Point eyeMidpoint =
new Point(2 * eyeRadius + eyeSeparation, eyeRadius);
setupEyes(eyeMidpoint, eyeRadius, eyeSeparation);
}
public Eyes(Point eyeMidpoint)
{
/*
Create a pair of eyes
with the default eyeRadius, pupilRadius,
eyeColor, pupilColor, and eyeSeparation.
*/
setupEyes(eyeMidpoint, eyeRadius, eyeSeparation);
}
public Eyes(Point eyeMidpoint, int eyeRadius, int pupilRadius)
{
/*
Create a pair of eyes
with the default eyeColor, pupilColor,
and eyeSeparation.
*/
this.eyeRadius = eyeRadius;
this.pupilRadius = pupilRadius;
setupEyes(eyeMidpoint, eyeRadius, eyeSeparation);
}
public Eyes(Point eyeMidpoint, int eyeRadius, int pupilRadius,
Color eyeColor, Color pupilColor)
{
/*
Create a pair of eyes with the default eyeSeparation.
*/
this.eyeColor = eyeColor;
this.pupilColor = pupilColor;
this.eyeRadius = eyeRadius;
this.pupilRadius = pupilRadius;
setupEyes(eyeMidpoint, eyeRadius, eyeSeparation);
}
public Eyes(Point eyeMidpoint, int eyeSeparation,
int eyeRadius, int pupilRadius,
Color eyeColor, Color pupilColor)
{
/*
Create a pair of eyes.
*/
this.eyeColor = eyeColor;
this.pupilColor = pupilColor;
this.eyeRadius = eyeRadius;
this.pupilRadius = pupilRadius;
this.eyeSeparation = eyeSeparation;
setupEyes(eyeMidpoint, eyeRadius, eyeSeparation);
}
public void track(Graphics graphicsContext, Point point)
{
/*
Draw my eyes paying attention to point.
This method assumes eyeColor, pupilColor,
eyeRadius, pupilRadius, leftEyeCenter,
RightEyeCenter, leftPupilCenter, and RightPupilCenter
are all global.
*/
//compute my new pupil centers
leftPupilCenter =
computeGaze(point, leftEyeCenter, eyeRadius,
pupilRadius);
rightPupilCenter =
computeGaze(point, rightEyeCenter, eyeRadius,
pupilRadius);
//draw my sclera
drawFilledCircle(graphicsContext, eyeColor,
leftEyeCenter, eyeRadius);
drawFilledCircle(graphicsContext, eyeColor,
rightEyeCenter, eyeRadius);
//draw my pupils
drawFilledCircle(graphicsContext, pupilColor,
leftPupilCenter, pupilRadius);
drawFilledCircle(graphicsContext, pupilColor,
rightPupilCenter, pupilRadius);
}
protected void drawFilledCircle(Graphics graphicsContext,
Color color, Point center, int radius)
{
/*
Draw a filled circle centered at center with radius radius.
*/
graphicsContext.setColor(color);
graphicsContext.fillOval(center.x - radius,
center.y - radius, 2 * radius, 2 * radius);
}
protected Point computeGaze(Point point, Point eyeCenter,
int eyeRadius, int pupilRadius)
{
/*
Compute one of my pupil's locations
to track the given point.
*/
//compute the distance between the point
//and the eye's center
int xDistance = point.x - eyeCenter.x;
int yDistance = point.y - eyeCenter.y;
double distance =
Math.sqrt(xDistance * xDistance + yDistance * yDistance);
//compute the offsets to add to the eye's center
//to find the new pupil's center
int xOffset =
(int) (xDistance * (eyeRadius - pupilRadius) / distance);
int yOffset =
(int) (yDistance * (eyeRadius - pupilRadius) / distance);
Point newPupilCenter =
new Point(eyeCenter.x + xOffset, eyeCenter.y + yOffset);
return newPupilCenter;
}
private final void setupEyes(Point eyeMidpoint, int eyeRadius,
int eyeSeparation)
{
/*
Setup eye positions.
*/
leftEyeCenter =
new Point(eyeMidpoint.x - eyeSeparation - eyeRadius,
eyeMidpoint.y);
rightEyeCenter =
new Point(eyeMidpoint.x + eyeSeparation + eyeRadius,
eyeMidpoint.y);
}
}
import Eyes;
import java.applet.Applet;
import java.awt.Image;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Color;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseEvent;
import java.util.Vector;
import java.util.Enumeration;
public class EyesApplet extends Applet
implements MouseMotionListener
{
/*
Display a bunch of eyes that move their pupils
to track the mouse.
*/
//the eyes
Vector eyes;
//window dimensions and background color,
//and their defaults
int windowWidth = 500;
int windowHeight = 500;
Color backgroundColor = Color.gray;
//an offscreen bitmap and its graphics context
//(for double-buffereing)
Image offscreenImage;
Graphics offscreenGraphics;
public void init()
{
//setup my window
this.setBackground(backgroundColor);
this.setSize(windowWidth, windowHeight);
//set myself up to pay attention to the mouse
addMouseMotionListener(this);
//setup the offscreen bitmap
offscreenImage =
this.createImage(windowWidth, windowHeight);
offscreenGraphics = offscreenImage.getGraphics();
//setup the eyes
eyes = new Vector();
eyes.addElement(new Eyes());
eyes.addElement(new Eyes(new Point(100, 130)));
eyes.addElement(new Eyes(new Point(400, 130)));
eyes.addElement(new Eyes(new Point(100, 230), 10, 5));
eyes.addElement(new Eyes(new Point(400, 230), 10, 5));
eyes.addElement(new Eyes(new Point(100, 330), 10, 5,
Color.blue, Color.red));
eyes.addElement(new Eyes(new Point(400, 330), 10, 5,
Color.blue, Color.red));
eyes.addElement(new Eyes(new Point(100, 430), 1, 10,
5, Color.pink, Color.blue));
eyes.addElement(new Eyes(new Point(400, 430), 1, 10,
5, Color.pink, Color.blue));
//begin by staring at the lower-righthand corner
//of the window
moveEyes(new Point(windowWidth, windowHeight));
}
public void update(Graphics graphicsContext)
{
/*
Override the window blanking in Applet.update()
to decrease flicker.
*/
paint(graphicsContext);
}
public void paint(Graphics graphicsContext)
{
/*
Draw the offscreen bitmap to the window.
*/
graphicsContext.drawImage(offscreenImage, 0, 0, this);
}
public void mouseMoved(MouseEvent event)
{
/*
The mouse has moved; move the eyes to track it.
*/
moveEyes(event.getPoint());
repaint();
}
public void mouseDragged(MouseEvent event)
{
/*
Implement this method to complete
the implementation of the MouseMotionListener interface.
*/
}
private void moveEyes(Point point)
{
/*
Move the eyes to track the given point.
*/
//blank the offscreen image
//(don't really need to do all of this...)
offscreenGraphics.setColor(backgroundColor);
offscreenGraphics.fillRect(0, 0, windowWidth, windowHeight);
//paint the new eyes into the offscreen image
Enumeration eyelist = eyes.elements();
while (eyelist.hasMoreElements())
((Eyes) eyelist.nextElement()).track(
offscreenGraphics, point);
}
}
<html>
<title> put up a pair of eyes to follow the mouse </title>
<body>
<applet code = EyesApplet.class width = 300 height = 300>
</applet>
</body>
</html>
</div>