This is just a trivial example of how to do java2d programming. It's basically a really crude demonstration of basic java2D concepts.
Don't let the "Tank" name fool you, this isn't anything neat. I cobbled this together as a first, really rough attempt at making a typical tank game, but I never got past the rudimentary stage. If it took over the entire screen when it ran, it'd be a bouncing ball screen-saver.
The things missing include:
Maybe one of these days. Meanwhile, it's simple enough that I thought it'd be useful as (very) basic intro into java2d topics.
There are two class files:
Specifically, the TankSimulator class:
Meanwhile, the Tank class:
TankSimulator extends JComponent. Because TankSimulator extends JComponent, I can add it to a JFrame's content pane. Fortunately, that's just what I do in TankSimulator's main() method:
At that point I call TankSimulator's heartbeat() method, which just loops forever. Each loop, it updates the state of the tank objects, sleeps 10 milliseconds, and then calls frame.repaint().
See Painting in AWT and Swing, but basically repaint() schedules the JFrame to be re-rendered as soon as possible, which in our application is almost immediately.
When the JFrame is repainted, it in turn calls the TankSimulator's paint() method, which in turn iterates through the TankSimulator's Vector of Tank objects, and calls each tank's .draw() method.
Note: The "draw()" method is not at all standard, I just chose that name. I guess if I wanted to be more standard, I'd think about having each Tank object extend JComponent, or something, and have a paint() method.
One curiosity to note here is that while JFrame passes TankSimulator.paint() a Graphics object, it is in fact passing a Graphics2D object. Graphics2D came along after most of the Swing stuff was already defined and in use. Instead of just changing all the Swing classes so they passed around a Graphics2D object instead, they pass the Graphics2D as a Graphics object, and trust that anybody who needs to use the Graphics2D stuff will cast the object to Graphics2D. Which is what we're doing here.
TankSimulator.paint(Graphics) casts the Graphics to Graphics2D, and then calls each tank's draw(Graphics2D) method. Like I said, draw() is not a standard method, so I'm free to do anything I like there.
One twist that I had to wrap my head around is the general model of how java2d works. When I started trying to learn java2D, I was thinking in terms of manipulating a canvas, creating graphical shapes/objects and placing them on the canvas.
However, java2d turns that sort of inside-out. Don't think about manipulating the objects and the canvas. Instead, think of the canvas as something you're never allowed to directly touch. Instead, you get to manipulate a this nifty gadget that's a cross between a slide projector, a spotlight, and a paint sprayer.
Once I got into the habit of thinking of it this way, it all made a lot more sense.
Tank represents a tank object, with instance variables for location and motion:
Note, here I'm using vector more in the geometric sense of motion, rather than the java sense. I'm probably using it imprecisely, I think a Vector actually should contain both the current coordinates and the change in coordinates, and I'll bet that using the term to describe the rate of change in rotation will have various mathematicians climbing the walls... but you get the general idea.
The Tank also has a number of instance variables reflecting the appearance of the tank icon. If I ever do anything more with this code, I'll probably refactor this stuff out to some sort of icon class.
I also have two simple methods for controlling the animation of the object. Now note, these are really crude by any standard:
As just two little examples of how crude this is:
First, increment() should really take some sort of duration parameter, and increment the object according to that parameter. The way it's done now, it assumes that you have everything locked to the same frame rate, and you call increment() on every object once per frame.
Second, bounce() is basically a quick kludge to check for colliding with the map boundaries. Strictly speaking, this should live somewhere else.
It gets even thornier when you think about combining these two problems, because right now, the same outside code is calling increment() and then calling bounce(). If you have increment() going over a larger duration, you have to figure out how and when you're going to check for collisions.
Finally we come to the actual java2D work methods... well, not quite, first we're going to need some helper methods:
Now that we have the stage all set, let's talk about doing the java2d dirty work. The last two methods on the Tank class are:
The getTransform() method creates a java2d AffineTransform object that you'll use to render the Tank. It uses the other methods on Tank, like getScale(), getX(), getY(), getRotationRadians(), getWidth() and getHeight(), to create an appropriate AffineTransform. Then you'll set this AffineTransform on the Graphics2D object when it's time to render this Tank.
The draw() method gets invoked with a Graphics2D reference as a parameter. draw() sets the Stroke, Fill and AffineTransform, then passes the GeneralPath object to the Graphics2D object's draw() and fill() methods.
AffineTransform is named after a mathematical operation you use to manipulate the sort of data structure that java2d uses to keep track of what pixels go where. I'm not going to get any deeper into it than this because, well, I don't understand it :-). At least, not well enough to explain it properly.
I will say that the AffineTransform is the "lens" I mentioned above, in the section titled "Head Twisting". If you look at the Tank.draw() method, you'll see that it calls Tank.getTransform() and sets the AffineTransform on the Graphics2D object (g2.setTransform(this.getTransform())) right before it actually draws the tank's path (g2.draw(this.getPath()). Anything that you pipe through the graphics2D object after you call setTransform() gets, well, transformed by the AffineTransform.
I wrote a little more about affine transforms, specifically some of the gotchas when using them for rotating, translating and scaling.