let the games begin
last | | contents | | next


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. First though let's learn about how Java does graphics and detects events with something simpler---an applet to track mouse movements.


 
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>



 
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 Clocks.
 
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 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.
last | | contents | | next