Chapter 6. Simple Sprites

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

Programming Simple Sprites

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.

Tip

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.

Basic Game Entities

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

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;
   }
}

Creating a Reusable Sprite Class

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.

Collision Testing

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.

Sprite Class Source Code

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); }
}

Tip

Animation is a feature missing from the Sprite class at this point; we will go over that subject in the next chapter.

Testing the Sprite Class

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.

The SpriteTest program demonstrates how to use the Sprite class.

Figure 6.1. The SpriteTest program demonstrates how to use the Sprite class.

// 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);
    }
}

   

What You Have Learned

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

Review Questions

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.”

1.

What is the name of the support class created in this chapter to help the Sprite class manage position and velocity?

2.

During which keyboard event should you disable a keypress variable, when detecting multiple key presses with global variables?

3.

What are the three types of parameters you can pass to the collidesWith () method?

4.

What Java class provides an alternate method for loading images that is not tied to the applet?

5.

Which Java package do you need to import to use the Graphics2D class?

6.

What numeric data type does the Point class use for internal storage of the X and Y values?

7.

What data types can the Point class work with at the constructor level?

8.

Which sprite property determines the angle at which the sprite will move?

9.

Which sprite property determines at which angle an image is pointed, regardless of movement direction?

10.

Which AffineTransform method allows you to translate, rotate, and scale a sprite?

On Your Own

Use the following exercises to test your understanding of the material covered in this chapter.

Exercise 1

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.

Exercise 2

Modify the SpriteTest program even further by adding collision testing, such that the asteroids will rebound off one another when they collide.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.83.28