Sunday, April 3, 2011

Moving Objects and Sprite Manager - Part 6

This is part 6 of the series. It is suggested to read the whole series from the beginning.

Sprite Manager


If you have only one sprite, you may not need a sprite manager. Sprite Manager is a program that handle multiple sprites. With today computer power, it is possible to make animation or game with hundreds or even thousands of moving sprites using a high level language like Java.


Encapsulation

Encapsulation is a information hiding principle in object oriented programming. A Sprite is a good candidate for encapsulation. We will define the Sprite class in a top down approach. First we define the attributes of the sprite.

class Sprite 
{
  public BufferedImage image = null;   // image of the sprite

  // initial coordinates and velocity of the sprite
  public int x=50, y=250;
  private int xv=2;
  private int yv=0;
  
  private int animationFrame=0;   // incremented for every frame.
  private int gesture=0;          // animation gesture (0-3)
  public  int relativeSpeed=2;    // update position at every 2 frames (moving half slower)
  public  int gestureWait=8;      // update animation gesture at every 8 frames
}

Using too many public variables is considered bad programming practices. But as I said, this site is focused on simplicity and readability. Using too many getters and setters will mess up the source code. I am making a demonstration for programmers, not creating a text book for university students. By the way, you may always convert the public variables to private and create getters and setters for them if you wish.

This class is going to handle 128x128 pixels sprite only. And the sprite must be evenly divided into 16 regions of 32x32 pixels.

// pre-defined source regions
  private int[][][] frameRect = // [direction][gesture][x,y,w,h]
    {
    {{ 0, 0,32,32}, {32, 0,32,32}, {64, 0,32,32}, {96, 0,32,32} },  // left
    {{ 0,32,32,32}, {32,32,32,32}, {64,32,32,32}, {96,32,32,32} },  // right
    {{ 0,64,32,32}, {32,64,32,32}, {64,64,32,32}, {96,64,32,32} },  // down
    {{ 0,96,32,32}, {32,96,32,32}, {64,96,32,32}, {96,96,32,32} }   // up
    };  

How about sprite of other sizes ? You may extends the Sprite class. An example is given below.
// handle sprite with 256x256 pixels, composed of 16 regions with 64x64 pixels.
class BigSprite extends Sprite
{

  // pre-defined source regions
  private int[][][] frameRect = // [direction][gesture][x,y,w,h]
    {
    {{ 0,  0,64,64}, {64,  0,64,64}, {128,  0,64,64}, {192,  0,64,64} },  // left
    {{ 0, 64,64,64}, {64, 64,64,64}, {128, 64,64,64}, {192, 64,64,64} },  // right
    {{ 0,128,64,64}, {64,128,64,64}, {128,128,64,64}, {192,128,64,64} },  // down
    {{ 0,192,64,64}, {64,192,64,64}, {128,192,64,64}, {192,192,64,64} }   // up
    };  

  public int[] getImageBound(int direction, int gesture)
  {
    return frameRect[direction][gesture];
  }

}

Artificial Intelligence

Simple intelligence will be given to the bunny sprite. It is defined in the move() method of the Sprite class. It follows two simple rules :

  1. It changes direction if it hit the border of the active region.
  2. It changes direction if it feels bored. It has 0.5% chance of feeling bored while moving.

What is active region ? That depends on the context. For the bunny sprite, it should be running on the ground but not flying in the sky, therefore the active region is the lower half of the background.

You may extend the Sprite class and override the move() method to include more sophisticated intelligence.

List of Sprites

Sprite Manger manges multiple sprite. It naturally forms a list of sprite and the data structure java.util.ArrayList is a good choice for holding the sprites. Adding and deleting sprite can be done with standard ArrayList API. For the animation part, we need to loop over all sprite in an animation frame.

// spriteList is of type java.util.ArrayList
  private void animateFrame()
  {
    java.awt.Graphics2D gr=(java.awt.Graphics2D) drawingBoard.getGraphics();
    gr.drawImage(backgroundImage,0,0,this);

    for (int i=0;i<spriteList.size();i++)
    {      
      Sprite sprite=spriteList.get(i);
      sprite.move();
      sprite.draw(gr);
    }
  }

The move() method and draw() method of the Sprite class is straight forward. It is not difficult to understand if you have read the complete series of the articles. Details will be presented in Sprite.java in the source code section.

The spriteList is initialized as follows :

// add 50 bunnies sprites to the sprite list  
  public void initSpriteList() throws Exception
  {
    BufferedImage image = javax.imageio.ImageIO.read(new java.io.File("bunny_sprite.png"));
 
    java.util.Random random=new java.util.Random();
    for (int i=0;i<50;i++)
    {
      Sprite sprite=new Sprite(image);
      sprite.x=50+16*random.nextInt(20);
      sprite.y=250+4*random.nextInt(20);
      sprite.relativeSpeed=1+(i%3);   // range from 1 to 4
      sprite.gestureWait=8;           // change it to 1 to see what happen
      spriteList.add(sprite);
    }

  }

The above method adds 50 sprites to the list. As I said, the sprite manager is capable of handling thousands of sprites. So you may change the value from 50 to 1000 to see what happened.

Demonstration

The background image in the previous articles is too small. We need a bigger background now otherwise it would be too crowded for hundreds of bunnies. Here is the background we used in this demonstration.

Save the above image as "background.jpg"

The bunny sprite is same as the previous articles.

bunny_sprite.png

The result of the demonstration is as follows. Note that the bunnies are actually running in different direction at different speed, of course I cannot show this in a static photo.

Source code

/******************************************************************************
* File : SpriteManager.java
* Author : http://java.macteki.com/
* Description :
*   Sprite Manager.
*   required Sprite.java, background.jpg, bunny_sprite.png
* Tested with : JDK 1.6
******************************************************************************/
 
import java.awt.image.BufferedImage;
 
class SpriteManager extends javax.swing.JPanel implements Runnable
{
  // list of sprite
  java.util.ArrayList<Sprite> spriteList=new java.util.ArrayList<Sprite>();
 
  // image object for double buffering
  private BufferedImage drawingBoard;
 
  BufferedImage backgroundImage;
 
  private boolean running=false;  // animation thread
 
  public SpriteManager() throws Exception
  {
 
    // read background image 
    String jpeg_file="background.jpg";
    backgroundImage = javax.imageio.ImageIO.read(new java.io.File(jpeg_file));
 
    // set panel size to dimension of background image
    int w=backgroundImage.getWidth(this);
    int h=backgroundImage.getHeight(this);
    setPreferredSize(new java.awt.Dimension(w,h));
 
    // resize the drawing board to fit the background
    drawingBoard=new BufferedImage(w,h,BufferedImage.TYPE_INT_RGB);
 
  }
 
  // add 50 bunnies sprites to the sprite list  
  public void initSpriteList() throws Exception
  {
    BufferedImage image = javax.imageio.ImageIO.read(new java.io.File("bunny_sprite.png"));
 
    java.util.Random random=new java.util.Random();
    for (int i=0;i<50;i++)
    {
      Sprite sprite=new Sprite(image);
      sprite.x=50+16*random.nextInt(20);
      sprite.y=250+4*random.nextInt(20);
      sprite.relativeSpeed=1+(i%3);   // range from 1 to 4
      sprite.gestureWait=8;           // change it to 1 to see what happen
      spriteList.add(sprite);
    }

  }
 
  // override the paint() method
  public void paintComponent(java.awt.Graphics gr)
  {
    gr.drawImage(drawingBoard,0,0,this);
  }
 
  // animation thread
  public void run()
  {
    while (running)
    {
      try {
        animateFrame();
        repaint();
        Thread.sleep(30);
      } catch (Exception e)
      {
        e.printStackTrace();
      }
    }
  }
 
  private void animateFrame()
  {
    java.awt.Graphics2D gr=(java.awt.Graphics2D) drawingBoard.getGraphics();
    gr.drawImage(backgroundImage,0,0,this);
 
    for (int i=0;i<spriteList.size();i++)
    {      
      Sprite sprite=spriteList.get(i);
      sprite.move();
      sprite.draw(gr);
    }
 
  }
 
  public void start() throws Exception
  {
    initSpriteList();
    running=true;
    new Thread(this).start();
  }
 
  public void stop()
  {
    running=false;
  }
 
 
 
  public static void main(String[] args) throws Exception
  {
    javax.swing.JFrame window=new javax.swing.JFrame();
    window.setDefaultCloseOperation(javax.swing.JFrame.EXIT_ON_CLOSE);
    window.setTitle("Macteki Sprite Manager");
 
    SpriteManager manager=new SpriteManager();
    window.add(manager);
    window.pack();
 
    window.setVisible(true);
 
    manager.start();
  }
  
}

Sprite.java

/******************************************************************************
* File : Sprite.java
* Author : http://java.macteki.com/
* Description :
*   The sprite object
* Tested with : JDK 1.6
******************************************************************************/
 
import java.awt.image.BufferedImage;
 
class Sprite 
{
  public BufferedImage image=null;   // image of the sprite
 
  // initial coordinates and velocity of the sprite
  public int x=50, y=250;
  private int xv=2;
  private int yv=0;
  
  private int animationFrame=0;   // incremented for every frame.
  private int gesture=0;          // animation gesture (0-3)
  public  int relativeSpeed=2;    // update position at every 2 frames (moving half slower)
  public  int gestureWait=8;      // update animation gesture at every 8 frames
 
  java.util.Random random=new java.util.Random();
 
  // pre-defined source regions
  private int[][][] frameRect = // [direction][gesture][x,y,w,h]
    {
    {{ 0, 0,32,32}, {32, 0,32,32}, {64, 0,32,32}, {96, 0,32,32} },  // left
    {{ 0,32,32,32}, {32,32,32,32}, {64,32,32,32}, {96,32,32,32} },  // right
    {{ 0,64,32,32}, {32,64,32,32}, {64,64,32,32}, {96,64,32,32} },  // down
    {{ 0,96,32,32}, {32,96,32,32}, {64,96,32,32}, {96,96,32,32} }   // up
    };  
 
 

  public Sprite(BufferedImage img)
  {
    this.image = makeTransparent(img);
  }

  public BufferedImage makeTransparent(BufferedImage tmpImage)
  {
    int h=tmpImage.getHeight(null);
    int w=tmpImage.getWidth(null);
 
    BufferedImage resultImage=new BufferedImage(w,h,BufferedImage.TYPE_INT_ARGB);
 
    // assume the upperleft corner of the original image is a transparent pixel
    int transparentColor=tmpImage.getRGB(0,0);  
    for (int y=0;y<h;y++)
      for (int x=0;x<w;x++)
      {
        int color=tmpImage.getRGB(x,y);
        if (color==transparentColor) color=color & 0x00FFFFFF; // clear the alpha flag
        resultImage.setRGB(x,y,color);
      }
 
    return resultImage;
  }
 
  // activeRegion is defined by corners coordinates (x1,y1) and (x2,y2)
  public int[] getActiveRegion()
  {
    // upperLeft corner(x,y),  lowerRight corner(x,y)
    return new int[]{ 0,250,750,440};  // lower area of the screen.
  }

  public void move()
  {
    animationFrame++;    
    if (animationFrame % relativeSpeed==0) // control when to update the position
    {
      int bored=random.nextInt(200);
      if (bored==1) // the sprite is bored for 0.5% of the time
      {
        // if it feels bored, it changes direction
        int[] xvTable={-2,2,0,0};
        int[] yvTable={0,0,2,-2}; 
        int dir = random.nextInt(4);
        xv=xvTable[dir];  yv=yvTable[dir];
      }
 
      // move sprite to new position
      x+=xv;
      y+=yv;
 
      // get Sprite dimension
      int w=frameRect[0][0][2];  // 32
      int h=frameRect[0][0][3];  // 32

      int rightBound=getActiveRegion()[2];
      int leftBound=getActiveRegion()[0];
      int upperBound=getActiveRegion()[1];
      int lowerBound=getActiveRegion()[3];
       
      if (x>rightBound-w || x<leftBound) // hit border
      {
        x-=xv;      // undo movement
        xv=-xv;     // change direction of velocity
      }
 
      if (y>lowerBound-h || y<upperBound) // don't move too high or too low
      {
        y-=yv;      // undo movement
        yv=-yv;     // change direction of velocity
      }
 
    }
 
    if (animationFrame % gestureWait==0)  // control when to update gesture
    {
        gesture=(gesture+1)%4;
    }
    
  }
 
  public int[] getImageBound(int direction, int gesture)
  {
    return frameRect[direction][gesture];
  }
 
  public void draw(java.awt.Graphics2D gr)
  {
    // draw sprite at new position
 
    int direction=0;
    if (xv>0) direction=1; else if (xv<0) direction=0;
    if (yv>0) direction=2; else if (yv<0) direction=3;
    int[] srcBounds=getImageBound(direction,gesture);
    int w=srcBounds[2], h=srcBounds[3]; 
    int dx2=x+w;
    int dy2=y+h;
    int sx1=srcBounds[0], sy1=srcBounds[1], sx2=sx1+w, sy2=sy1+h;
    gr.drawImage(image,x,y,dx2,dy2,sx1,sy1,sx2,sy2,null);
 
  }
 
}

Acknowledgement

  1. bunny_sprite.png is provided by Moosader :
    http://moosader.deviantart.com/art/Public-domain-bunny-sprite-151156167

  2. background.jpg is provided by Jesccia Crabtree
    http://www.jessicacrabtree.com/journal1/public-domain-nature-photos

Conclusion

This article provides a simple yet flexible Sprite Manager. If you understand it well enough, you could override the Sprite class and make modifications to the SpriteManager to do really interesting things. The first thing you should try is adding more Sprites to the spriteList. You could add up to 1000 sprites without noticeable performance degradation. If you want 3000 sprites, you may need to increase the heap size. Run the program with the following parameter if memory error occurs :

java -Xmx128m SpriteManager

No comments:

Post a Comment