Comp201: Principles of Object-Oriented Programming I
Spring 2008 -- Paint Strategies  in BallWar   


This version of Ballworld uses a strategy-based painting technique.    Just like the updating behavior, painting is now delegated to a strategy that can be dynamically specified.

For reference, please see the BallWar documentation.

Generally, painting is accomplished by starting with a prototype shape or image that is of unit size and whose origin is defined to be its center.   This prototype is then scaled, rotated, translated and possibly even mirror-image flipped to correspond to the size, location and orientation of the host Ball.     These transformations are accomplished using affine transformations.    See the discussion below on affine transformations.

(Note that in the following diagram, vararg input parameters are shown as arrays.)

 

IPaintStrategy Interface

The IPaintStrategy interface provides three methods to support painting of the host Ball.    A Ball contains a reference to a single IPaintStrategy to which it delegates the painting duties.

public void paint(Graphics g, Ball host)

This is the method called by Ball when it wants to be painted.  Since no transformation information is given, this method must use the resources available through the host Ball to figure out the location, size, orientation and color of what needs to painted. Typically (See the description of APaintStrategy below), this method will create the necessary AffineTransform object embodying the required translation, rotation and scaling and then call to paintXfrm to actually paint the screen.   The color is usually set by simply setting the color on the Graphics context before delegating to paintXfrm.  (Technically, this an implementation of the Template Method Design Pattern)

public void paintXfrm(Graphics g, Ball host, AffineTransform at)

This method also paints the host Ball onto the Graphics context, but using the given AffineTransform object to determine the translating, rotating and scaling of any internal prototype shapes or images.   This allows the prototype transformations to be defined independently from the host and is typically used when a composite paint strategy is delegating to multiple paint strategies and desires that they all use the same transformation.

public void init(Ball host)

Like the init method of IUpdateStrategy, this method is called whenever a paint strategy is loaded into a host Ball and provides the opportunity for the paint strategy to initialize any internal states it may need.

IPaintStrategy.NullObject

This is a static field of the interface that is a reference to a singleton implementation of the interface whose methods are all no-operations. This is a Null Object Design Pattern implementation that provides a well-defined object that does nothing.    Typically, this is used to provide a well-defined initial value for IPaintStrategy references. 

 

APaintStrategy

This abstract class provides the invariant painting behavior needed by most IPaintStrategy implementations. 

public void paint(Graphics g, Ball host)

This method will create the necessary AffineTransform object embodying the required translation, rotation, scaling and color from information gathered from the host BallSee the discussion below on affine transforms and how to use them.  A call is first made to its own paintCfg method to perform any additional processing of the affine transformation.  Immediately after that it calls to paintXfrm to actually paint the screen.  

public void paintXfrm(Graphics g, Ball host, AffineTransform at)

This is not implemented in this abstract class because it is highly variant.

public void init(Ball host)

This method does nothing, providing this default behavior to all the subclasses.

protected void paintCfg(Graphic g, Ball host)

Overriding this protected method, a subclass can add additional processing of the internal affine transformation before paintXfrm is called by the paint method.   A typical use would be to add the ability to mirror-image flip the image or shape if the direction of travel is right-to-left instead of left-to-right.    By default, this method does nothing, providing this default behavior to all the subclasses.

 

ShapePaintStrategy

This concrete class adds the invariant behavior to APaintStrategy to paint a Shape onto the screen by implementing the paintXfrm method.  This class can be instantiated and used directly by supplying the desired prototype Shape object to its constructor, or it can be subclassed and the prototype Shape object provided by the subclass constructor's call to the superclass constructor.   In general, IShapeFactories (see below) will be used to provide abstract instantiation of the desired prototype Shapes.

ImagePaintStrategy

This concrete class provides invariant behavior for painting image files onto the screen.   The methods paintXfrm is overridden to use the supplied AffineTransform to paint a transformed image onto the screen.    paintCfg is overridden to initially resize the image to a unit size and to redefine the origin to be in the center of the image (by default, the origin of an Image object is defined to be its upper left hand corner, not its center).   The constructor takes a filename, which if started with a "/" character, is referenced with respect to the default package directory, otherwise it is referenced relative to the ballwar.model.paint directory.   A "fill factor" is also supplied to indicate what percentage the usable image actually fills the given image's actual width and height.   This is useful for making collisions appear more at the edges of the drawn image.   This class can be instantiated and used directly by supplying the desired filename and fill factor to its constructor, or it can be subclassed and the required information provided by the subclass constructor's call to the superclass constructor. 

 

ADecoratorPaintStrategy

This abstract class provides default behavior for all IPaintStrategy methods for use by its subclasses. In this implementation of the Decorator Design Pattern, all the methods simply delegate to the IPaintStrategy decoree.  The subclasses wrap the decoree paint strategy and by overriding the desired methods, intercept the calls to that strategy and perform additional processing.   For instance, the FixedColorDecoratorPaintStrategy subclass of ADecoratorStrategy overrides the paintXfrm method to reset the Graphics context color to a particular color, thus causing the decoree to paint with that fixed color.   The Decorator design pattern uses composition to enable additional functionality to be added to an object (the decoree) without needing to subclass it.   The FixedColorDecoratorPaintStrategy can be used to "decorate" any IPaintStrategy to provide fixed color painting.

 

MultiPaintStrategy

This concrete class provides invariant behavior for creating a paint strategy that is a composite of multiple paint strategies.  The init and paintXfrm methods are overridden to simply delegate to the set of "composee" IPaintStrategies (pstrats).   Note that the paint method is not overridden from the APaintStrategy behavior, so that the same AffineTransform is used for all the composees.   This eliminates duplicated processing and instantiation of affine transforms and provides a well-defined, consistent transform for all the composees.    This class can be instantiated and used directly by supplying the desired composee IPaintStrategies to its constructor, or it can be subclassed and the paint strategies provided by the subclass constructor's call to the superclass constructor.

AnimatePaintStrategy

This concrete class provides invariant behavior for "animating" an image by successively changing paint strategies on each paintXfrm call.  A set of IPaintStrategies is provided to AnimatePaintStrategy's constructor and the strategies are cycled though in the order they were given.   To display the same paint strategy multiple times in the cycle, simply specify the same strategy more than once in the constructor's parameter list.  This class can be instantiated and used directly by supplying the desired  IPaintStrategies to its constructor, or it can be subclassed and the paint strategies provided by the subclass constructor's call to the superclass constructor.

 

 

Shape Factories

Shape factories are used to abstract and encapsulate the often complex instantiation process required for Shape objects, particularly Polygons.   IShapeFactories can also make any other defined Shape instance, such as Ellipses, Rectangles, RoundedRectangles or even more generally defined shapes.

IShapeFactory Interface

The IShapeFactory interface provides a single factory method to instantiate a Shape given the center of the shape and x and y scale factors.   The EllipseShapeFactory and RectangleShapeFactory simply instantiate double precision Ellipse2D.Double or Rectangle2D.Double objects respectively.  Many Shapes require the use of double precision Point2D.Double points.  See the note below on Point2D.Double.

 

PolygonShapeFactory

Polygons are different than other Shapes in that they are defined using integer Points rather than double precision values.    Hence, the PolygonShapeFactory's constructor takes a set of Points defining the polygon plus a scale factor that is used to normalize the Polygon to a unit size.    This enables the factory's makeShape method to work normally.    This class can be used directly by supplying the points and scale factor to its constructor or by subclassing and having the subclass's constructor provide the desired information to the superclass constructor.

 

Point2D.Double

Most Shape implementations use double precision values to specify  their parameters to gain increased accuracy.   Thus they do not use the regular Point class, but rather the Point2D.Double class (a public nested class inside of Point2D) which is a point defined with double precision values rather than integer values.  Both Point and Point2D.Double are subclasses of Point2D, so they are interchangeable at that level.

 

Affine Transforms

An affine transform is the mathematical process of scaling (making larger or smaller), rotating (pivoting around the origin) and translating (moving a specified distance and direction) of a point in space.   Remember that a point is really a vector from the origin, so a affine transformation technically transforms vectors, not points.

Affine transforms may seem mathematically daunting, but they are actually quite straightforward.   Plus, Java provides an AffineTransform class that encapsulates the mathematics and makes dealing with affine transformation very easy.

Example of the operations involved in a single Affine transformation, showing the individual steps of rotating, scaling and translating:

Original vector:            then Rotated vector:         

 

 

then Scaled vector:        then Translated vector: 

 

Note that the order of operation is important:

Original vector:           then Rotated vector:          then Translated vector:     

as compared to

Original vector:            then Translated vector:          then Rotated vector:     

 

 

In fancy mathematical words, we same that rotation and translation are non-commutative.    A net Affine transformation is thus dependent on not only what operations (rotate, scale, translate) are defined, but also in what order those operations are defined.

Java's AffineTransform class will execute the requested operations in the reverse order of definition, i.e. from last to first.

For complete information on AffineTransform please see the Java API documentation: http://java.sun.com/j2se/1.5.0/docs/api/java/awt/geom/AffineTransform.html

AffineTransform  Quick Start:

The following is an example of how to use Java's affine transform.   This is not a complete description of all of the capabilities of AfffineTransform!

  1. Instantiate the transform, which will create an identity (no-op) transform:

AfffineTransform at = new AffineTransform();

  1. Initialize the transform to be a translation, rotation or scaling (choose one of the following):

at.setToTranslation(x,y);

at.setToRotation(theta); // theta is in radians

at.setToScale(xScale, yScale);  // x and y axis can be scaled independently

  1. Add operations to the transform (add as many as desired):

at.translate(x,y);

at.rotate(theta); // theta is in radians

at.scale(xScale, yScale);  // x and y axis can be scaled independently

  1. Transform the desired Shape object:

Shape transformedShape = at.createTransformedShape(originalShape);

Remember that the operations will be done in the reverse order of definition, that is, whatever the transform was initialized to will be done last.

To calculate the angle of rotation, the velocity vector of the host Ball can be used, but in order to uniquely determine the angle, the Math.atan2 method must be used instead of the normal Math.atan method.   See the note below comparing these two arctangent methods.

For Image objects, the Graphics context will be able to use the affine transform to transform the image when it draws it:

((Graphics2D)g).drawImage(image, at, host.getEnv().getComponent());

Notes:  The Graphics context given to a paint call is actually a Graphics2D object, so the cast is legal.  Also, many Image processing algorithms require an ImageObserver object to notify in the event that there is a delay or problems in processing the image.   The Component class implements the ImageObserver interface, so it is necessary to get the Component that the image is being drawn upon from the host's environment.

 

Math.atan2(y,x) vs. Math.atan(y/x)

The problem of the arctangent function is that given a value for the tangent, the quadrant of the resultant angle is not uniquely determined.  That is, atan(theta) = atan(theta+PI).

To solve this problem, Java provides an alternate arctangent method called atan2 that takes the individual x and y components and thus can uniquely determine the exact angle.

See the Java API documentation for the arctangent function in the Math class: http://java.sun.com/j2se/1.5.0/docs/api/java/lang/Math.html

 

 

 


Last Revised Thursday, 03-Jun-2010 09:50:22 CDT

©2008 Stephen Wong and Dung Nguyen