An Actor Class

Java Gaming for the Masses

An Actor Class


There are three demos in this set of notes.

The first example can be found at the following URL:

http://www.cs.indiana.edu/
         classes/
           a348/
             t540/
               spr2002/
                 lectures/
                   code/
                     Two/
                       BufferedGraphicsTest.html
To get directly to my applet please click here (IE on Win32 only.)

A better bet is to simply run your

appletviewer
on
http://www.cs.indiana.edu/
       classes/a348/t540/spr2002/lectures/code/Two/BufferedGraphicsTest.html
(You can also retrive everything from /l/www/classes/a348/t540/spr2002/lectures/code/Two).

You've seen the first example already (last week) but the second example is slightly different. Its purpose is to test a utility class that's used relatively frequently. Here's the URL of the example

http://www.cs.indiana.edu/
         classes/
           a348/
             t540/
               spr2002/
                 lectures/
                   code/
                     Two/
                       VectorTest.html
To get directly to my applet please click here (IE on Win32 only.)

A better bet is to simply run your

appletviewer
on
http://www.cs.indiana.edu/
         classes/a348/t540/spr2002/lectures/code/Two/VectorTest.html
Finally, here's an example of the use of the most important part of this set of notes.

http://www.cs.indiana.edu/
         classes/
           a348/
             t540/
               spr2002/
                 lectures/
                   code/
                     Two/
                       ActorTest.html
To get directly to my applet please click here (IE on Win32 only.)

A better bet is to simply run your

appletviewer
on
http://www.cs.indiana.edu/
         classes/a348/t540/spr2002/lectures/code/Two/ActorTest.html
Now a summary of what's needed (in terms of files, listed below):

  1. Actor2D.java
  2. Moveable.java
  3. Vector2D.java
  4. VectorTest.java
  5. VectorTest.html
  6. ImageLoader.java
  7. ImageGroup.java
  8. ActorGroup2D.java
  9. AnimationStrip.java
  10. Animator.java
  11. Robot.java
  12. RobotGroup.java
  13. stile.gif
    robot.gif
    robot_dead.gif
    robot_north.gif
    robot_south.gif
    robot_west.gif
    robot_east.gif
  14. ActorTest.java
    (also contains RobotAdapter)
  15. ActorTest.html
  16. BufferedGraphics.java
  17. BufferedGraphicsTest.java
  18. BufferedGraphicsTest.html
  19. pipe.gif
  20. StaticActor.java
  21. StaticActorGroup.java

The purpose of this set of notes is to bring together all the files needed so you can build these examples on your own workstations, play with them, look at them, understand and modify them to suit your needs for your projects. We first need (and that's the central point here) an actor class.

No actors, no games.

import java.awt.*;
import java.awt.geom.*;
import java.util.*;

// contains general information for moving and rendering a 2-D game object
public abstract class Actor2D extends Object implements Moveable
{
    // some general states an actor might have
    public final int STATE_ALIVE = 1;
    public final int STATE_DYING = 2;
    public final int STATE_DEAD  = 4;
    
    // the state of this actor
    protected int state;
    
    // the actor group this actor belongs to
    protected ActorGroup2D group;
    
    // position, velocity, and rotation of the actor, 
    // along with cached transformation
    protected Vector2D pos;
    protected Vector2D vel;
    protected double   rotation;
    protected AffineTransform xform;
    
    protected final double TWO_PI = 2*Math.PI;
    
    // a bounding rectangle for things such as collision testing
    protected Rectangle2D bounds;
    
    // a list of actors this actor has collided with during one frame
    protected LinkedList collisionList;
    
    // width and height of this actor
    protected int frameWidth;
    protected int frameHeight;
    
    // reference to this actor's current animation strip
    protected AnimationStrip currAnimation;
    
    // number of frames to wait before animating the next frame,
    // plus a wait counter
    protected int animWait;
    protected int animCount;     
    
    // creates a new Actor2D object belonging to the given ActorGroup 
    public Actor2D(ActorGroup2D grp)
    {
	group = grp;
	
	bounds = new Rectangle2D.Double();
	collisionList = new LinkedList();
	
	state = 0;
	
	pos   = new Vector2D.Double();
	vel   = new Vector2D.Double();
	rotation = 0;
	xform = new AffineTransform();
	
	currAnimation = null;
	animWait  = 0;
	animCount = 0;
	
	frameWidth  = 0;
	frameHeight = 0;
    }
    
    // animates the actor every animWait frames
    public void animate()
    {
	if(currAnimation != null)
	    {
		if(++animCount >= animWait)
                    {
			currAnimation.getNextFrame();
			animCount = 0;
                    }     
	    }
    }
    
    // draws the actor using its native transformation
    public void paint(Graphics2D g2d)
    {
	if(currAnimation != null)
	    {
		g2d.drawImage(currAnimation.getCurrFrame(), 
			      xform, 
			      AnimationStrip.observer);  
	    }
    }
    
    // draws the actor at the sent x,y coordinates
    public void paint(Graphics2D g2d, double x, double y)
    {
	if(currAnimation != null)
	    {
		g2d.drawImage(currAnimation.getCurrFrame(), 
			      AffineTransform.getTranslateInstance(x, y),
			      AnimationStrip.observer
			      );               
	    }
    }
    
    // simple bounding-box determination of whether this actor 
    // has collided with the sent actor                   
    public boolean intersects(Actor2D other)
    {
	return bounds.intersects(other.getBounds());
    }
    
    // updates the bounding rectangle of this actor to meet its 
    // current x and y positions
    public void updateBounds()
    {
	// make sure we know the correct width and height of the actor
	if(frameWidth <= 0 && currAnimation != null)
	    {
		frameWidth = currAnimation.getFrameWidth();
	    }
	if(frameHeight <= 0 && currAnimation != null)
	    {
		frameHeight = currAnimation.getFrameHeight();
	    }
	
	bounds.setRect(pos.getX(), pos.getY(), frameWidth, frameHeight);
    }
    
    // makes sure that the actor's bounds have not exceeded the bounds 
    // specified by its actor group
    public void checkBounds()
    {
	if(group == null) return;
	
	if(bounds.getX() < group.MIN_X_POS)
	    {
		pos.setX(group.MIN_X_POS);
	    }
	
	else if(bounds.getX() + frameWidth > group.MAX_X_POS)
	    {
		pos.setX(group.MAX_X_POS - frameWidth);
	    }
	
	if(bounds.getY() < group.MIN_Y_POS)
	    {
		pos.setY(group.MIN_Y_POS);
	    }
	
	else if(bounds.getY() + frameHeight > group.MAX_Y_POS)
	    {
		pos.setY(group.MAX_Y_POS - frameHeight);
	    }
    }
    
    // returns a String representation of this actor       
    public String toString()
    {
	return super.toString();
    }
    
    // bitwise OR's the sent attribute state with the current attribute state
    public final void setState(int attr)
    {
	state |= attr;
    }
    
    // resets an attribute using the bitwise AND and NOT operators 
    public final void resetState(int attr)
    {
	state &= ~attr;
    }
    
    public final int getState()
    {
	return state;
    }
    
    public final void clearState()
    {
	state = 0;
    }
    
    // determines if the sent state attribute is contained in this 
    // actor's state attribute
    public final boolean hasState(int attr)
    {
	return ((state & attr) != 0);
    }
    
    // access methods for the velocity, position, and rotation of the actor
    
    public final void setX(double px)
    {
	pos.setX(px);
    }
    
    public final void setY(double py)
    {
	pos.setY(py);
    }
    
    public final double getX()
    {
	return pos.getX();
    }
    
    public final double getY()
    {
	return pos.getY();
    }
    
    public final void setPos(int x, int y)
    {
	pos.setX(x);
	pos.setY(y);
    }
    
    public final void setPos(double x, double y)
    {
	pos.setX(x);
	pos.setY(y);
    }
    
    public final void setPos(Vector2D v)
    {
	pos.setX(v.getX());
	pos.setY(v.getY());
    }
    
    public final Vector2D getPos()
    {
	return pos;
    }
    
    public final void setRot(double theta)
    {
	rotation = theta;
    }
    
    public final double getRot()
    {
	return rotation;
    }
    
    public final void rotate(double theta)
    {
	rotation += theta;
	
	while(rotation > TWO_PI)
	    {
		rotation -= TWO_PI;
	    }
	while(rotation < -TWO_PI)
	    {
		rotation += TWO_PI;
	    }
    }
    
    public final void setVel(int x, int y)
    {
	vel.setX(x);
	vel.setY(y);
    }
    
    public final void setVel(Vector2D v)
    {
	vel.setX(v.getX());
	vel.setY(v.getY());
    }
    
    public final Vector2D getVel()
    {
	return vel; 
    }
    
    public final void moveBy(double x, double y)
    {
	pos.translate(x, y);
    }
    
    public final void moveBy(int x, int y)
    {
	pos.translate(x, y);
    }
    
    public final void moveBy(Vector2D v)
    {
	pos.translate(v);
    }
    
    public final void accelerate(double ax, double ay)
    {
	vel.setX(vel.getX() + ax);
	vel.setY(vel.getY() + ay);
    }
    
    public int getWidth()
    {
	return frameWidth;
    }
    
    public int getHeight()
    {
	return frameHeight;
    }
    
    // methods inherited from the Moveable interface
    
    public Rectangle2D getBounds()
    {
	return bounds;
    }
    
    // determines if a Moveable object has collided with this object
    public boolean collidesWith(Moveable other)
    {
	return (bounds.contains(other.getBounds()) || 
		bounds.intersects(other.getBounds()));
    }
    
    // adds a collision object to this collision list
    public void addCollision(Moveable other)
    {
	if(collisionList == null)
	    {
		collisionList = new LinkedList(); 
		collisionList.add(other);
		return;
	    }
	
	if(! collisionList.contains(other))
	    {
		collisionList.add(other);
	    }
    }
    
    // stub method for processing collisions with those 
    // actors contained within the collisionsList
    // this method is left empty, but not abstract
    public void processCollisions()
    {
	
    }
    
    // updates the object's position and bounding box, animates it, 
    // then updates the transformation 
    public void update()
    {
	pos.translate(vel);
	
	updateBounds();
	checkBounds();
	
	animate();                    
	
	// subclasses which require the transformation to be 
	// centered about an anchor point other than the position 
	// will need to override this method
	if(rotation != 0)
	    {
		xform.setToIdentity();
		xform.translate(pos.getX()+frameWidth/2, 
				pos.getY()+frameHeight/2); 
		xform.rotate(rotation); 
		xform.translate(-frameWidth/2, -frameHeight/2); 
	    }               
	else
	    {
		xform.setToTranslation(pos.getX(), pos.getY()); 
	    }
    }
    
} // Actor2D 
You also need this interface.

import java.awt.geom.*;

public interface Moveable
{
    public Rectangle2D getBounds();
    public boolean collidesWith(Moveable other);
    public void addCollision(Moveable other);
    public void processCollisions();
    public void update();
    
} // Moveable
We cannot create actors just yet. Why? (For at least two reasons).

The following class provides support for the Actor2D class.

public abstract class Vector2D extends Object
{
    public static final Vector2D.Double ZERO_VECTOR 
	= new Vector2D.Double(0, 0);
    
    public static class Double extends Vector2D
    {
	// the x and y components of this Vector2D.Double object
	public double x;
	public double y;
	
	// creates a default Vector2D with a value of (0,0)
	public Double()
	{
	    this(0.0, 0.0);
	}
	
	// creates a Vector2D.Double object with the sent values
	public Double(double m, double n)
	{    setX(m);
	setY(n);
	}
	
	private Double(int m, int n)
	{    setX((double) m);
	setY((double) n);
	}
	
	// get/set access methods for the x and y components
	
	public final void setX(double n)
	{    x = n;
	}
	
	public final void setY(double n)
	{    y = n;
	}
	
	public final double getX()
	{    return x;
	}
	
	public final double getY()
	{    return y;
	}
	
	// adds this vector to the sent vector
	public Vector2D plus(Vector2D v)
	{
	    return new Double(getX() + v.getX(), getY() + v.getY());
	}
	
	// subtracts the sent vector from this vector
	public Vector2D minus(Vector2D v)
	{
	    return new Double(getX() - v.getX(), getY() - v.getY());
	}
	
    } // Double
    
    public static class Integer extends Vector2D
    {
	// the x and y components of this Vector2D.Integer object
	public int x;
	public int y;
	
	// creates a default Vector2D with a value of (0,0)
	public Integer()
	{
	    this(0, 0);
	}
	
	// creates a Vector2D.Integer object with the sent values
	public Integer(int m, int n)
	{    setX(m);
	setY(n);
	}
	
	private Integer(double m, double n)
	{    setX((int) m);
	setY((int) n);
	}
	
	// get/set access methods for the x and y components
	
	public final void setX(double n)
	{    x = (int) n;
	}
	
	public final void setY(double n)
	{    y = (int) n;
	}
	
	public final double getX()
	{    return (double) x;
	}
	
	public final double getY()
	{    return (double) y;
	}
	
	// adds this vector to the sent vector
	public Vector2D plus(Vector2D v)
	{
	    return new Integer(getX() + v.getX(), getY() + v.getY());
	}
	
	// subtracts the sent vector from this vector
	public Vector2D minus(Vector2D v)
	{
	    return new Integer(getX() - v.getX(), getY() - v.getY());
	}
	
    } // Integer
    
    protected Vector2D()  {  }          
    
    public abstract void setX(double n);
    
    public abstract void setY(double n);
    
    public abstract double getX();
    
    public abstract double getY();
    
    // adds this vector to the sent vector
    public abstract Vector2D plus(Vector2D v);
    
    // subtracts the sent vector from this vector
    public abstract Vector2D minus(Vector2D v);
    
    // determines if two vectors are equal
    public boolean equals(Vector2D other)
    {
	return (getX() == other.getX() && getY() == other.getY());
    }
    
    // normalizes this vector to unit length
    public void normalize()
    {
	double len = length();
	setX(getX() / len);
	setY(getY() / len);
    }
    
    public void scale(double k)
    {
	setX(k * getX());
	setY(k * getY());
    }
    
    // translates the Vector2D by the sent value        
    public void translate(double dx, double dy)
    {
	setX(getX() + dx);
	setY(getY() + dy);
    }
    
    // translates the Vector2D by the sent vector
    public void translate(Vector2D v)
    {
	setX(getX() + v.getX());
	setY(getY() + v.getY());
    }
    
    // calculates the dot (or inner) product of this and the sent vector       
    public double dot(Vector2D v)
    {
	return getX()*v.getX() + getY()*v.getY();               
    }
    
    // returns the length of this vector
    public double length()
    {
	return Math.sqrt(this.dot(this));
    }
    
    // returns the String representation of this Vector2D
    public String toString() 
    {
	return getClass().getName() + " [x=" + getX() + ",y=" + getY() + "]";
    } 
    
} // Vector2D
Then a simple test, to prove the concept.

import java.applet.*;
import java.awt.*;
import java.util.*; 

public class VectorTest extends Applet implements Runnable
{  
    // an array of vector positions         
    private Vector2D[] vects;
    
    // an array of velocity values for the above array
    private Vector2D[] vels;
    
    // colors used to draw lines
    private final Color[] COLORS = { 
	Color.blue,  Color.red,   Color.green, Color.darkGray, 
	Color.black, Color.orange, Color.pink, Color.cyan
    };
    
    // a thread for animation
    private Thread animation;
    
    // an offscreen rendering image
    private Image offscreen;
    
    public void init()
    {
	int len = COLORS.length;
	
	vects = new Vector2D[len];
	vels  = new Vector2D[len];
	
	Random r = new Random();
	
	for(int i = 0; i < len; i++)
	    {
		// create points that make up an circle
		vects[i] 
		    = new Vector2D.Double
			(50*(Math.cos(Math.toRadians(i*(360/len)))),  
			 50*(Math.sin(Math.toRadians(i*(360/len)))));
		
		// translate the point to the center of the screen
		vects[i].translate(getSize().width/2, getSize().height/2);
		
		vels[i] = new Vector2D.Integer(1 + r.nextInt()%5, 
					       1 + r.nextInt()%5);
	    }  
	
	offscreen = createImage(getSize().width, getSize().height);
	
	animation = new Thread(this);
    }
    
    public void start()
    {
	animation.start();
    }
    
    public void stop()
    {
	animation = null;
    }
    
    public void run()
    {
	Thread t = Thread.currentThread();
	while(t == animation)
	    {
		repaint();
		try
                    { 
			t.sleep(20); 
                    }
		catch(InterruptedException e) 
                    { 
                    }
	    }
    }
    
    public void update(Graphics g)
    {
	// save the width and height of the applet window
	double width  = (double) getSize().width;
	double height = (double) getSize().height;
	
	for(int i = 0; i < COLORS.length; i++)
	    {
		vects[i].translate(vels[i].getX(), vels[i].getY());
		
		if(vects[i].getX() > width)
                    {
			vects[i].setX(width);
			vels[i].setX(-vels[i].getX()); 
                    }
		else if(vects[i].getX() < 0)
                    {
			vects[i].setX(0);
			vels[i].setX(-vels[i].getX()); 
                    }
		
		if(vects[i].getY() > height)
                    {
			vects[i].setY(height);
			vels[i].setY(-vels[i].getY()); 
                    }
		else if(vects[i].getY() < 0) 
                    {
			vects[i].setY(0);
			vels[i].setY(-vels[i].getY());
                    }
	    }  
	
	if(offscreen == null || 
	   offscreen.getWidth(null)  != getSize().width || 
	   offscreen.getHeight(null) != getSize().height)
	    {
		offscreen = createImage(getSize().width, getSize().height);
	    }
	
	paint(g);
    }
    
    public void paint(Graphics g)
    {
	// cast the sent Graphics context to get a usable Graphics2D object
	Graphics2D g2d = (Graphics2D)offscreen.getGraphics();
	g2d.setPaint(Color.white);
	g2d.fillRect(0, 0, getSize().width, getSize().height);
	
	g2d.setStroke(new BasicStroke(3.0f)); 
	
	// connect all of the lines together
	Vector2D prev = vects[COLORS.length-1];
	for(int i = 0; i < COLORS.length; i++)
	    {
		g2d.setPaint(COLORS[i]);
		
		g2d.drawLine((int) prev.getX(), (int) prev.getY(),
			     (int) vects[i].getX(), (int) vects[i].getY());
		
		prev = vects[i];
	    }  
	
	g.drawImage(offscreen, 0, 0, this);
    }
    
} // VectorTest
And here's an HTML file in case you want to save some typing.

<html>

  <head>
    <title>VectorTest</title>
  </head>

  <body>
    <hr>
    <applet code=VectorTest.class width=300 height=300></applet>
    <hr>

  </body>
</html>
Now we start providing the missing classes and build an example.

Here's the file ImageLoader.java

import java.applet.*;
import java.awt.*;
import java.awt.image.*;
import java.util.*;

public class ImageLoader extends Object
{
    // an Applet to load and observe loading images
    protected Applet applet;
    
    // an Image, along with its width and height
    protected Image  image;
    protected int    imageWidth;
    protected int    imageHeight;
    
    // a buffer to render images to immediately after they are loaded
    protected static BufferedImage buffer 
	= new BufferedImage(200, 200, BufferedImage.TYPE_INT_RGB);
    
    public ImageLoader(
		       Applet a,         // creates and observes loading images
		       String filename,  // name of image to load on disk
		       boolean wait      // if true, add to a MediaTracker 
		                         //        object and wait to be loaded
		       )
    {
	applet = a;
	
	image = applet.getImage(applet.getDocumentBase(), filename);
	
	if(wait)
	    {
		// create a new MediaTracker object for this image
		MediaTracker mt = new MediaTracker(applet);
		
		// load the strip image
		mt.addImage(image, 0);
		try
                    {  
			// wait for our main image to load  
			mt.waitForID(0);
                    }
		catch(InterruptedException e) { /* do nothing */ }
	    }
	
	// get the width and height of the image
	imageWidth = image.getWidth(applet);
	imageHeight = image.getHeight(applet);
    }
    
    public int getImageWidth()
    {
	return imageWidth;
    }
    
    public int getImageHeight()
    {
	return imageHeight;
    }
    
    public Image getImage()
    {
	return image;
    }
    
    // extracts a cell from the image using an image filter 
    public Image extractCell(int x, int y, int width, int height)
    {
	// get the ImageProducer source of our main image 
	ImageProducer sourceProducer = image.getSource();
	
	Image cell = applet.createImage
	    (new FilteredImageSource(sourceProducer, 
				     new CropImageFilter
					 (x, y, width, height)));
	
	// draw the cell to the off-screen buffer 
	buffer.getGraphics().drawImage(cell, 0, 0, applet);
	
	return cell;
    }
    
    // extracts a cell from the image and scales 
    // it to the sent width and height 
    public Image extractCellScaled
	(int x, int y, int width, int height, int sw, int sh)
    {
	// get the ImageProducer source of our main image 
	ImageProducer sourceProducer = image.getSource();
	
	Image cell = applet.createImage
	    (new FilteredImageSource(sourceProducer, 
				     new CropImageFilter
					 (x, y, width, height)));
	
	// draw the cell to the off-screen buffer 
	buffer.getGraphics().drawImage(cell, 0, 0, applet);
	
	return cell.getScaledInstance(sw, sh, Image.SCALE_SMOOTH);
    }
    
} // ImageLoader 
Next, we provide the file ImageGroup.java

import java.applet.*;

// provides methods for creating and accessing AnimationStrip objects
public abstract class ImageGroup
{
    // an array of AnimationStrip objects that create 
    // our animation sequences as a whole
    protected AnimationStrip[] animations;
    
    // the width and height of an individual image frame
    protected int frameWidth; 
    protected int frameHeight;
    
    // creates a new ImageGroup object
    protected ImageGroup()
    {
	animations = null;
    }
    
    // initializes the ImageGroup using the sent Applet reference object
    public abstract void init(Applet a);
    
    public final int getFrameWidth()
    {
	return frameWidth;
    }
    
    public final int getFrameHeight()
    {
	return frameHeight;
    }
    
    // accesses the AnimationStrip at the given index          
    public final AnimationStrip getAnimationStrip(int index)
    {
	if(animations != null)
	    {
		try
                    {
			return animations[index];     
                    }
		catch(ArrayIndexOutOfBoundsException e)
                    {
			// send error to debugger or standard output...
                    }
	    }
	
	return null;
    }
    
} // ImageGroup
Here's file ActorGroup2D.java

import java.applet.*;

// defines related attributes common to Actor2D objects
public abstract class ActorGroup2D extends ImageGroup
{
    // default min/max values for int's and float's
    
    protected static final int MAX_INT_UNBOUND = Integer.MAX_VALUE;
    protected static final int MIN_INT_UNBOUND = Integer.MIN_VALUE;
    
    protected static final double MAX_DBL_UNBOUND = Double.MAX_VALUE;
    protected static final double MIN_DBL_UNBOUND = Double.MIN_VALUE;
    
    // the maximum and minimum position and velocity an Actor2D can have 
    // overriding classes can change these values at construction time or 
    // within the init method
    
    public int MAX_X_POS = MAX_INT_UNBOUND;
    public int MAX_Y_POS = MAX_INT_UNBOUND;     
    
    public int MIN_X_POS = MIN_INT_UNBOUND;
    public int MIN_Y_POS = MIN_INT_UNBOUND;     
    
    public int MAX_X_VEL = MAX_INT_UNBOUND;
    public int MAX_Y_VEL = MAX_INT_UNBOUND;     
    
    public int MIN_X_VEL = MIN_INT_UNBOUND;
    public int MIN_Y_VEL = MIN_INT_UNBOUND;     
    
    // constructs a new ActorGroup2D object
    protected ActorGroup2D() 
    {
	super();
    }
    
    // initializes shared Actor2D attributes
    public abstract void init(Applet a);
    
} // ActorGroup2D 
Here's AnimationStrip.java

import java.awt.*; 
import java.awt.image.*;
import java.util.*;

// defines a dynamic list of Image frames that 
// can be animated using a given Animator object    
public class AnimationStrip extends Object 
{
    // observes drawing for external objects
    public static ImageObserver observer;
    
    // a linked list of Image frames, along with the size of the list
    protected LinkedList frames;
    protected int        numFrames;
    
    // the Animator responsible for animating frames
    protected Animator animator;
    
    // creates a new AnimationStrip object
    public AnimationStrip()
    {
	frames    = null;
	numFrames = 0;
	animator  = null;
    }
    
    public final void setAnimator(Animator anim)
    {
	animator = anim;
	animator.setFrames(frames);
    }
    
    // adds an Image frame to the list
    public void addFrame(Image i)
    {
	if(frames == null)
	    {
		frames = new LinkedList();
		numFrames = 0;
	    }
	
	frames.add(i);
	numFrames++;
    }
    
    // returns the Animator's current frame
    public Image getCurrFrame()
    {
	if(frames != null)
	    {
		return animator.getCurrFrame();
	    }
	return null;
    }
    
    // allows the Animator to generate the next frame of animation
    public void animate()
    {
	if(animator != null)
	    {
		animator.nextFrame();
	    }
    }
    
    // returns the Animator's next frame of animation
    public Image getNextFrame()
    {
	if(animator != null)
	    {
		animator.nextFrame();
		return animator.getCurrFrame();
	    }
	
	return null;
    }
    
    // returns the first frame of animation
    public Image getFirstFrame()
    {
	if(frames != null)
	    {
		return (Image)frames.getFirst();
	    }
	
	return null;               
    }    
    
    // returns the last frame of animation 
    public Image getLastFrame()
    {
	if(frames != null)
	    {
		return (Image)frames.getLast();
	    }
	
	return null;               
    }    
    
    // resets the Animator's internal animation sequence
    public void reset()
    {
	if(animator != null)
	    {
		animator.reset();
	    }
    }
    
    // returns an animation frame's width
    public int getFrameWidth()
    {
	if(frames != null && !frames.isEmpty())
	    {
		return getFirstFrame().getWidth(observer);
	    }
	return 0;
    }
    
    // returns an animation frame's height
    public int getFrameHeight()
    {
	if(frames != null && !frames.isEmpty())
	    {
		return getFirstFrame().getHeight(observer);
	    }
	return 0;
    }
    
} // AnimationStrip 
Here's Animator.java

import java.awt.*;
import java.util.*;

// defines a custom way of animating a list of Image frames
public abstract class Animator extends Object
{
    // references a linked list of Image frames
    protected LinkedList frames;
    
    // the current index of animation
    protected int currIndex;
    
    // creates a new Animator object with a null-referenced set of frames
    protected Animator() 
    {
	frames = null;
	currIndex = 0; 
    }
    
    public final void setFrames(LinkedList list)
    {
	frames = list;
    }
    
    // resets this animation
    public void reset()
    {
	currIndex = 0;
    }
    
    // returns the current frame of animation
    public Image getCurrFrame()
    {
	if(frames != null)
	    {
		return (Image)frames.get(currIndex);
	    }
	return null;
    }
    
    // this method defines how frames are animated
    public abstract void nextFrame();
    
    // animates frames based on a sent array of indices
    public static class Indexed extends Animator
    {
	protected int[] indices;
	protected int   arrayIndex;
	
	public Indexed()
	{
	    super();
	    arrayIndex = 0;
	}
	
	public Indexed(int[] idx)
	{
	    indices = new int[idx.length];
	    
	    System.arraycopy(idx, 0, indices, 0, idx.length);
	    arrayIndex = 0;
	}
	
	public void nextFrame()
	{
	    if(frames != null)
		{
		    // increments the index counter
		    if(++arrayIndex >= indices.length)
			{
			    arrayIndex = 0;
			}
		    currIndex = indices[arrayIndex];
		}
	}                
	
    } // Animator.Indexed
    
    // iterates through the animation frames, looping 
    // back to the start when necessary
    public static class Looped extends Animator
    {
	public Looped()
	{
	    super();
	}
	
	public void nextFrame()
	{
	    if(frames != null)
		{
		    if(++currIndex >= frames.size())
			{
			    reset();
			}
		}
	}                
	
    } // Animator.Looped
    
    // iterates through the animation frames, but 
    // stops once it reaches the last frame                    
    public static class OneShot extends Animator
    {
	public OneShot()
	{
	    super();
	}
	
	public void nextFrame()
	{
	    if(frames != null)
		{
		    if(++currIndex >= frames.size());
		    {
			currIndex = frames.size()-1;
		    }
		}
	}                
	
    } // Animator.OneShot
    
    // generates a random animation frame during each call to nextFrame
    public static class Random extends Animator
    {
	private java.util.Random random;
	
	public Random()
	{
	    super();
	    
	    random = new java.util.Random();
	}
	
	public void nextFrame()
	{
	    if(frames != null)
		{
		    currIndex = random.nextInt() % frames.size();
		}
	}                
	
    } // Animator.Random
    
    // represents an animation containing only one frame-- 
    // this class saves time since it does not processing         
    public static class Single extends Animator
    {
	public Single()
	{
	    super();
	}
	
	public void nextFrame()
	{
	    // do nothing...
	}                
	
    } // Animator.Single
    
} // Animator
Here's Robot.java

import java.awt.*;

// creates a simple robot that can move in the ordinal directions as well as 
// fire its weapon 
public class Robot extends Actor2D 
{
    // index correlating to the current animation
    protected int currAnimIndex;
    
    // saves the previous animation for shooting animations
    protected int prevAnimIndex;
    
    // the Robot state SHOOTING
    public final static int SHOOTING = 8;
    
    // used to tell the robot in which direction to move 
    public final static int DIR_NORTH = 0;
    public final static int DIR_SOUTH = 1;
    public final static int DIR_EAST  = 2;
    public final static int DIR_WEST  = 3;
    
    // creates a new Robot with the given actor group
    public Robot(ActorGroup2D grp)
    {
	super(grp);
	
	vel.setX(5);
	vel.setY(5);
	
	animWait = 3;
	
	currAnimIndex = 0;
	prevAnimIndex = 0;
	
	currAnimation = group.getAnimationStrip(RobotGroup.WALKING_SOUTH);
	
	frameWidth  = currAnimation.getFrameWidth();
	frameHeight = currAnimation.getFrameHeight();
    }
    
    // updates the position of the robot, and animates it if it is shooting
    public void update()
    {
	if(hasState(SHOOTING))
	    {
		animate();
	    }
	
	xform.setToTranslation(pos.getX(), pos.getY()); 
	
	updateBounds();
	checkBounds();
    }
    
    // flags the robot to shoot until stopShooting is called   
    public void startShooting()
    {
	prevAnimIndex = currAnimIndex;
	if((currAnimIndex % 2) == 0) 
	    {
		currAnimIndex++;
	    }
	currAnimation = group.getAnimationStrip(currAnimIndex);
	currAnimation.reset(); 
	setState(SHOOTING);
    }
    
    // stops shooting and restores the previous animation 
    public void stopShooting()
    {
	currAnimIndex = prevAnimIndex;
	currAnimation = group.getAnimationStrip(currAnimIndex);
	currAnimation.reset(); 
	resetState(SHOOTING);
    }
    
    // moves and animates the robot based on the sent ordinal direction
    public void move(int dir)
    {
	// prevent further shooting
	resetState(SHOOTING);
	
	switch(dir)
	    {
	    case DIR_NORTH:
		if(currAnimIndex != RobotGroup.WALKING_NORTH)
		    {
			prevAnimIndex = currAnimIndex;
			currAnimation = group.getAnimationStrip(
					          RobotGroup.WALKING_NORTH);
			currAnimIndex = RobotGroup.WALKING_NORTH;
			currAnimation.reset(); 
		    }
		else
		    {
			animate();
			pos.translate(0, -vel.getY());
		    }
		break;
		
	    case DIR_SOUTH:
		if(currAnimIndex != RobotGroup.WALKING_SOUTH)
		    {
			prevAnimIndex = currAnimIndex;
			currAnimation = group.getAnimationStrip(
                                                  RobotGroup.WALKING_SOUTH);
			currAnimIndex = RobotGroup.WALKING_SOUTH;
			currAnimation.reset(); 
		    }
		else
		    {
			animate();
			pos.translate(0, vel.getY());
		    }
		break;
		
	    case DIR_WEST:
		if(currAnimIndex != RobotGroup.WALKING_WEST)
		    {
			prevAnimIndex = currAnimIndex;
			currAnimation = group.getAnimationStrip(
                                                  RobotGroup.WALKING_WEST);
			currAnimIndex = RobotGroup.WALKING_WEST;
			currAnimation.reset(); 
		    }
		else
		    {
			animate();
			pos.translate(-vel.getX(), 0);
		    }
		break;
		
	    case DIR_EAST:
		if(currAnimIndex != RobotGroup.WALKING_EAST)
		    {
			prevAnimIndex = currAnimIndex;
			currAnimation = group.getAnimationStrip(
                                                  RobotGroup.WALKING_EAST);
			currAnimIndex = RobotGroup.WALKING_EAST;
			currAnimation.reset(); 
		    }
		else
		    {
			animate();
			pos.translate(vel.getX(), 0);
		    }
		break;
		
	    default:
		break;
	    } 
    }
    
} // Robot 
Here's RobotGroup.java

import java.applet.*;

public class RobotGroup extends ActorGroup2D
{
    // indices to pre-defined animation sequences
    public static final int WALKING_NORTH  = 0;
    public static final int SHOOTING_NORTH = 1;
    
    public static final int WALKING_SOUTH  = 2;
    public static final int SHOOTING_SOUTH = 3;
    
    public static final int WALKING_EAST   = 4;
    public static final int SHOOTING_EAST  = 5;
    
    public static final int WALKING_WEST   = 6;
    public static final int SHOOTING_WEST  = 7;
    
    // creates a new RobotGroup object
    public RobotGroup()
    {
	super();
	
	animations = new AnimationStrip[8];
    }
    
    // initializes the eight animation sequences
    public void init(Applet a)
    {
	ImageLoader loader;
	int i;
	
	// NORTH
	loader = new ImageLoader(a, "robot_north.gif", true);
	animations[WALKING_NORTH] = new AnimationStrip();
	for(i = 0; i < 4; i++)
	    {
		animations[WALKING_NORTH].
		    addFrame(loader.extractCell((i*72)+(i+1), 1, 72, 80));               
	    }
	animations[WALKING_NORTH].setAnimator(new Animator.Looped());
	
	animations[SHOOTING_NORTH] = new AnimationStrip();         
	for(i = 0; i < 2; i++)
	    {
		animations[SHOOTING_NORTH].
		    addFrame(loader.extractCell((i*72)+(i+1), 82, 72, 80));
	    }  
	animations[SHOOTING_NORTH].setAnimator(new Animator.Looped());
	
	
	// SOUTH
	loader = new ImageLoader(a, "robot_south.gif", true);
	animations[WALKING_SOUTH] = new AnimationStrip();
	for(i = 0; i < 4; i++)
	    {
		animations[WALKING_SOUTH].
		    addFrame(loader.extractCell((i*72)+(i+1), 1, 72, 80));   
	    }         
	animations[WALKING_SOUTH].setAnimator(new Animator.Looped());
	
	animations[SHOOTING_SOUTH] = new AnimationStrip();         
	for(i = 0; i < 2; i++)
	    {
		animations[SHOOTING_SOUTH].
		    addFrame(loader.extractCell((i*72)+(i+1), 82, 72, 80));
	    }  
	animations[SHOOTING_SOUTH].setAnimator(new Animator.Looped());
	
	
	// EAST
	loader = new ImageLoader(a, "robot_east.gif", true);
	animations[WALKING_EAST] = new AnimationStrip();
	for(i = 0; i < 4; i++)
	    {
		animations[WALKING_EAST].
		    addFrame(loader.extractCell((i*72)+(i+1), 1, 72, 80));  
	    }         
	animations[WALKING_EAST].setAnimator(new Animator.Looped());
	
	animations[SHOOTING_EAST] = new AnimationStrip();         
	for(i = 0; i < 2; i++)
	    {
		animations[SHOOTING_EAST].
		    addFrame(loader.extractCell((i*72)+(i+1), 82, 72, 80));
	    }  
	animations[SHOOTING_EAST].setAnimator(new Animator.Looped());
	
	
	// WEST      
	loader = new ImageLoader(a, "robot_west.gif", true);
	animations[WALKING_WEST] = new AnimationStrip();
	for(i = 0; i < 4; i++)
	    {
		animations[WALKING_WEST].
		    addFrame(loader.extractCell((i*72)+(i+1), 1, 72, 80));    
	    }         
	animations[WALKING_WEST].setAnimator(new Animator.Looped());
	
	animations[SHOOTING_WEST] = new AnimationStrip();         
	for(i = 0; i < 2; i++)
	    {
		animations[SHOOTING_WEST].
		    addFrame(loader.extractCell((i*72)+(i+1), 82, 72, 80));
	    }  
	animations[SHOOTING_WEST].setAnimator(new Animator.Looped());
    }
    
} // RobotGroup2D 
Now a number of .gif files may be needed:

Here's file ActorTest.java (contains RobotAdapter)

import java.applet.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.awt.geom.*;
import java.util.*;

// adapter class for controlling a Robot object
class RobotAdapter extends KeyAdapter
{
    private Robot robot;
    
    public RobotAdapter(Robot r)
    {
	robot = r;
    }
    
    // fires the robot gun or moves the robot
    public void keyPressed(KeyEvent e)
    {
	robot.resetState(Robot.SHOOTING);
	
	switch(e.getKeyCode())
	    {
	    case KeyEvent.VK_SPACE:
		robot.startShooting();
		break;
		
	    case KeyEvent.VK_UP:
		robot.move(Robot.DIR_NORTH);
		break;
		
	    case KeyEvent.VK_DOWN:
		robot.move(Robot.DIR_SOUTH);
		break;
		
	    case KeyEvent.VK_LEFT:
		robot.move(Robot.DIR_WEST);
		break;
		
	    case KeyEvent.VK_RIGHT:
		robot.move(Robot.DIR_EAST);
		break;
		
	    default:
		break;
	    } 
    }
    
    // if the space bar is relesed, stop the robot from shooting
    public void keyReleased(KeyEvent e) 
    {
	if(e.getKeyCode() == KeyEvent.VK_SPACE)
	    {
		robot.stopShooting();
	    }
    }
} // RobotAdapter


public class ActorTest extends Applet implements Runnable
{
    // a thread for animation
    private Thread animation;
    
    // an offscreen rendering buffer
    private BufferedGraphics offscreen;
    
    // a Paint for drawing a tiled background
    Paint paint;
    
    // geometry for filling the background
    private Rectangle2D floor;
    
    // our moveable robot
    private Robot robot;
    
    public void init()
    {               
	// create the RobotGroup
	RobotGroup group = new RobotGroup();
	group.init(this);
	
	// set the Robot bounds equal to the window size
	group.MIN_X_POS = 0;
	group.MIN_Y_POS = 0;
	
	group.MAX_X_POS = getSize().width;
	group.MAX_Y_POS = getSize().height;
	
	// create our robot in the center of the screen
	robot = new Robot(group);
	
	robot.setPos((getSize().width  - robot.getWidth()) /2,
		     (getSize().height - robot.getHeight())/2);
	
	// register a new RobotAdapter to receive Robot movement commands
	addKeyListener(new RobotAdapter(robot));
	
	// create the background paint
	createPaint();
	
	offscreen = new BufferedGraphics(this);
	
	AnimationStrip.observer = this;
	
	animation = new Thread(this);
    } // init
    
    // create a tiled background paint
    private void createPaint()
    {
	Image image = getImage(getDocumentBase(), "stile.gif");
	while(image.getWidth(this) <= 0);
	
	// create a new BufferedImage with the image's width and height
	BufferedImage bi = new BufferedImage(
					     image.getWidth(this), 
					     image.getHeight(this), 
					     BufferedImage.TYPE_INT_RGB);
	
	// get the Graphics2D context of the BufferedImage and 
	// render the original image onto it
	((Graphics2D)bi.getGraphics()).drawImage(image, 
						 new AffineTransform(), 
						 this);
	
	// create the anchoring rectangle for the paint's 
	// image equal in size to the image's size
	floor = new Rectangle2D.Double(0, 
				       0, 
				       getSize().width, 
				       getSize().height);
	
	// set the paint
	paint = new TexturePaint(bi, 
				 new Rectangle(0, 
					       0, 
					       image.getWidth(this), 
					       image.getHeight(this)));

    }   
    
    public void start()
    {
	// start the animation thread
	animation.start();
    }
    
    public void stop() 
    {
	animation = null;
    }
    
    public void run() 
    {
	Thread t = Thread.currentThread();
	while (t == animation)
	    {
		try
                    {     
			Thread.sleep(10);
                    }
		catch(InterruptedException e) 
                    {
			break;
                    }
		repaint();
	    }              
    } // run
    
    public void update(Graphics g)
    {
	robot.update();
	
	paint(g);
    }
    
    public void paint(Graphics g) 
    {
	Graphics2D bg = (Graphics2D)offscreen.getValidGraphics();
	
	// set the paint and fill the background
	bg.setPaint(paint);
	bg.fill(floor);
	
	// paint the robot
	robot.paint(bg);
	
	// draw the offscreen image to the window
	g.drawImage(offscreen.getBuffer(), 0, 0, this);
    } // paint
    
} // ActorTest 
Here's ActorTest.html now:

<html>

  <head>
    <title>ActorTest</title>
  </head>

  <body>
    <hr>
    <applet code=ActorTest.class width=300 height=300></applet>
    <hr>

  </body>

</html>
You should already have BufferedGraphics.java but here it is anyway:

import java.applet.*;
import java.awt.*;

public class BufferedGraphics extends Object
{
    // the Component that will be drawing the offscreen image
    protected Component parent;
    
    // the offscreen rendering Image
    protected Image buffer;
    
    // creates a new BufferedGraphics object           
    protected BufferedGraphics()
    {
	parent = null;
	buffer = null;
    }
    
    // creates a new BufferedGraphics object with the sent parent Component
    public BufferedGraphics(Component c)
    {
	parent = c;
	
	createBuffer();
    }
    
    public final Image getBuffer()
    {
	return buffer;
    }
    
    // returns the buffer's Graphics context after 
    // the buffer has been validated
    public Graphics getValidGraphics()
    {
	if(! isValid())
	    {
		createBuffer();
	    }
	return buffer.getGraphics();
    }
    
    // creates an offscreen rendering image matching 
    // the parent's width and height
    protected void createBuffer()
    {
	Dimension size = parent.getSize();
	buffer = parent.createImage(size.width, size.height);
    }
    
    // validates the offscreen image against several criteria, namely 
    // against the null reference and the parent's width and height 
    protected boolean isValid()
    {       
	if(parent == null)
	    {
		return false;
	    }
	
	Dimension s = parent.getSize();
	
	if(buffer == null || 
	   buffer.getWidth(null)  != s.width || 
	   buffer.getHeight(null) != s.height)
	    {
		return false;
	    }
	
	return true;
    }
    
} // BufferedGraphics
You also should already have BufferedGraphicsTest.java but here it is anyway:

import java.applet.*;
import java.awt.*;
import java.awt.image.*;
import java.awt.geom.*;

public class BufferedGraphicsTest extends Applet implements Runnable
{
    // a Thread for animation
    private Thread animation;
    
    // the offscreen rendering image
    private BufferedGraphics offscreen;
    
    // an Image to render
    private Image pipe;
    
    // the width and height of one Image frame
    private int imageWidth;
    private int imageHeight;
    
    // the position, velocity, and rotation of the Image object
    private int x;
    private int y;
    
    private int vx;
    private int vy;
    
    private double rot;
    
    private AffineTransform at;
    private final double ONE_RADIAN = Math.toRadians(10);
    
    public void init()
    {
	// create the image to render
	pipe = getImage(getDocumentBase(), "pipe.gif"); 
	while(pipe.getWidth(this) <= 0);
	
	// create the offscreen image
	offscreen = new BufferedGraphics(this);
	
	imageWidth = pipe.getWidth(this);
	imageHeight = pipe.getHeight(this);
	
	vx = 3+(int)(Math.random()*5);
	vy = 3+(int)(Math.random()*5);
	
	x = getSize().width/2  - imageWidth/2;
	y = getSize().height/2 - imageHeight/2;
	rot = 0;
	at = AffineTransform.getTranslateInstance(x, y);
	
    } // init
    
    public void start()
    {
	// start the animation thread
	animation = new Thread(this);
	animation.start();
    }
    
    public void stop() 
    {
	animation = null;
    }
    
    public void run() 
    {
	Thread t = Thread.currentThread();
	while (t == animation)
	    {
		try
                    {     
			Thread.sleep(33);
                    }
		catch(InterruptedException e) 
                    {
			break;
                    }
		
		repaint();
	    }              
    } // run
    
    public void update(Graphics g)
    {
	// update the object's position
	x += vx;
	y += vy;
	
	// keep the object within our window
	if(x < 0)
	    {
		x = 0;
		vx = -vx;
	    }
	else if(x > getSize().width - imageWidth)
	    {
		x = getSize().width - imageWidth;
		vx = -vx;
	    }
	
	if(y < 0)
	    {
		y = 0;
		vy = -vy;
	    }
	else if(y > getSize().height - imageHeight)
	    {
		y = getSize().height - imageHeight;
		vy = -vy;
	    }
	
	if(vx > 0)
	    rot += ONE_RADIAN;
	else
	    rot -= ONE_RADIAN;
	
	
	// set the transform for the image
	at.setToIdentity();
	at.translate(x + imageWidth/2, y + imageHeight/2);     
	at.rotate(rot);
	at.translate(-imageWidth/2, -imageHeight/2);
	
	paint(g);
    }     
    
    public void paint(Graphics g) 
    {
	// validate and clear the offscreen image
	Graphics2D bg = (Graphics2D)offscreen.getValidGraphics();
	bg.setColor(Color.black);
	bg.fill(new Rectangle(getSize().width, getSize().height));
	
	// draw the pipe to the offscreen image
	bg.drawImage(pipe, at, this);
	
	// draw the offscreen image to the applet window 
	g.drawImage(offscreen.getBuffer(), 0, 0, this);
	
    } // paint
    
} // BufferedGraphicsTest  
Here's pipe.gif.

We should also note StaticActor.java

import java.awt.*;

public class StaticActor extends Actor2D
{
    public StaticActor(ActorGroup2D grp)
    {
	super(grp);
	
	// just reference the 0th (and only) animation strip
	currAnimation = group.getAnimationStrip(0);
	
	frameWidth = currAnimation.getFrameWidth();
	frameHeight = currAnimation.getFrameHeight();
    }
    
} // StaticActor
We should also make a note of StaticActorGroup.java now:

import java.applet.*;

public class StaticActorGroup extends ActorGroup2D
{
    private String filename;
    
    protected StaticActorGroup()
    {
	filename = null;
    }
    
    public StaticActorGroup(String fn)
    {
	filename = fn;
	animations = new AnimationStrip[1];
    }
    
    public void init(Applet a)
    {
	animations[0] = new AnimationStrip();
	Image image = a.getImage(a.getCodeBase(), filename);
	while(image.getWidth(a) <= 0);
	animations[0].addFrame(image);
	animations[0].setAnimator(new Animator.Single());
    }
    
} // StaticActorGroup
That's basically it for this chapter (10, an Actor2D class).


Last updated: Apr 14, 2002 by Adrian German for T540