| Animation by Example: The Model-View-Controller
Design Pattern by David Matuszek |
Here is an example Applet that provides a much better foundation for creating your own animations.
I tried very hard to separate out three kinds of behavior:
init() that make the Model and Controller available
to the View:
view.model = model; view.controller = this;
A clean separation like this makes it a lot easier to keep a large program under control.
import java.applet.*; import java.awt.*; import java.awt.event.*; /** * This class defines the applet, and sets up the Model and View objects. * If we had user controls (such as Start and Stop buttons, or a speed * control), this is where they would be defined and handled.<p> * This example is more complex than it needs to be, because it enforces * a strict separation between the model (the part that does the actual * computation), the display, and the controls in the user interface (of * which there are none). This is called the MVC (Model-View-Controller) * pattern, and while it may be overkill for this example, MVC provides an * excellent basis for more complex animations. */ public class Controller extends Applet { // Create the model (the ball) and the view (the animation) Thread animation; Model model = new Model(); View view = new View(); /** * Lays out the applet components, starts a new thread for the * view to do its animation, and gives the model, view, and * controller access to one another. */ public void init() { // Lay out components setLayout(new BorderLayout()); this.add(BorderLayout.CENTER, view); // Give the view its own thread in which to do the animation animation = new Thread(view); animation.start(); // Get applet parameters and put them where they belong view.delay = Integer.valueOf(getParameter("delay")).intValue(); // Tell the view about the model and the controller view.model = model; view.controller = this; } /** * Finds the size of the canvas (this can't be done in init() * because the canvas isn't ready yet) and gives the animation * permission to run. */ public void start() { model.xLimit = view.getSize().width - model.BALL_SIZE; model.yLimit = view.getSize().height - model.BALL_SIZE; view.alive = true; view.okToRun = true; } /** * Withdraws permission for the animation to run. */ public void stop() { view.okToRun = false; } /** * Tells the animation thread to stop running. */ public void destroy() { view.alive = false; } } // ------------------------------------------------------------------------------------- /** * This class models the action of a bouncing ball. It is important to note * that this class is <i>independent</i> of the GUI. For this animation, the * Model does need to know the size of the box that the ball is bouncing * around in, and this is set from outside. Similarly, it provides a * makeOneStep() method so that the model can be controlled from outside the * class; but neither of these requires the model to know anything at all * about the rest of the program */ class Model { final int BALL_SIZE = 20; int xPosition = 0; int yPosition = 0; int xLimit, yLimit; int xDelta = 4; int yDelta = 3; /** * Moves the ball one step, by adding xDelta and yDelta to its xPosition * and yPosition, respectively, and checking for bounces off the "walls". */ void makeOneStep() { xPosition += xDelta; if(xPosition < 0 || xPosition >= xLimit) { xDelta = -xDelta; xPosition += xDelta; } yPosition += yDelta; if(yPosition < 0 || yPosition >= yLimit) { yDelta = -yDelta; yPosition += yDelta; } } } // ------------------------------------------------------------------------------------- /** * This class runs in a separate (animation) thread and controls all the * painting. */ class View extends Canvas implements Runnable { Controller controller; Model model; Dimension size = getSize(); int oldX, oldY; int delay = 1000; int stepNumber = 0; boolean alive, okToRun; /** * Sets up an (almost) infinite loop to do the animation, and every * delay milliseconds asks the model to update itself, then draws the * result. */ public void run() { // "alive" will be true until the Applet is destroyed while(alive) { // "okToRun" will be true if the Applet is started and not stopped if(okToRun) { model.makeOneStep(); repaint(); // Note: uses Graphics for this Canvas, not for the Applet } // Control the speed of execution try { Thread.sleep(delay); } catch(InterruptedException e) {} } } /** * The default update(Graphics g) method erases the canvas before calling * paint, and this results in flicker. We override update to avoid this * flicker; but that means that our paint(Graphics g) method must itself * erase the bit of canvas that the ball was just in, or it will leave * a trail. */ public void update(Graphics g) { paint(g); } /** * Repaints the canvas based on information that it gets from the model. */ public void paint(Graphics g) { // Erase previous ball (since update() no longer does this) g.setColor(Color.white); g.fillRect(oldX, oldY, model.BALL_SIZE, model.BALL_SIZE); // Draw new ball and remember its position g.setColor(Color.red); g.fillOval(model.xPosition, model.yPosition, model.BALL_SIZE, model.BALL_SIZE); oldX = model.xPosition; oldY = model.yPosition; // Display steps and position in the status line; // this is a good place to put debugging information. if(++stepNumber % 10 == 0) controller.showStatus("Step " + stepNumber + ", x = " + model.xPosition + ", y = " + model.yPosition); } }
Previous