Up to this point you have learned about a lot of Java classes that are useful for making a game, particularly the Graphics2D
class. The previous two chapters provided the groundwork for this chapter by showing you how to tap into the Graphics2D
class to draw vectors and bitmaps. At this point, the source code for even a simple bitmap-based game will tend to be too complicated and too difficult to manage without a better way to handle the objects in a game. What you need at this point is a new class that knows how to work with game objects—something known as an actor or a sprite. The goal of this chapter is to develop a way to handle the game objects moving around on the screen.
Here are the specific topics covered in this chapter:
Programming simple sprites
Creating a Sprite
class
Learning about collision testing
A sprite usually represents an animated graphic image that moves around in a game and is involved with other images to form the gameplay of a game. The difference between a regular image and a sprite is often that a sprite will encapsulate the image data as well as the methods needed to manipulate it. We will create a new class later in this chapter called ImageEntity,
which will be able to load and draw a bitmap, and we will then create a new Sprite
class that will use ImageEntity.
Animation will be held off until the next chapter.
I would like to build a pair of classes to simplify sprite programming. We will create the Sprite
class in this chapter and then add the AnimatedSprite
class in the next chapter to handle animation. The new Sprite
class that I’m going to show you here might be described as a heavy class. What do I mean by “heavy”? This is not a simple, abstract class. Instead, it is tied closely to the JFrame
and Graphics2D
objects in our main program. You would not be able to use this Sprite
class on its own in a Java applet (running in a web browser) without modifications, because it relies on the presence of the JFrame to work. Although it is possible to write a Java game that runs in a JFrame or a JApplet (which are both somewhat related), the code to support both applications and applets at the same time is messy. Our new Sprite
class will work fine as a support class for an application-based Java game. If we want to use it in an applet, minor modifications can be made (they are trivial!).
A sprite cannot draw itself without the JFrame
and Graphics2D
objects in a main program. Although the Sprite
class could use methods such as getGraphics()
to pull information from the main applet, our examples use a double buffer (a back buffer image used to update graphics smoothly, without flickering the screen).
The BaseGameEntity
class will handle all of the position, velocity, rotation, and other logistical properties, while ImageEntity
will make use of them by providing methods such as transform()
and draw().
I want to simplify the Sprite
class so it doesn’t expose all of these properties and methods, but provides a simpler means to load and draw images. This simplification will be especially helpful in the next chapter because animation tends to complicate things. Although we have three classes just to draw a single sprite, there’s reason behind this apparent madness—I don’t want to duplicate code in all of the game entities if it can be helped. In the next few chapters we’ll be drawing vectors and sprites, and it is helpful if we can reuse some of the code.
A useful sprite class should handle its own position and velocity data, rather than individual X and Y values for these properties. The sprite’s position and velocity will be handled by the BaseGameEntity
class. The Sprite
class will not inherit from ImageEntity;
instead, it will use this class internally, like a regular variable.
I also want the get methods that return values to resemble simple properties, while the change methods will be in the usual “set” format. For instance, I want the Sprite
class to have a position()
method that returns the position of the Sprite
object, but it will use a setPosition()
method to change the X and Y values. We should be able to access position and velocity by writing code like this:
sprite.position().x sprite.position().y
Whenever possible, we will forego good object-oriented design in favor of simpler source code. This is especially helpful for beginners who may have never seen a truly huge source code listing, and thus would not understand why such things are important.
On top of these requirements, we should not be concerned with numeric data types! I don’t want to typecast integers, floats, and doubles! So, this Sprite
class will need to deal with the differences in the data types automatically and not complain about it! These are minor semantic issues, but they tend to seriously clean up the code. The result will be a solidly built sprite handler. First, let’s take a look at a support class that will make it possible.
An accessor method is a method that returns a private variable in a class. A mutator method is a method that changes a private variable in a class. These are also commonly called “get” and “set” methods.
The BaseVectorShape
class was introduced back in Chapter 3 for the Asteroids-style game. We will use a very similar class for sprite programming in a future version of Galactic War (beginning in Chapter 11). Here is the code for this class.
public class BaseGameEntity extends Object { //variables protected boolean alive; protected double x,y; protected double velX, velY; protected double moveAngle, faceAngle; //accessor methods public boolean isAlive() { return alive; } public double getX() { return x; } public double getY() { return y; } public double getVelX() { return velX; } public double getVelY() { return velY; } public double getMoveAngle() { return moveAngle; } public double getFaceAngle() { return faceAngle; } //mutator methods public void setAlive(boolean alive) { this.alive = alive; } public void setX(double x) { this.x = x; } public void incX(double i) { this.x += i; } public void setY(double y) { this.y = y; } public void incY(double i) { this.y += i; } public void setVelX(double velX) { this.velX = velX; } public void incVelX(double i) { this.velX += i; } public void setVelY(double velY) { this.velY = velY; } public void incVelY(double i) { this.velY += i; } public void setFaceAngle(double angle) { this.faceAngle = angle; } public void incFaceAngle(double i) { this.faceAngle += i; } public void setMoveAngle(double angle) { this.moveAngle = angle; } public void incMoveAngle(double i) { this.moveAngle += i; } //default constructor BaseGameEntity() { setAlive(false); setX(0.0); setY(0.0); setVelX(0.0); setVelY(0.0); setMoveAngle(0.0); setFaceAngle(0.0); } }
The ImageEntity
class gives us the ability to use a bitmap image for the objects in the game instead of just vector-based shapes (such as the asteroid polygon). It’s never a good idea to completely upgrade a game with some new technique, which is why some of the objects in the first version of Galactic War will still be vectors, while the player’s ship will be a bitmap. When you reach Chapter 11, you will have an opportunity to examine the progression of the game from its meager beginning to a complete and complex game with sprite entity management. I always recommend making small, incremental changes, play-testing the game after each major change to ensure that it still runs. There’s nothing more frustrating than spending two hours making dramatic changes to a source code file, only to find the changes have completely broken the program so that either it will not compile or it is full of bugs.
The ImageEntity
class also inherits from the BaseGameEntity
class, so it is related to VectorEntity.
This class is awesome! It encapsulates all of the functionality we need to load and draw bitmap images, while still retaining the ability to rotate and move them on the screen!
// Base game image class for bitmapped game entities
import java.awt.*;
import java.awt.geom.*;
import javax.swing.*;
import java.net.*;
public class ImageEntity extends BaseGameEntity {
//variables
protected Image image;
protected JFrame frame;
protected AffineTransform at;
protected Graphics2D g2d;
//default constructor
ImageEntity(JFrame a) {
frame = a;
setImage(null);
setAlive(true);
}
public Image getImage() { return image; }
public void setImage(Image image) {
this.image = image;
double x = frame.getSize().width/2- width()/2;
double y = frame.getSize().height/2 - height()/2;
at = AffineTransform.getTranslateInstance(x, y);
}
public int width() {
if (image != null)
return image.getWidth(frame);
else
return 0;
}
public int height() {
if (image != null)
return image.getHeight(frame);
else
return 0;
}
public double getCenterX() {
return getX() + width() / 2;
}
public double getCenterY() {
return getY() + height() / 2;
}
public void setGraphics(Graphics2D g) {
g2d = g;
}
private URL getURL(String filename) {
URL url = null;
try {
url = this.getClass().getResource(filename);
}
catch (Exception e) { }
return url;
}
public void load(String filename) {
Toolkit tk = Toolkit.getDefaultToolkit();
image = tk.getImage(getURL(filename));
while(getImage().getWidth(frame) <= 0);
double x = frame.getSize().width/2- width()/2;
double y = frame.getSize().height/2 - height()/2;
at = AffineTransform.getTranslateInstance(x, y);
}
public void transform() {
at.setToIdentity();
at.translate((int)getX() + width()/2, (int)getY() + height()/2);
at.rotate(Math.toRadians(getFaceAngle()));
at.translate(-width()/2, -height()/2);
}
public void draw() {
g2d.drawImage(getImage(), at, frame);
}
//bounding rectangle
public Rectangle getBounds() {
Rectangle r;
r = new Rectangle((int)getX(), (int)getY(), width(), height());
return r;
}
}
Following is the source code for the new Sprite
class. This class includes a ton of features! In fact, it’s so loaded with great stuff that you probably won’t even know what to do with it all at this point. I am not a big fan of inheritance, preferring to build core functionality into each class I use. We will peruse the properties and methods of this class as we need them. This highly reusable Sprite
class will be a useful helper class for any future game project you work on! It is a bit daunting only because I wanted to provide a complete class now rather than give it to you in parts over time. This class resulted from work done on the Galactic War game.
The Sprite
class includes several methods for detecting collisions with other sprites, and it also provides tests for collision with Rectangle
and Point2D
objects as a convenience. Remember that I wanted this Sprite
class to be intuitive and not cause the compiler to complain about silly things, such as data type conversions? Well, the same is true of the collision testing code. There are three versions of the collidesWith()
method in the Sprite
class, providing support for three different parameters:
Rectangle
Sprite
Point2D
This should cover almost any game object that you would like to test for a collision. Since these methods are built into the Sprite
class, you can call them with a single parameter, and the internal data in the sprite itself is used for the second parameter that would normally be passed to a collision routine.
This new Sprite
class does not inherit from anything other than the base Object,
although it uses ImageEntity
internally for access to that class’ excellent support for image loading and drawing. Why doesn’t this class inherit from BaseGameEntity
or ImageEntity?
Those classes followed a logical inheritance chain but also included a lot of features that do not need to be in the core of the Sprite
class. We still have access to those properties and methods if we want to use them, by using an ImageEntity
as a private variable, but we get around the problem of having to deal with private/public access and inheritance. Inheritance is a beautiful concept, but in practice too much of it can make a program too complicated.
// Sprite class import java.awt.*; import javax.swing.*; public class Sprite extends Object { private ImageEntity entity; protected Point pos; protected Point vel; protected double rotRate; protected int currentState; //constructor Sprite(JFrame a, Graphics2D g2d) { entity = new ImageEntity(a); entity.setGraphics(g2d); entity.setAlive(false); pos = new Point(0, 0); vel = new Point(0, 0); rotRate = 0.0; currentState = 0; } //load bitmap file public void load(String filename) { entity.load(filename); } //perform affine transformations public void transform() { entity.setX(pos.x); entity.setY(pos.y); entity.transform(); } //draw the image public void draw() { entity.g2d.drawImage(entity.getImage(),entity.at,entity.frame); } //draw bounding rectangle around sprite public void drawBounds(Color c) { entity.g2d.setColor(c); entity.g2d.draw(getBounds()); } //update the position based on velocity public void updatePosition() { pos.x+ = vel.x; pos.y+ = vel.y; } //methods related to automatic rotation factor public double rotationRate() { return rotRate; } public void setRotationRate(double rate) { rotRate = rate; } public void updateRotation() { setFaceAngle(faceAngle() + rotRate); if (faceAngle() < 0) setFaceAngle(360 - rotRate); else if (faceAngle() > 360) setFaceAngle(rotRate); } //generic sprite state variable (alive, dead, collided, etc) public int state() { return currentState; } public void setState(int state) { currentState = state; } //returns a bounding rectangle public Rectangle getBounds() { return entity.getBounds(); } //sprite position public Point position() { return pos; } public void setPosition(Point pos) { this.pos = pos; } //sprite movement velocity public Point velocity() { return vel; } public void setVelocity(Point vel) { this.vel = vel; } //returns the center of the sprite as a Point public Point center() { int x = (int)entity.getCenterX(); int y = (int)entity.getCenterY(); return(new Point(x,y)); } //generic variable for selectively using sprites public boolean alive() { return entity.isAlive(); } public void setAlive(boolean alive) { entity.setAlive(alive); } //face angle indicates which direction sprite is facing public double faceAngle() { return entity.getFaceAngle(); } public void setFaceAngle(double angle) { entity.setFaceAngle(angle); } public void setFaceAngle(float angle){ entity.setFaceAngle((double) angle); } public void setFaceAngle(int angle){ entity.setFaceAngle((double) angle); } //move angle indicates direction sprite is moving public double moveAngle() { return entity.getMoveAngle(); } public void setMoveAngle(double angle) { entity.setMoveAngle(angle); } public void setMoveAngle(float angle){ entity.setMoveAngle((double) angle); } public void setMoveAngle(int angle){ entity.setMoveAngle((double) angle); } //returns the source image width/height public int imageWidth() { return entity.width(); } public int imageHeight() { return entity.height(); } //check for collision with a rectangular shape public boolean collidesWith(Rectangle rect) { return (rect.intersects(getBounds())); } //check for collision with another sprite public boolean collidesWith(Sprite sprite) { return (getBounds().intersects(sprite.getBounds())); } //check for collision with a point public boolean collidesWith(Point point) { return (getBounds().contains(point.x, point.y)); } public JFrame frame() { return entity.frame; } public Graphics2D graphics() { return entity.g2d; } public Image image() { return entity.image; } public void setImage(Image image) { entity.setImage(image); } }
Let’s give the new classes we’ve developed in this chapter a test run. The following program (shown in Figure 6.1) draws a background image and then draws a sprite randomly on the screen. This test program uses a thread and the Runnable
interface in order to draw a sprite repeatedly on the screen without user input. We’ll study this feature more thoroughly in Chapter 10, when we learn more about threads and the game loop. Study this short demo program well, because it demonstrates perhaps the first high-speed example you’ve seen thus far.
// SpriteTest program import java.awt.*; import java.awt.image.*; import javax.swing.*; import java.util.*; import java.net.*; public class SpriteTest extends JFrame implements Runnable { int screenWidth = 640; int screenHeight = 480; //double buffer objects BufferedImage backbuffer; Graphics2D g2d; Sprite asteroid; ImageEntity background; Thread gameloop; Random rand = new Random(); public static void main(String[] args) { new SpriteTest(); } public SpriteTest() { super("Sprite Test"); setSize(640,480); setVisible(true); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //create the back buffer for smooth graphics backbuffer = new BufferedImage(screenWidth, screenHeight, BufferedImage.TYPE_INT_RGB); g2d = backbuffer.createGraphics(); //load the background background = new ImageEntity(this); background.load("bluespace.png"); //load the asteroid sprite asteroid = new Sprite(this, g2d); asteroid.load("asteroid2.png"); gameloop = new Thread(this); gameloop.start(); } public void run() { Thread t = Thread.currentThread(); while (t == gameloop) { try { Thread.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); } //draw the background g2d.drawImage(background.getImage(), 0, 0, screenWidth-1, screenHeight-1, this); int width = screenWidth - asteroid.imageWidth() - 1; int height = screenHeight - asteroid.imageHeight() - 1; Point point = new Point(rand.nextInt(width), rand.nextInt(height)); asteroid.setPosition(point); asteroid.transform(); asteroid.draw(); repaint(); } } public void paint(Graphics g) { //draw the back buffer to the screen g.drawImage(backbuffer, 0, 0, this); } }
This significant chapter produced a monumental new version of Galactic War that is a foundation for the chapters to come. The final vestiges of the game’s vector-based roots have been discarded, and the game is now fully implemented with bitmaps. In this chapter, you learned:
How to create a new, powerful Sprite
class
How to detect sprite collision
How to write reusable methods and classes
The following questions will help you to determine how well you have learned the subjects discussed in this chapter. The answers are provided in Appendix A, “Chapter Quiz Answers.”
Use the following exercises to test your understanding of the material covered in this chapter.
The SpriteTest program demonstrates the use of the Sprite
class. Modify the program so that it draws multiple instances of the asteroid sprite on the screen, each moving and animating differently.
18.119.106.237