In this article I will concentrate on the
basics. I always tell people that Java + AWT is a 2d game
programming API, all the elements are there for you to use, you just
need to know how. However, game programming in Java requires a
few special considerations that may not be obvious to someone who has
programmed a game in another language; threading, timing and managing
the garbage collector to name a few. In this article I'll go over
these 'gotcha's' and show you all the tools you'll need to make a game
by presenting a simple applet based demo. I assume you are
familiar with Java and object oriented programming (for a more
basic intro click
here).
We will stick to Java AWT classes to keep it
simple. You should know that the latest Java2D classes give you
ALOT of functionality, like hardware acclerated blitting, a better
timer, and full screen windowing on platforms that support it, to name
a few.
The latest JDK, 1.4.2 does not by default compile
code that will run with outdated vms, namely the Microsoft vm that
currently ships with Windows. To get your applets/applications to
run on machines without requiring the user to install the latest JRE
you must compile with the switch "-target 1.1" and limit your class use
to AWT classes.
In the example applet you will notice the first few frames the animations tend to flicker. This is the result of image caching. There is probably a work around for this but I haven't found one.
The latest JIT compiling virtual machines can give erratic
performance (start slow and become faster) until the vm has 'warmed
up'. This usually isn't noticable unless you actually track
slowest and fastest frame rates.
This wasn't obvious to me when I first learned
Java as I had no experience with the 'event driven' paradigm, so here
is a short tour: Applets are event driven. Applets are
executed from a web browser (duh). The browser gives the applet
an area of the page to draw itself in and loads the applet. There
are four methods in Applet that the browser calls: init();
start(); stop(); update(); and destroy(); So init() is
where you put your init code, start() is where you put your start up
code and stop() is where you put your cleanup code. You'll never
code destroy();
How applets draw themselves is not terribly
intuitive and bears some explanation. The browser will call
update() when the area of the screen the applet is in needs to be
redrawn and pass a Graphics context to the applet to draw itself
on. The update method, which is a method of the Applets parent
class Component, as well as the repaint() method will in turn call the
paint() method. So to get smooth, non flicker animation you
should overide the Applets paint() method and call it when you need to
redraw the applets area. (NOTE: if your confused where
these methods come from download the JDK Documentation and look at
awt.Component. At times it seems learning Java is learning the
classes, so its important to be familiar with them.)
So far we have:
public class GameApplet extends
applet {
init() {}
start() {}
stop() {}
update(Graphics g) {
paint(g);
}
paint(Graphics
g) {
// your code here;
}
}
Double buffering is how you get smooth flicker free animation in
computer graphics. The idea is you draw your game on a non
visible buffer then copy it to the viewable area. If you try
drawing directly on the applet you will notice 'tearing' and
flickering. So for a backbuffer we need an offscreen Image and a
Graphics (think a Graphics as a Device Context in the win api) for that
image:
Graphics backbuffergc;
Image backbuffer;
CreateImage() is also a method that Applet inherits
from its base class, Component. Then we modify the init method
like so:
init() {
backbuffer =
createImage(width, height);
backbuffergc =
backbuffer.getGraphics();
}
paint(Graphics g) {
g.drawImage(backbuffer,0,0,width,height,this);
}
To reiterate, the Graphics context g here is passed
to the applet from the browser; this is your area of the screen to draw
on.
Java's AWT only supports Image formats of .gif or
.jpg. Loading images over the internet can take a bit of time so
a good procedure to use is to put all of your gfx into one large Image
and load it using a MediaTracker and then divide the big image into the
smaller images you will use for tiles, animation frames etc using an
ImageFilter. Gifs are good to use because you get
transparency. Again, consult the Javadocs for documentation of
these classes.
Image [] tiles = new Image [NUMTILES];
public void loadimages () {
MediaTracker t
= new MediaTracker (this);
Image
imagestrip = getImage(getDocumentBase(),"bigimage.gif");
t.addImage(imagestrip,0);
try {
t.waitForAll();
}
catch
(InterruptedException e) {}
if
(t.isErrorAny()) {
System.out.println("Error loading Images");
}
ImageProducer
source = imagestrip.getSource();
for(int
i=0;i<NUMTILES;i++) {
ImageFilter getsubimage=new
CropImageFilter(i*TILEWIDTH,0,TILEWIDTH,TILEHEIGHT);
ImageProducer producer=new
FilteredImageSource(source,getsubimage);
tiles[i]=createImage(producer);
}
}
We'll put the call to this method in the init()
method so our Images will begin loading first thing.
This was another confusing concept for me.
In event driven programming your applet code only executes when it
recieves an event from the browser (ie a mouse move or click in the
applets area). So to get our applet to run continuously we have
to provide a separate thread of execution. Space Invaders would
not be much fun if the aliens only moved when you did, would it?
A detailed discussion of threads is beyond the scope of this
article, but basically, this is where your main loop will go.
To use a thread in our applet we will implement the
Runnable interface. Runnable lets us set up a thread to execute
seemingly continuously. We only have to provide a run() method
which in our case will be the actual main loop
public class gameapplet extends
Applet implements Runnable ...
Thread mainloop = null;
public void start () {
mainloop = new
Thread (this);
if (mainloop
!= null) {
mainloop.start();
}
}
public void run
() {
while
(true) {
// update the game logic
repaint (); // calls paint() - draw the
applet
Thread.currentThread().yield(); // This plays nice with WinME
}
}
public void stop () {
if (mainloop
!= null) {
mainloop.stop();
mainloop=null;
}
}
So our main loop thread begins when the browser
calls the applets start() method and is stopped when the browser calls
stop().
One of the big 'gotcha's' with Java is getting
accurate timing. There are basically two ways to time a game as
you know: 1. Pause the game every time thru the main loop using
currentThread.sleep(int) or 2. Using System.currentTimeMillis() to keep
track of the change in time. The problem with both of these is
the fact the Java timer on most platforms is very inaccurate (as bad as
+ - 50 millisecs on WinME). Win 9x/ME is the worst, Win XP and
Mac are better and Linux is the best from my anecdotal
experience. On all platforms but Linux using the Java timer will
give you erratic, jumpy animation.
There are multiple solutions to this problem.
The GAGE2D (link: http://java.dnsalias.com/) library uses Java Native
Interface (JNI) to access the OS timer thru a .dll under Windows.
This of course makes your app unportable and is not usable for an
applet anyway. The Java3D api includes a high resolution timer
class, but not many people have Java3D installed on their
machines. One of the simplest and most straight forward solutions
is to simply use a self correcting timer class that has been posted in
the forums at javagaming.org and should be on the wiki (links
http://community.java.net/games/). Using this class is straight
forward;
HighResTimer timer;
//in the init method:
timer=new HighResTimer();
timer.setDelay(REFRESH_RATE);
timer.startTimer();
timer.setAutoCorrection(true, 50);
//in the run method:
synchronized(timer) {
try {
timer.wait();
}
catch(Exception e) {}
}
The class as published gives you access to the tick
count which you can use to get framerate independant animation, but
simply waiting is usable for our purposes.
One of the most often discussed features of Java
with regard to games / real time applications is the garbage
collector. The gc is a simple thing to manage however and poses
no real problems. Its true that when the gc kicks in during your
game it creates a noticable lag. This can be seen simply by
coding a System.gc() in your application. This method forces the
gc to execute and causes 100 - 150 millisec pause.
The most simple solution to preventing these lags is
to simply not let any objects go out of reference in your main
loop. You do this by simply not creating or destroying new
objects during your game. Creating new objects during, for
example, level loading is fine. But creating a whole slew of
particles every time the player fires his weapon is wasteful and
invites the gc to step in. A common solution is to create a pool
of objects to draw from and then simply reuse them; using the particle
system example, pre create an array of 100 particles with a member
boolean variale to indicate if its in use and then when you need one
simply iterate thru the array.
Again there is more than one way to approach this
problem. For example the GAGE2D library provides a garbagecan
class to hold unused objects to prevent them from going out of
reference.
At this point we have a basic framework in the
form of an applet that gives us the ability to load graphics, utilize a
continuously running thread of execution, perform double buffering, and
use relatively stable timing under any platform. The final element we
need is the ability to read the players input.
As always, there is more than one way to do
input. The Component class has the methods processKeyEvent() and
processMouseEvent() which you can use in the actual applet.
My personal preference is to separate out the
actually game into a gamemanager object where all of the actual game
logic and rendering occurs and register this class as a KeyListener to
the applet. Most simply this class would consist of public
methods init(), update() and paint(). To do input with this type
of setup simply add your gamemanager object as a keyListener to the
applet;
// in the applets init method
this.addKeyListener(gamemanager);
Then you must code for the KeyListener interface in your gamemanager
object;
public class GameManager
implements KeyListener ...
// in the game application
public void keyTyped (KeyEvent
e) {
if(e.getKeyCode() == KeyEvent.VK_UP) {
//perform some action
}
...
// process
other keys
}
public void keyPressed (KeyEvent
e) {...}
public void keyReleased (KeyEvent
e) {...}
So now you have all the tools to make a game.
The example applet consists of a basic walkaround demo for a console
style rpg game. It demonstrates a scrolling tile map and an
animated player sprite using the classes GameManager, Map, and
Player. The example is intended to be just that, an
example. An actual game would require a great deal more
abstraction and quiet different organization. The source is
commented so feel free to browse it, copy it, condemn it, whatever.
Demo
Source Code
Demo
Applet