/*********************************************************************************
* TotalCross Software Development Kit *
* Copyright (C) 2000-2012 SuperWaba Ltda. *
* All Rights Reserved *
* *
* This library and virtual machine is distributed in the hope that it will *
* be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. *
* *
* This file is covered by the GNU LESSER GENERAL PUBLIC LICENSE VERSION 3.0 *
* A copy of this license is located in file license.txt at the root of this *
* SDK or can be downloaded here: *
* http://www.gnu.org/licenses/lgpl-3.0.txt *
* *
*********************************************************************************/
package totalcross.game;
import totalcross.sys.Settings;
import totalcross.ui.gfx.*;
import totalcross.ui.image.Image;
import totalcross.ui.image.ImageException;
/**
* This class implements a game Sprite. <br>
* <br>
* A sprite is a graphical object typically used in games that can be moved,
* with collision detection feature and background saving/restoring capability.<p>
* The sprite attributes are:<br>
* <p>
* <li>position<br>
* The sprite position <U>centerX,centerY</U> is in fact its center position.<br>
* <br>
* <li>size<br>
* <U>width/height</U> contains respectively the image horizontal/vertical dimensions.<br>
* <br>
* <li>half size<br>
* <U>halfWidth/halfHeight</U> contains respectively the half horizontal/vertical dimensions.<br>
* <br>
* <li>valid region<br>
* The valid region of a sprite are relative to the sprite center. The valid region
* can be provided in the Sprite <i>constructor</i> or by the <i>setRegion</i> call.
* Both are totalcross.ui.gfx.Rect objects. If null is passed in the constructor the default region
* is computed to prevent the sprite living even partially the screen.<br>
* Once set the region can be modified by incrementing/decrementing the values of
* <U>regionMinx</U>, <U>regionMiny</U>, <U>regionMaxx</U> and <U>regionMaxy</U><br>
* <br>
* <li>draw operation<br>
* The default value of <U>drawOp</U> is DRAW_PAINT
* if its value is DRAW_PAINT the image is copied "as is" to the buffer, and<br>
* if its value is DRAW_SPRITE the transparency color specified in the <i>constructor</i>
* is used to prevent the copy of image pixels of this color in order to preserve the
* background.
* <p>
* You can find a complete game API sample named 'Scape' in the TotalCross samples folder.<br>
* Here is some sample code:
*
* <pre>
* // Ball is a the Sprite bouncing on the screen.
* // The ball bounces on the left, the top and the bottom border and
* // must be hit by the racket on the right border to keep the ball inside the screen.
* // If the racket misses the ball, the game is over.
*
* <i>public final class Ball extends Sprite</i> {
*
* // random generator for the starting speeds
* private Random rand=new Random();
*
* // keep a reference to the game mainclass for data access & game control.
* private Ping game;
*
* // other important sprite the ball sprite must know about for interaction...
* private Racket racket;
*
* // ball movement speed in both directions in double values for acceleration precision
* private double speedx,speedy;
*
* // and in integer format for quicker move computing.
* private int ispeedx,ispeedy;
*
* //----------------------------------------------------------------
* // Ball constructor.
* // @param game the Ping game mainclass.
* // @param racket Sprite that interacts with the ball.
* //----------------------------------------------------------------
*
* <i>public Ball(Ping game,Racket racket)</i> {
*
* // setup the sprite by loading the bitmap, defining the transparency color.
* // The ball element does not fill the whole bitmap, the corners are filled
* // with WHITE pixels. By setting WHITE as the transparency color and selecting the
* // DRAW_SPRITE draw mode, the bitmap white pixels are not shown and the background
* // is not affected by the bitmap display. The background saving feature is disabled
* // because the whole screen is cleared and redrawn at each frame. No valid region
* // (null) means, default valid region is right. Except that the miny value must be
* // incremented to prevent the ball hiding the s
*
* super(new Image("ball.bmp"),Color.WHITE,false,null);
* drawOp=Graphics.DRAW_SPRITE;
*
* // the ball may go out of the window when the user misses it
* doClip = true;
*
* // by default the valid area of a sprite is the screen.
* // reduce the playfield of the racket by the level & score display zone
* regionMiny+=Ping.GAME_MINY;
*
* this.game=game;
* this.racket=racket;
*
* // initialize the ball
* reinit();
* }
*
* // initialization needed each time a game starts.
* // The racket is placed in the middle of the right border and the ball
* // is placed next to it.
* // Also defines the initial ball speed.
*
* <i>public void reinit()</i> {
*
* Coord pos=racket.getBallPosition(halfWidth);
* setPos(ballHitX=pos.x,pos.y,false);
*
* speedx=4+rand.nextFloat()*6.0f;
* speedy=2+rand.nextFloat()*3.0f;
* ispeedx=-1;
* ispeedy=-1;
*
* // the toward position specified in the towardPos call is not a
* // direction but an ending position, so speed is irrelevant.
*
* speed=1000;
* ...
* }
*
* //----------------------------------------------------------------
* // Move the ball to next position depending on the current speed.
* //----------------------------------------------------------------
*
* <i>public void move()</i> {
*
* // add the speed vector and enable position validation.
* // see the "Scape" sample code for more details about position validation.
* // this towardPos() moves the sprite toward a point, the third parameter
* // enables position validation which is needed for each position change to
* // precisely detect the racket or the borders hits.
* // This call could be optimized to disable the time consuming position
* // validation when the ball is not near the screen border.
*
* towardPos(centerX+ispeedx,centerY+ispeedy,true);
* }
* </pre>
* @author Frank Diebolt
* @author Guilherme Campos Hazan
* @version 1.1
*/
public class Sprite
{
/**
* Sprite center position.
*/
public int centerX, centerY;
/**
* Sprite width/height dimensions. <b>READ-ONLY</b> attributes.
*/
public int width, height;
/**
* Sprite valid region. <br>
* The sprite cannot leave this area if the default position validation function is not overriden.
*
* @see #onPositionChange
*/
protected int regionMinx, regionMiny, regionMaxx, regionMaxy;
/**
* Sprite image.
*/
public Image image;
/**
* Graphics to draw at.
*/
protected Graphics gfx;
/**
* Surface to draw at.
*/
protected GfxSurface surface;
private boolean multiFrame;
/**
* Speed in pixels used by the function towardPos.
*
* @see #towardPos
*/
public int speed = 8;
// cached double buffering with screen erasing
protected boolean screenErased;
/** set to false if this sprite never reaches the screen boundaries. Default is true. */
public boolean doClip = true;
protected final static int INVALID = -500;
/**
* Sprite constructor. <br>
*
* @param image
* sprite image.
* @param transColor
* sprite's transparency color or -1 if none<br>
* (needed in DRAW_SPRITE mode to keep the current background).
* @param saveBckgd
* true if the background should be saved each time the sprite is drawn to restore it once the sprite
* moves.
* @param region
* defines the sprite valid area.<br>
* If null, a default region is set to prevent the sprite to leave even partially the screen.
* @throws ImageException
* @throws IllegalStateException
* @throws IllegalArgumentException
*/
public Sprite(Image image, int transColor, boolean saveBckgd, Rect region) throws IllegalArgumentException, IllegalStateException, ImageException
{
this(image, image.getFrameCount(), transColor, saveBckgd, region);
}
/**
* Sprite constructor. <br>
*
* @param image
* sprite image.
* @param transColor
* sprite's transparency color or -1 if none<br>
* (needed in DRAW_SPRITE mode to keep the current background).
* @param saveBckgd
* true if the background should be saved each time the sprite is drawn to restore it once the sprite
* moves.
* @param region
* defines the sprite valid area.<br>
* If null, a default region is set to prevent the sprite to leave even partially the screen.
* @throws ImageException
* @throws IllegalStateException
* @throws IllegalArgumentException
*/
public Sprite(Image image, int nrFrames, int transColor, boolean saveBckgd, Rect region) throws IllegalArgumentException, IllegalStateException, ImageException
{
gfx = GameEngineMainWindow.getEngineGraphics();
surface = GameEngineMainWindow.getSurface();
image.setFrameCount(nrFrames);
screenErased = true; /* GameEngineMainWindow.engine.gameIsDoubleBuffered */;
multiFrame = image.getFrameCount() > 1;
this.image = image;
width = image.getWidth();
height = image.getHeight();
if (width <= 0 || height <= 0)
throw new GameEngineException("bad Sprite bitmap");
saveBackground(saveBckgd);
if (region == null)
{
regionMinx = width / 2;
regionMiny = height / 2;
regionMaxx = Settings.screenWidth - regionMinx;
regionMaxy = Settings.screenHeight - regionMiny;
}
else
setRegion(region);
}
/**
* Background image.
*/
protected Image background;
/**
* Background graphic context.
*/
protected Graphics bgGfx;
/**
* Background restoring position.
*/
protected int bgX, bgY;
/**
* Enable/disable background saving. <br>
*
* @param enable
* true if background have to be saved and restored at each drawing.
* @throws ImageException
*/
private void saveBackground(boolean enable) throws ImageException
{
if (enable && !Settings.isOpenGL)
{
background = new Image(width, height);
bgGfx = background.getGraphics();
}
else if (background != null)
{
bgGfx = null;
background = null;
}
bgX = INVALID;
}
/**
* Retrieve the sprite valid region. <br>
* <B>NOTE</B>: should be called cautiously due to a Rect object alloc.
*
* @return positions validity region
*/
public final Rect getRegion()
{
return new Rect(regionMinx, regionMiny, regionMaxx - regionMinx + 1, regionMaxx - regionMiny + 1);
}
/**
* Change the sprite valid region.
*
* @param region new positions validity area
*/
public final void setRegion(Rect region)
{
regionMinx = region.x;
regionMiny = region.x;
regionMaxx = region.x2();
regionMaxy = region.y2();
}
/**
* Default position validation function. <br>
* This function can be overloaded to define your own sprite position validation or collision detection.<br>
* The overloaded function may use the sprite data members and should update the sprite position centerX and centerY
* if needed.
*
* @return true if the (centerX,centerY) is a valid position, false if it's not and the position has been corrected.<br>
* <B>NOTE</B>: false returns will stop the movements of the towardPos function.
* @see #towardPos
*/
public boolean onPositionChange()
{
boolean b = true;
if (centerX < regionMinx)
{
centerX = regionMinx;
b = false;
}
else
if (centerX > regionMaxx)
{
centerX = regionMaxx;
b = false;
}
if (centerY < regionMiny)
{
centerY = regionMiny;
b = false;
}
else
if (centerY > regionMaxy)
{
centerY = regionMaxy;
b = false;
}
return b;
}
/**
* Sets the sprite position (actually its center). <br>
* Note: if doValidate is false, you may consider just setting centerX and centerY attributes directly (it is 60%
* faster).
*
* @param x position.
* @param y position.
* @param doValidate if true the position is validated which means that the onPositionChange() function is called.
* @return true if the defined position has been set
* @see #onPositionChange
*/
public final boolean setPos(int x, int y, boolean doValidate)
{
centerX = x;
centerY = y;
if (doValidate)
return onPositionChange();
return true;
}
/**
* Retrieve the sprite position (actually its center). <br>
* <B>NOTE</B>: should be called cautiously due to a Coord object alloc. You may also consider accessing the centerX
* and centerY directly.
*
* @return sprite's center position
*/
public final Coord getPos()
{
return new Coord(centerX, centerY);
}
/**
* Moves the sprite toward the specified position. <br>
* This function is typically used when the user points the end position with the pen. The object position is
* computed to move smoothly from it's current position to the target position.
*
* @param x position.
* @param y position.
* @param doValidate if true the position is validated which means that the onPositionChange() function is called.<br>
* <B>NOTE</B>: a false return of onPositionChange() will stop the towardPos function.
* @return true if the defined position has been set
* @see #onPositionChange
*/
public boolean towardPos(int x, int y, boolean doValidate)
{
int dx = x - centerX;
int dy = y - centerY;
int steps;
if (dx == 0) // vertical move
{
steps = Math.min(dy >= 0 ? dy : -dy, speed);
if (dy < 0)
centerY -= steps;
else
if (dy > 0)
centerY += steps;
if (doValidate)
return onPositionChange();
}
else
if (dy == 0) // horizontal move
{
steps = Math.min(dx >= 0 ? dx : -dx, speed);
if (dx < 0)
centerX -= steps;
else
if (dx > 0)
centerX += steps;
if (doValidate)
return onPositionChange();
}
else
{
// diagonal moves
// derived from TOTALCROSS drawLine algorithm, thx to Guich.
// It's Bresenham's fastest implementation!
dx = dx >= 0 ? dx : -dx; // store the change in X and Y of the line endpoints
dy = dy >= 0 ? dy : -dy;
int CurrentX = centerX; // store the starting point (just point A)
int CurrentY = centerY;
// DETERMINE "DIRECTIONS" TO INCREMENT X AND Y (REGARDLESS OF DECISION)
int Xincr = (centerX > x) ? -1 : 1; // which direction in X?
int Yincr = (centerY > y) ? -1 : 1; // which direction in Y?
// DETERMINE INDEPENDENT VARIABLE (ONE THAT ALWAYS INCREMENTS BY 1 (OR -1) )
// AND INITIATE APPROPRIATE LINE DRAWING ROUTINE (BASED ON FIRST OCTANT
// ALWAYS). THE X AND Y'S MAY BE FLIPPED IF Y IS THE INDEPENDENT VARIABLE.
steps = speed;
if (dx >= dy) // if X is the independent variable
{
int dPr = dy << 1; // amount to increment decision if right is chosen (always)
int dPru = dPr - (dx << 1); // amount to increment decision if up is chosen
int P = dPr - dx; // decision variable start value
for (; dx >= 0 && steps > 0; dx--) // process each point in the line one at a time (just use dX)
{
centerX = CurrentX; // update the sprite's position
centerY = CurrentY;
if (doValidate && !onPositionChange())
return false;
CurrentX += Xincr; // increment independent variable
steps--;
if (P > 0) // is the pixel going right AND up?
{
CurrentY += Yincr; // increment dependent variable
steps--;
P += dPru; // increment decision (for up)
}
else
// is the pixel just going right?
P += dPr; // increment decision (for right)
}
}
else
// if Y is the independent variable
{
int dPr = dx << 1; // amount to increment decision if right is chosen (always)
int dPru = dPr - (dy << 1); // amount to increment decision if up is chosen
int P = dPr - dy; // decision variable start value
for (; dy >= 0 && steps > 0; dy--) // process each point in the line one at a time (just use dY)
{
centerX = CurrentX; // update the sprite's position
centerY = CurrentY;
if (doValidate && !onPositionChange())
return false;
CurrentY += Yincr; // increment independent variable
steps--;
if (P > 0) // is the pixel going up AND right?
{
CurrentX += Xincr; // increment dependent variable
steps--;
P += dPru; // increment decision (for up)
}
else
// is the pixel just going up?
P += dPr; // increment decision (for right)
}
}
}
return true;
}
/**
* Draw the sprite at it's current position using the defined drawOp. <br>
* If the Sprite has been created with enabled background saving (usefull only when the screen is not cleared before
* the new frame display) the previously stored background will be restored first and the sprite is displayed in a
* second step.<br>
* <B>NOTE</B>: if several sprite moves simultaneously and may overlap the background restoring may erase a
* previously drawn sprite. In this case, the only solution is to call explicitly the hide() function on all the
* sprites in the DRAWN REVERSE ORDER.<br>
* By doing so, you always restore a clean image without sprites before starting a new draw cycle of all the sprites.
*/
public void show()
{
int w2 = width >> 1;
int h2 = height >> 1;
// are we in background restoring mode ?
if (!Settings.isOpenGL && background != null)
{
// Is it not the first paint ?
if (bgX != INVALID)
{
// position didn't change, what are we doing here ?
if (!screenErased && bgX == centerX && bgY == centerY)
return;
// buffer -> screen
gfx.copyRect(background, 0, 0, width, height, bgX - w2, bgY - h2);
}
// screen -> buffer
bgX = centerX;
bgY = centerY;
bgGfx.copyRect(surface, bgX - w2, bgY - h2, width, height, 0, 0);
}
// copy the sprite image to the graphic context
gfx.drawImage(image, centerX - w2, centerY - h2, doClip);
if (multiFrame)
image.nextFrame();
}
/**
* Restore the sprite's saved background. <br>
* <B>NOTE</B>: the position may have change, the saved background position has been memorized also, to restore it at
* the right place.
*/
public void hide()
{
if (background == null || bgX == INVALID)
return;
gfx.copyRect(background, 0, 0, width, height, bgX - width / 2, bgY - height / 2); // buffer -> screen
bgX = INVALID;
}
/**
* Test if the sprite collides with another one. <br>
*
* @param s sprite to test with
* @return true if both sprite overlaps, false otherwise
*/
public boolean collide(Sprite s)
{
int w2 = width >> 1;
int h2 = height >> 1;
int sw2 = s.width >> 1;
int sh2 = s.height >> 1;
return !((s.centerX + sw2 <= centerX - w2) || (s.centerY + sh2 <= centerY - h2) || (s.centerX - sw2 >= centerX + w2) || (s.centerY - sh2 >= centerY + h2));
}
}