Sunday, April 3, 2011

Moving Objects and Sprite Manager - Part 5

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


Partial drawing of image

Let's begin by recalling our sprite image.

bunny_sprite.png

by Moosader


The image is actually 128x128 pixels in size. And it is evenly divided into 16 regions. Each region is a partial image of 32x32 pixels. That composes 4 animation frames for each of the four moving directions.

By default, the drawImage() method of a Graphics2D object will draw the whole image. You may think of a Graphics2D object as a drawing board. The drawImage() API is in fact very flexible. It allows us to select partial region of the source image to be drawn.

Compare the following examples :
  1. gr.drawImage(spriteImage,50,50,this);
  2. gr.drawImage(spriteImage,50,50,82,82,0,0,32,32,this);
Both versions will draw the image at position(50,50) inside the graphics object(just think of it as a drawing board). But the second version contains more parameters. It is of the form :
gr.drawImage(spriteImage,dx1,dy1,dx2,dy2,sx1,sy1,sx2,sy2,imageObserver);
where (dx1,dy1,dx2,dy2) is the bounding rectangle in the destination (the drawing board)
(sx1,sy1,sx2,sy2) is the bounding rectangle of the source image.


Let's write a simple example to show the two drawImage() methods.

We just need to change the paintComponent() method in TransparentImage.java in the previous article.

// override the paint() method
  public void paintComponent(java.awt.Graphics gr)
  {
    // draw background
    gr.drawImage(backgroundImage,0,0,this);

    // draw whole image (128x128 pixels)
    gr.drawImage(spriteImage,50,200,this);
    
    // draw a partial image (32x32 pixels)
    int dx1=100,dy1=100,dx2=dx1+32,dy2=dx1+32;  // target position
    int sx1=0,sy1=0,sx2=sx1+32,sy2=sy1+32;      // source region
    gr.drawImage(spriteImage,dx1,dy1,dx2,dy2,sx1,sy1,sx2,sy2,this);
  }

And here comes the result.


Defining the source regions


The original source image actually contains 16 regions. For each directions there are four gestures for the moving sprite. We could pre-define those regions in advance using a 3 dimensional array.

int[][][] bunnyRect = // [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
    };


In the animation loop, we just need to select the correct region according to the moving direction and the animation gesture. The gesture number will be incremented while the bunny is moving. And it goes back to zero when gesture>3.

Running Bunny


In part 2 we have a bouncing ball moving in the sky, the following sample added a running bunny on the ground.

/******************************************************************************
* File : MovingSprite.java
* Author : http://java.macteki.com/
* Description :
*   A ball flying in the sky and a bunny running on the ground
*   required background.jpg and bunny_sprite.png
* Tested with : JDK 1.6
******************************************************************************/


import java.awt.image.BufferedImage;

class MovingSprite extends javax.swing.JPanel implements Runnable
{
  // image object for double buffering
  BufferedImage drawingBoard=new BufferedImage(300,300,BufferedImage.TYPE_INT_RGB);

  // image object for holding the background JPEG
  BufferedImage backgroundImage;
  
  BufferedImage bunnySprite;

  int xBall=100, yBall=100;   // initial coordinates of the moving ball
  int xVelocity=3;            // moving 3 pixels per frame (to the right)

  // initial coordinates and velocity of the bunny sprite
  int xBunny=50, yBunny=250;
  int xvBunny=2;

  public MovingSprite() throws Exception
  {
    int w=drawingBoard.getWidth(this);
    int h=drawingBoard.getHeight(this);
    this.setPreferredSize(new java.awt.Dimension(w,h));

    // read background image 
    String jpeg_file="background.jpg";
    backgroundImage = javax.imageio.ImageIO.read(new java.io.File(jpeg_file));

    // read the sprite image
    BufferedImage tmp = javax.imageio.ImageIO.read(new java.io.File("bunny_sprite.png"));
    bunnySprite = makeTransparent(tmp);
  }

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

 
  // override the paint() method, we don't count on the system.
  // We draw our own panel.
  public void paintComponent(java.awt.Graphics gr)
  {
    // Redraw the whole image instead of redrawing every object in the screen.
    // This technique is commonly known as "double buffering"
    gr.drawImage(drawingBoard,0,0,this);
  }  


  // start the bouncing thread
  public void start()  
  {
    new Thread(this).start();
  }

  // thread entry point
  public void run()
  {
    try
    {
      while (true)
      {
        redrawBackground(); // redraw the background
        moveBall();         // move the ball to a new position
        moveSprite();       // move the bunny sprite
        repaint();          // redraw the panel.
        Thread.sleep(30);   // delay for persistence of vision.
      }
    }
    catch (Exception e)
    {
      e.printStackTrace();
    }
  }


  // redraw the background image.
  // this effectively erase all sprites.
  public void redrawBackground()
  {
    java.awt.Graphics2D gr=(java.awt.Graphics2D) drawingBoard.getGraphics();
    gr.drawImage(backgroundImage,0,0,this);
  }  

  public void moveBall()
  { 

    int diameter=10;  // diameter of the ball

    // update ball position
    xBall+=xVelocity;
    if (xBall>=300-diameter || xBall<0) // hit border
    {
      xBall-=xVelocity;        // undo movement
      xVelocity=-xVelocity;    // change direction of velocity
    }

    // draw ball at new position
    java.awt.Graphics2D gr = (java.awt.Graphics2D) drawingBoard.getGraphics();
    gr.setColor(java.awt.Color.RED);  // this is the ball color
    java.awt.geom.Ellipse2D newCircle=
      new java.awt.geom.Ellipse2D.Double(xBall,yBall,diameter,diameter);

    gr.fill(newCircle);
  }

  // Yes, it is very ugly to declare variable here.
  // Don't worry, everything will be moved to a Sprite class soon.
  int animationFrame=0;   // incremented for every frame.
  int gesture=0;          // animation gesture (0-3)
  int relativeSpeed=2;    // update position at every 2 frames (moving half slower)
  int gestureWait=8;      // update animation gesture at every 8 frames

  // pre-defined source regions
    int[][][] bunnyRect = // [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 void moveSprite()
  {
    animationFrame++;    
    if (animationFrame % relativeSpeed==0) // control when to update the position
    {
 
      // move bunny to new position
      xBunny+=xvBunny;
      if (xBunny>=300-32 || xBunny<0) // hit border
      {
        xBunny-=xvBunny;      // undo movement
        xvBunny=-xvBunny;     // change direction of velocity
      }
    }

    if (animationFrame % gestureWait==0)  // control when to update gesture
    {
        gesture=(gesture+1)%4;
    }

    // draw bunny at new position
    java.awt.Graphics2D gr = (java.awt.Graphics2D) drawingBoard.getGraphics();

    int w=32, h=32; 
    int dx2=xBunny+w;
    int dy2=yBunny+h;
    int direction;
    if (xvBunny>0) direction=1; else direction=0;
    int[] srcBounds=bunnyRect[direction][gesture];
    int sx1=srcBounds[0], sy1=srcBounds[1], sx2=sx1+32, sy2=sy1+32;
    gr.drawImage(bunnySprite,xBunny,yBunny,dx2,dy2,sx1,sy1,sx2,sy2,this);
  }

  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 moving sprite");

    MovingSprite spritePanel=new MovingSprite();
    window.add(spritePanel);

    window.pack();
    window.setVisible(true);
    
    spritePanel.start();
  }
}


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



Next

We will have a sprite manager that handle thousands of sprites in the next section.

Part 5 completed. To be continued...

No comments:

Post a Comment