Difference between revisions of "XanderPaintingFramework"

From Robowiki
Jump to navigation Jump to search
m (→‎Extending PaintManager to interface with other frameworks: fix incorrect statements in some comments of one of the examples)
m (→‎ToDo's: update TODO list)
Line 231: Line 231:
 
= ToDo's =
 
= ToDo's =
  
As of the version released with XanderCat 10.19, the following features remain to be implemented:
+
The following features remain to be implemented:
#  <strike>Clip the window painters so that they cannot paint outside of the window bounds.</strike> (Done!  But not available in 10.19)
 
#  <strike>Add resize support on the windows.</strike> (Done!  But not available in 10.19)
 
 
#  Add close button on the window title bars.
 
#  Add close button on the window title bars.
#  <strike>Provide better initial window positioning.</strike> (Done!  But not available in 10.19)
 
#  <strike>Add code to save/load painter states between battles.</strike> (Done! But not available in 10.19)
 
 
#  Add ability to change more of the colors and styles used in the framework.
 
#  Add ability to change more of the colors and styles used in the framework.
 
#  Improve default color palette.
 
#  Improve default color palette.

Revision as of 20:42, 2 December 2011

Introduction

The Xander Painting Framework is a framework to aid in painting information to the screen in Robocode. It's primary goals are to:

  1. Provide a convenient means for toggling painters on and off.
  2. Provide a construct for painting informational displays in windows that can be moved around the screen.
  3. Make it easy to remove most of the painting components from finished robots to reduce code size, and make it equally easy to add them back in for development and debugging.

Xander Painting Framework Screen shot (showing Painters of the Xander Robot Framework): Xander-painter-framework.png

Note: If you have looked at or are aware of the Xander Robot Framework, note that the Xander Painting Framework stands on it's own. While the Xander Robot Framework makes use of the Xander Painting Framework, the Xander Painting Framework does not require the Xander Robot Framework.

Paintables and the Paintable Interface

The first step towards using the Xander Painting Framework is to make some information paintable. Doing this requires two steps:

  1. Implementing the Paintable interface on a class that will provide information to be painted.
  2. Adding the Paintable object to the Paintables class.

Any code this is necessary to make an object paintable will remain with the robot whether the framework is enabled or disabled, so it is best to keep such code to a minimum if possible.

The Paintable interface:

public interface Paintable {

	/**
	 * Returns a unique name for the Painter that is responsible for painting
	 * this Paintable.
	 * 
	 * @return   Painter name for this Paintable
	 */
	public String getPainterName();
	
}

There are two types of paintable objects: paintable objects with specific painters, and paintable objects without. In the former case, the getPainterName() method should return the name of the specific painter responsible for painting the specific paintable instance. In the latter case, the getPainterName() method should return null and any Painter can paint information from a single instance (and only a single instance) of the class.

Painting

Probably the best way to demonstrate how to use the framework for painting, and to demonstrate how what might sound like a complicated framework makes life easier is to provide an example. Below is the source code for a Painter class that prints the name of the current drive, gun, and radar to a window on the screen (this Painter is part of the Xander Robot Framework; the RobotProxy class is a Paintable class that provides access to this information).

/**
 * Paints the name of the currently active radar, drive, and gun to the screen.
 * 
 * @author Scott Arnold
 */
public class ActiveComponentsPainter extends TextPainter<RobotProxy> {

	public ActiveComponentsPainter() {
		super("Active Components", 200, 3);  // default size: width 200, height 3 lines of text
	}

	@Override
	public Class<RobotProxy> getPaintableClass() {
		return RobotProxy.class;
	}

	@Override
	protected String getText(RobotProxy robotProxy) {
		StringBuilder sb = new StringBuilder();
		sb.append("Radar: " + robotProxy.getActiveRadarName() + "\n");
		sb.append("Drive: " + robotProxy.getActiveDriveName() + "\n");
		sb.append("Gun:   " + robotProxy.getActiveGunName());
		return sb.toString();
	}
}

The above example makes use of the framework's TextPainter class. The framework providers the following painting interface and classes:

  • Painter -- root interface for painters within the framework
  • AbstractPainter -- abstract painter class that adds support for togging painters on and off through the menu
  • WindowPainter -- abstract painter class that extends the AbstractPainter class and provides a window to paint within.
  • TextPainter -- abstract painter class that extends the WindowPainter class and is designed for text-only displays.

The Painter Interface

Implementing the Painter interface is the minimum required to be painted by the framework. While you can create a class that implements Painter, it is better to extend one of the framework abstract classes. Otherwise, the Painter will still appear in the menu but you will not be able to toggle the painter on and off.

The AbstractPainter Class

The AbstractPainter class adds support for toggling the painter on and off. Anything that extends AbstractPainter or one of it's subclasses will have a toggle button for it in the menu.

Extend this class for painters that wish to paint over the entire battle field, such as painters that paint bullet waves.

The WindowPainter Class

The WindowPainter class provides a interactive window that the painter can paint within. When using this class, position (0, 0) is the lower left hand corner of the window. Windows can be moved around the screen by dragging them by their title bars, and resized by dragging the resize handle at the bottom right of the window. Not all Graphics2D painting methods are supported when using a WindowPainter; only draw(Shape) and the various drawString(...) methods are supported.

The TextPainter Class

The TextPainter class is a subclass of WindowPainter that makes it a little easier to draw text to the screen. When using a TextPainter, you need only provide the lines of text to print, without having to worry about actually drawing them to the window.

Enabling and Disabling the Framework

The heart of the framework is the PaintManager class. To enable the framework, you need to call one of the enable(...) methods on the PaintManager instance. To disable the framework, comment out or remove the line that calls enable(...).

The arguments to the enable(...) methods activate painting and provide the PaintManager with two things it needs to do its job: the battle field height and the list of Painters.

Example:

	// in a one-time setup method somewhere in the robot class
	PaintManager.getInstance().enable(getBattleFieldHeight(), painters);

Given that Robocode does not provide built-in listeners, you also need to ensure the following methods get called on the PaintManager instance (if you do this manually, you will need to comment out these lines as well when disabling the framework, and uncomment them when enabling the framework):

  • public void onPaint(Graphics2D g)
  • public void mouseClicked(MouseEvent e)
  • public void mousePressed(MouseEvent e)
  • public void mouseReleased(MouseEvent e)
  • public void mouseDragged(MouseEvent e)
  • public void mouseMoved(MouseEvent e)

To make this potentially easier, PaintManager implements MouseListener and MouseMotionListener.

Extending PaintManager to interface with other frameworks

To avoid having to manually call the onPaint, MouseListener, and MouseMotionListener methods, you can optionally extend PaintManager and provide the appropriate code to interface with your own robot.

PaintManager is a Singleton, but you can extend it so long as you implement the static getInstance() method in a manner that initializes the instance in PaintManager, and ensure that you call the getInstance() method on your extended PaintManager class instead of PaintManager the first time you call getInstance() (which is usually when you call enable(...)). As a Singleton, you should also make the constructor private.

As an example, below is the XanderPaintManager class used by the Xander Robot Framework, that extends PaintManager and nicely integrates the Xander Painting Framework into the Xander Robot Framework:

/**
 * PaintManager to use with the Xander robot framework.  If using the Xander
 * robot framework, make sure you call the getInstance() methods on this class, 
 * and not on the regular PaintManager class. 
 * 
 * @author Scott Arnold
 */
public class XanderPaintManager extends PaintManager implements PaintListener {
	
	private XanderPaintManager() {
		// RobotEvents is a Xander Robot Framework class that manages events.
		// It contains all the event listeners needed to ensure all the proper
		// PaintManager methods get called.
		RobotEvents robotEvents = Resources.getRobotEvents();
		robotEvents.addPainter(this);
		robotEvents.addMouseListener(this);
		robotEvents.addMouseMotionListener(this);
	}
	
	// Provide a getInstance() method that initializes the instance variable of the superclass
	public static synchronized PaintManager getInstance() {
		if (instance == null) {
			instance = new XanderPaintManager();
		}
		return instance;
	}

	/**
	 * Enable the PaintManager with only the painters that are
	 * part of the Xander robot framework.
	 * 
	 * @param battleFieldHeight
	 */
	public void enable(double battleFieldHeight) {
		enable(battleFieldHeight, (List<Painter<? extends Paintable>>)null);
	}
	
	@Override
	public void enable(double battleFieldHeight,
			List<Painter<? extends Paintable>> painters) {
		List<Painter<? extends Paintable>> fullPainterList = new ArrayList<Painter<? extends Paintable>>();
		// create all the framework painters first
		fullPainterList.addAll(XanderPainters.getPainters());
		// then add in all the custom painters
		if (painters != null) {
			fullPainterList.addAll(painters);
		}
		// then pass all painters to the superclass
		super.enable(battleFieldHeight, fullPainterList);
	}

This example from the Xander Robot Framework also overrides the primary enable(...) method, and adds another custom enable(...) method. This is not something that needs to be done; the Xander Robot Framework does this to ensure it's own framework painters get added to the PaintManager in addition to any custom painters the robot adds.

Saving and Restoring Painter Settings

Optionally, you can choose to save the state of the painting framework between battles. This allows all of your previously enabled painters, colors, windows sizes, and window locations to be as you left them the next time you use the painting framework.

To use this functionality, make sure the following methods gets called:

  • public void restoreState(AdvancedRobot robot) -- call this method after enabling the framework.
  • public void saveState(AdvancedRobot robot) -- call this method at the end of the battle.

Note: Don't forget to also comment out the call to these methods as well (or have your custom PaintManager subclass handle it, if you have one) when disabling the framework to reduce the packaged JAR size.

Updating your robot version does not clear out the saved painting framework information. If your robot has changed significantly and you want to clear out previously saved window information, you can either not call restoreState(...), or at any time before calling saveState(...), you can call clearState().

Window information is keyed by the Painter names, so if you change a painter name, the state will not be restored.

Orphan Painters and Paintables

An orphan is any Painter that does not have a Paintable to paint, or any Paintable that does not have a Painter to paint it.

When an orphan Paintable exists, the Xander Painting Framework simply ignores it as if it didn't exist.

When an orphan Painter exists, the Xander Painting Framework assumes that you were expecting it to paint something. Despite having nothing to paint, it will still appear in the paint menu on the screen, but it will appear in the disabled menu color and will not have a toggle button.

Errors

If you have a Paintable that specifies a Painter name, and the corresponding Painter of that name specifies a Paintable class that is not compatible with the actual Paintable class, an IllegalArgumentException will be thrown.

Odds and Ends

Part of the difficulty in developing this framework is integrating it into the limited painting environment that Robocode provides. Some Java painting capabilities are not allowed by Robocode, and some just don't work. On top of that, there are no built-in event listeners for Robocode events nor mouse events. Below is a list of these issues that had to be overcome, how they were overcome, and tips to handling the remaining oddities.

Transforms on Graphics2D Not Functional

Creating a window that can be moved around the battle field was easier said than done. In order to support movable windows, there needs to be a way to shift the coordinate system. Normally, this could be done by manipulating the Transform associated with the Graphics2D object. However, in the unique Robocode evironment, this is not possible.

Early on, one idea for handling this situation was to create a separate image, create a separate Graphics2D just for that image, and then paint the image onto the main Robocode Graphics2D object. However, Robocode security does not allow you to call the drawImage methods, so this idea was out.

The final solution was to build a proxy class to the Robocode Graphics2D object. The proxy class has some of the same paint methods defined in it as Graphics2D, but before delegating actual painting to the Graphics2D object, the Shape or text position is translated to provide the proper on screen location. Text is as simple as changing the (x,y) coordinate of where the text will be drawn. For Shapes, a Transform is applied to the Shape itself, rather than to the Graphics2D object.

Clipping on Graphics2D Not Functional

Another problem with the window system was how to clip the painting such that a window painter cannot paint outside of the window bounds. Much like Transforms, using the standard Graphics2D clipping functions is not an option, as they either don't work or cause exceptions in the Robocode environment.

The solution for clipping Shapes was to use a set of classes specifically designed for clipping Shapes into rectangles. It does this be creating a new GeneralPath for the Shape where all points are forced to remain within the clipping Rectangle. This Clipping to a Rectangle solution is credited to Jeremy Wood, and is covered by a modified BSD license, Copyright (c) 2011, Jeremy Wood. A copy of the BSD license conditions is included in the Javadoc for the Clipper class in Clipper.java.

For clipping text, the FontMetrics instance of the Graphics2D object is used to determine how much of the text will fit within the window bounds. Text that is too long is truncated and an ellipsis is added to the end. Text completely off the screen or text that begins off screen is not printed.

drawString methods utilizing the AttributedCharacterIterator are not clipped.

No Built-In Event Handlers

Another problem with developing this framework was in handling the events, as there are no built-in listeners for a Robocode robot. As such, the user of this framework must ensure that the proper event methods get called. Much of this is detailed in the sections on enabling and disabling the framework (see the appropriate section above). However, a few other minor related details that can help make this framework work more smoothly include:

  • Between rounds, mouse events can get lost. This can cause strange behavior if you are interacting with the framework between rounds; for example, if a round ends while dragging a window, upon start of the next round the window can continue to drag even if you are no longer holding the mouse button down. To avoid this problem, if you have extended the PaintManager class, simply set the PaintManager variables dragTarget and resizeTarget to null between rounds (they are declared protected to allow for this). Regardless of whether or not you extended PaintManager, you can also just call mouseReleased(null) on the PaintManager instance between rounds, as this accomplishes the same thing (however, this is not guaranteed to continue to work properly with future updates to the famework).

ToDo's

The following features remain to be implemented:

  1. Add close button on the window title bars.
  2. Add ability to change more of the colors and styles used in the framework.
  3. Improve default color palette.
  4. Change toggle button enabled images from squares to check marks to make their behavior more obvious.

Obtaining a Copy

The latest bundled copy of the Xander Painting Framework source code is available as a ZIP file. In addition to the main painting framework classes in the xander.paint package, it also includes the Xander file utility class in the xander.util package, and the Clipper classes in the com.bric package, both of which are required.

For an example of it in use, an early release of the Xander Painting Framework is available in robot XanderCat 10.19 (though it doesn't have all the latest features). Look in package "xander.core.paint" which contains classes that use the Xander Painting Framework with the Xander Robot Framework.