Hide Your Data
Establish Firm Contracts
Protect Your Constants
Control Your Own State
Delegate Responsibilities
Use Generic Interfaces
Delegate Control
patterns: Singleton, Observer, Factory, Decorator, Adapter
animation applets need at least two things: double-buffering and a thread. over and over again i'd write the same bits of code and only vary the animation code. eventually even i realized that this was stupid. what i needed was an observer.
the observer would be the animation code and it would observe a clock. the clock's sole purpose would be to regularly execute the tick() method of the animation code.
further, i realized that i could subtract the double-buffering out too. that went into a generic AnimationShell applet that my animation applets now subclass.
consequently, i can write a complex animation applet in less than a third the time it used to take me---and i can't make mistakes in the double-buffering or thread portions since they're already done and debugged.
what was once 1 class now becomes 4 classes (the interface, the
two helper classes, plus the guts of the animation code (not included below
since it varies from application to application).
but in exchange i've gained
(1) robustness (this code is much easier to modify arbitrarily)
(2) cleanliness (there is a clean separation of responsibilities amongst all
the classes, each one has its job to do and ONLY that job) and
(3) understandability (it should be very easy for anyone to figure out what i'm
doing once they know about the observer pattern.
contents of file ClockObserver.java:
interface ClockObserver
{
/*
ClockObservers must have a tick() method that the clock they're
observing will ask them to execute on each clock tick.
*/
public void tick();
}
contents of file Clock.java:
import java.lang.Runnable;
import java.lang.Thread;
import java.util.Vector;
import java.util.Enumeration;
class Clock
implements Runnable
{
/*
Define an object that regularly executes the tick() method of all
its observers.
Objects become observers by executing this clock's addClockObserver()
method or by passing themselves to this clock on its creation.
Public Constructors & Methods:
------------------------------
Clock()
Clock(ticksPerSecond)
Clock(ticksPerSecond, observer)
addClockObserver(observer)
deleteClockObserver(observer)
start()
stop()
run()
timeSinceLastTick()
*/
//my thread, observer list, clock rate, and temporary variables
protected Thread thread;
protected Vector observers = new Vector();
protected int ticksPerSecond = 25;
protected long sleepTime = 40;
protected long startTickTime= 0, currentTickTime= 0, lastTickTime= 0;
protected long tickCount = 0;
///////////////////////////////////////////////////////////
public Clock()
{
/*
Create a clock with a default speed and no observers.
*/
thread = new Thread(this);
}
///////////////////////////////////////////////////////////
public Clock(int ticksPerSecond)
{
/*
Create a clock with a given speed and no observers.
*/
thread = new Thread(this);
this.ticksPerSecond = ticksPerSecond;
//convert to milliseconds
sleepTime = (long) (1000 / ticksPerSecond);
}
///////////////////////////////////////////////////////////
public Clock(int ticksPerSecond, ClockObserver observer)
{
/*
Create a clock with a given speed and a given observer
and start it.
*/
thread = new Thread(this);
this.ticksPerSecond = ticksPerSecond;
//convert to milliseconds
sleepTime = (long) (1000 / ticksPerSecond);
addClockObserver(observer);
this.start();
}
///////////////////////////////////////////////////////////
public synchronized void addClockObserver(ClockObserver observer)
{
observers.addElement(observer);
}
///////////////////////////////////////////////////////////
public synchronized void deleteClockObserver(ClockObserver observer)
{
observers.removeElement(observer);
}
///////////////////////////////////////////////////////////
public synchronized void die(ClockObserver observer)
{
/*
Die if requested to by my last observer.
This would leave the observer (and any careless past observers)
with a null pointer. how to fix that without requiring them to remember
to null out their references to me? need some way to make myself private
while still allowing arbitrary objects to observe me. I need a
ClockManager.
*/
if ((observers.size() == 1) && observers.elementAt(0).equals(observer))
{
this.stop();
observers = null;
try {this.finalize();}
catch (Throwable ignored) {} //just die
}
}
///////////////////////////////////////////////////////////
public void start()
{
startTickTime = System.currentTimeMillis();
lastTickTime = startTickTime;
currentTickTime = startTickTime;
if (thread == null)
thread = new Thread(this);
thread.start();
}
///////////////////////////////////////////////////////////
public void stop()
{
thread = null;
}
///////////////////////////////////////////////////////////
public void run()
{
while (thread != null)
{
currentTickTime = System.currentTimeMillis();
//execute all my observers' tick() methods
Enumeration watchers = observers.elements();
while (watchers.hasMoreElements())
((ClockObserver) watchers.nextElement()).tick();
tickCount++;
long elapsedTime = System.currentTimeMillis() - currentTickTime;
lastTickTime = currentTickTime;
//the time to sleep depends on how much time it took to execute all
//the tick() methods, so sleep only long enough to keep the beat
if (elapsedTime < sleepTime)
{
try {Thread.sleep(sleepTime - elapsedTime);}
catch (InterruptedException exception)
{
System.out.println("Clock: something bad happened.");
exception.printStackTrace();
throw new RuntimeException(exception.toString());
}
}
}
}
///////////////////////////////////////////////////////////
public long timeSinceLastTick()
{
return currentTickTime - lastTickTime;
}
///////////////////////////////////////////////////////////
protected long getTickCount()
{
return tickCount;
}
}
contents of file AnimationShell.java:
import ClockObserver;
import Clock;
import java.applet.Applet;
import java.awt.Image;
import java.awt.Graphics;
import java.awt.Color;
public class AnimationShell extends Applet
implements ClockObserver
{
/*
This applet shell can be subclassed to produce animation applets.
It uses an offscreen image to double buffer the animation and so
avoid image flicker and it observes a clock that executes its tick()
method to control when the animation is redrawn. It also lets subclasses
control how the animation is drawn (whether it's to be paused, resumed,
restarted, or killed).
Public Methods:
---------------
init()
start()
stop()
run()
update(graphicsContext)
paint(graphicsContext)
tick()
pauseAnimation()
resumeAnimation()
restartAnimation()
endAnimation()
*/
//my offscreen bitmap and its graphics context
protected Image offscreenImage;
protected Graphics offscreenGraphics;
//my clock and its speed
Clock clock;
protected int CLOCK_TICKS_PER_SECOND = 25;
//my window's background color and dimensions
protected Color backgroundColor = Color.black;
protected int windowWidth, windowHeight;
//the animation controller
protected boolean paused;
///////////////////////////////////////////////////////////
public void init()
{
/*
Setup my onscreen and offscreen bitmaps.
*/
//fetch my window's dimensions and blank the screen
windowWidth = this.getSize().width;
windowHeight = this.getSize().height;
this.setBackground(backgroundColor);
//setup my offscreen bitmap
offscreenImage = this.createImage(windowWidth, windowHeight);
offscreenGraphics = offscreenImage.getGraphics();
offscreenGraphics.setColor(backgroundColor);
offscreenGraphics.fillRect(0, 0, windowWidth, windowHeight);
//start my clock
clock = new Clock(CLOCK_TICKS_PER_SECOND, this);
//start the animation
this.start();
}
///////////////////////////////////////////////////////////
public void start()
{
/*
Start the animation.
*/
paused = false;
}
///////////////////////////////////////////////////////////
public void stop()
{
/*
Stop the animation.
*/
paused = true;
}
///////////////////////////////////////////////////////////
public void tick()
{
/*
This method is regularly executed by my clock.
*/
if (! paused)
run();
}
///////////////////////////////////////////////////////////
public void run()
{
/*
Generate the next animation frame and paint it.
*/
updateAnimation();
this.repaint();
}
///////////////////////////////////////////////////////////
public void pauseAnimation()
{
this.stop();
}
///////////////////////////////////////////////////////////
public void resumeAnimation()
{
this.start();
}
///////////////////////////////////////////////////////////
public void restartAnimation()
{
/*
Start the animation over from the beginning.
*/
this.stop();
this.init();
}
///////////////////////////////////////////////////////////
public void endAnimation()
{
/*
Kill the animation dead.
*/
this.stop();
offscreenImage = null;
offscreenGraphics = null;
clock.die(this);
clock = null;
}
///////////////////////////////////////////////////////////
public void update(Graphics graphicsContext)
{
/*
Overridde the default Applet.update() to decrease flicker.
*/
paint(graphicsContext);
}
///////////////////////////////////////////////////////////
public void paint(Graphics graphicsContext)
{
/*
Paint my offscreen bitmap to my window.
*/
//blank my offscreen bitmap
offscreenGraphics.setColor(backgroundColor);
offscreenGraphics.fillRect(0, 0, windowWidth, windowHeight);
//paint the current animation frame into it
paintAnimationFrame(offscreenGraphics);
//draw it to the screen
graphicsContext.drawImage(offscreenImage, 0, 0, this);
}
///////////////////////////////////////////////////////////
protected void updateAnimation()
{
/*
Update the animation that i will later paint to my offscreen bitmap.
This method does nothing here; it's to be overridden by subclasses.
*/
}
///////////////////////////////////////////////////////////
protected void paintAnimationFrame(Graphics graphicsContext)
{
/*
Paint a frame of the animation into graphicsContext.
This method does nothing here; it's to be overridden by subclasses.
*/
}
}