Files
narya/src/java/com/threerings/media/sprite/Sprite.java
T
Walter Korman 1c6e066444 Allow sprite sub-classes to modify their render offset more easily.
git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@513 542714f4-19e9-0310-aa3c-eee0fc999fb1
2001-10-22 18:13:09 +00:00

529 lines
14 KiB
Java

//
// $Id: Sprite.java,v 1.26 2001/10/22 18:13:09 shaper Exp $
package com.threerings.media.sprite;
import java.awt.*;
import java.util.ArrayList;
import java.util.Iterator;
import com.threerings.media.Log;
/**
* The sprite class represents a single moveable object in an animated
* view. A sprite has a position within the view, and a set of images
* used to render it (perhaps multiple frames for animation).
*/
public class Sprite
{
/** The number of distinct directions. */
public static final int NUM_DIRECTIONS = 8;
/** Direction constants. */
public static final int DIR_NONE = -1;
public static final int DIR_SOUTHWEST = 0;
public static final int DIR_WEST = 1;
public static final int DIR_NORTHWEST = 2;
public static final int DIR_NORTH = 3;
public static final int DIR_NORTHEAST = 4;
public static final int DIR_EAST = 5;
public static final int DIR_SOUTHEAST = 6;
public static final int DIR_SOUTH = 7;
/** String translations for the direction constants. */
public static String[] XLATE_DIRS = {
"Southwest", "West", "Northwest", "North", "Northeast",
"East", "Southeast", "South"
};
/** Default frame rate. */
public static final int DEFAULT_FRAME_RATE = 15;
/** Animation mode indicating no animation. */
public static final int NO_ANIMATION = 0;
/** Animation mode indicating movement cued animation. */
public static final int MOVEMENT_CUED = 1;
/** Animation mode indicating time based animation. */
public static final int TIME_BASED = 2;
/**
* Construct a sprite object.
*
* @param x the sprite x-position in pixels.
* @param y the sprite y-position in pixels.
* @param frames the multi-frame image used to display the sprite.
*/
public Sprite (int x, int y, MultiFrameImage frames)
{
init(x, y, frames);
}
/**
* Construct a sprite object without any associated frames. The sprite
* should be populated with a set of frames used to display it via a
* subsequent call to {@link #setFrames}.
*
* @param x the sprite x-position in pixels.
* @param y the sprite y-position in pixels.
*/
public Sprite (int x, int y)
{
init(x, y, null);
}
/**
* Returns the sprite's x position in screen coordinates.
*/
public int getX ()
{
return _x;
}
/**
* Returns the sprite's y position in screen coordinates.
*/
public int getY ()
{
return _y;
}
/**
* Returns the sprite's width in pixels.
*/
public int getWidth ()
{
return _rbounds.width;
}
/**
* Returns the sprite's height in pixels.
*/
public int getHeight ()
{
return _rbounds.height;
}
/**
* Returns the sprite's rendered bounds in pixels.
*/
public Rectangle getBounds ()
{
return _rbounds;
}
/**
* Moves the sprite to the specified location.
*
* @param x the x-position in pixels.
* @param y the y-position in pixels.
*/
public void setLocation (int x, int y)
{
// invalidate our current position
invalidate();
// move ourselves
_x = x;
_y = y;
// we need to update our draw position which is based on the size
// of our current frame
updateRenderOrigin();
// invalidate our new position
invalidate();
}
/**
* Sprites have an orientation in one of the eight cardinal
* directions: <code>DIR_NORTH</code>, <code>DIR_NORTHEAST</code>,
* etc. Sprite derived classes can choose to override this member
* function and select a different set of images based on their
* orientation, or they can ignore the orientation information.
*/
public void setOrientation (int orient)
{
_orient = orient;
}
/**
* Returns the sprite's orientation as one of the eight cardinal
* directions: <code>DIR_NORTH</code>, <code>DIR_NORTHEAST</code>,
* etc.
*/
public int getOrientation ()
{
return _orient;
}
/**
* Initialize the sprite object with its variegated parameters.
*/
protected void init (int x, int y, MultiFrameImage frames)
{
_x = x;
_y = y;
updateRenderOrigin();
// initialize frame animation member data
_frameIdx = 0;
_animMode = NO_ANIMATION;
_frameDelay = 1000L/DEFAULT_FRAME_RATE;
setFrames(frames);
invalidate();
}
/**
* Called by the sprite manager when a sprite is added to said manager
* for management.
*
* @param mgr the sprite manager.
*/
protected void setSpriteManager (SpriteManager mgr)
{
_spritemgr = mgr;
}
/**
* Paint the sprite to the specified graphics context.
*
* @param gfx the graphics context.
*/
public void paint (Graphics2D gfx)
{
gfx.drawImage(_frame, _rbounds.x, _rbounds.y, null);
}
/**
* Paint the sprite's path, if any, to the specified graphics context.
*
* @param gfx the graphics context.
*/
public void paintPath (Graphics2D gfx)
{
if (_path != null) {
_path.paint(gfx);
}
}
/**
* Returns whether the sprite is inside the given shape in
* pixel coordinates.
*/
public boolean inside (Shape shape)
{
return shape.contains(_x, _y);
}
/**
* Returns whether the sprite's drawn rectangle intersects the given
* shape in pixel coordinates.
*/
public boolean intersects (Shape shape)
{
return shape.intersects(_rbounds);
}
/**
* Sets the animation mode for this sprite. The available modes are:
*
* <ul>
* <li><code>TIME_BASED</code>: cues the animation based on a target
* frame rate (specified via {@link #setFrameRate}).
* <li><code>MOVEMENT_CUED</code>: ticks the animation to the next
* frame every time the sprite is moved along its path.
* <li><code>NO_ANIMATION</code>: disables animation.
* </ul>
*
* @param mode the desired animation mode.
*/
public void setAnimationMode (int mode)
{
_animMode = mode;
}
/**
* Sets the number of frames per second desired for the sprite
* animation. This is only used when the animation mode is
* <code>TIME_BASED</code>.
*
* @param fps the desired frames per second.
*/
public void setFrameRate (int fps)
{
_frameDelay = 1000L/fps;
}
/**
* Set the image array used to render the sprite.
*
* @param frames the sprite images.
*/
public void setFrames (MultiFrameImage frames)
{
if (frames == null) {
Log.warning("Someone set up us the null frames! " +
"[sprite=" + this + "].");
return;
}
_frames = frames;
_frameIdx %= _frames.getFrameCount();
_frame = _frames.getFrame(_frameIdx);
// determine our drawing offsets and rendered rectangle size
if (_frame == null) {
_rbounds.width = 0;
_rbounds.height = 0;
} else {
_rbounds.width = _frame.getWidth(null);
_rbounds.height = _frame.getHeight(null);
}
updateRenderOffset();
updateRenderOrigin();
invalidate();
}
/**
* Set the sprite's active path and start moving it along its
* merry way. If the sprite is already moving along a previous
* path the old path will be lost and the new path will begin to
* be traversed.
*
* @param path the path to follow.
*/
public void move (Path path)
{
// save our path
_path = path;
// and initialize it
_path.init(this, System.currentTimeMillis());
}
/**
* Cancels any path that the sprite may currently be moving along.
*/
public void cancelMove ()
{
// TODO: make sure we come to a stop on a full coordinate,
// even in the case where we aborted a path mid-traversal.
_path = null;
}
/**
* Called by the active path when it begins.
*/
protected void pathBeginning ()
{
// nothing for now
}
/**
* Called by the active path when it has completed.
*/
protected void pathCompleted ()
{
// inform observers that we've finished our path
notifyObservers(new PathCompletedEvent(this, _path));
// we no longer want to keep a reference to this path
_path = null;
}
/**
* Returns the bounds that will be drawn upon when this sprite is
* rendered.
*/
public Rectangle getRenderedBounds ()
{
return _rbounds;
}
/**
* Invalidate the sprite's display rectangle for later repainting.
*/
public void invalidate ()
{
if (_frame == null) {
return;
}
if (_spritemgr != null) {
_spritemgr.addDirtyRect(getRenderedBounds());
} else {
Log.warning("Was invalidated but have no sprite manager " +
this + ".");
}
}
/**
* This method is called periodically by the sprite manager to give
* the sprite a chance to update its state. The sprite manager will
* attempt to call this with the desired refresh rate, but will drop
* calls to tick if it can't keep up. Thus, a sprite should rely on
* the timestamp information to compute elapsed progress if it wishes
* to handle heavy loads gracefully.
*/
public void tick (long timestamp)
{
int fcount = _frames.getFrameCount();
boolean moved = false;
// move the sprite along toward its destination, if any
if (_path != null) {
moved = _path.updatePosition(this, timestamp);
}
// increment the display image if performing image animation
int nfidx = _frameIdx;
switch (_animMode) {
case NO_ANIMATION:
// nothing doing
break;
case TIME_BASED:
nfidx = (int)((timestamp/_frameDelay) % fcount);
break;
case MOVEMENT_CUED:
// update the frame if the sprite moved
if (moved) {
nfidx = (_frameIdx + 1) % fcount;
}
break;
}
// only update the sprite if our frame index changed
if (nfidx != _frameIdx) {
_frameIdx = nfidx;
_frame = _frames.getFrame(_frameIdx);
// dirty our rectangle since we've altered our display image
invalidate();
}
}
/**
* Updates the sprite's render offset which is used to determine
* where to place the top-left corner of the render bounds.
*/
protected void updateRenderOffset ()
{
_rxoff = 0;
_ryoff = 0;
}
/**
* Update the coordinates at which the sprite image is drawn to
* reflect the sprite's current position.
*/
protected void updateRenderOrigin ()
{
// our render origin may differ from our location
_rbounds.x = _x + _rxoff;
_rbounds.y = _y + _ryoff;
}
/**
* Add a sprite observer to observe this sprite's events.
*
* @param obs the sprite observer.
*/
public void addSpriteObserver (SpriteObserver obs)
{
// create the observer list if it doesn't yet exist
if (_observers == null) {
_observers = new ArrayList();
}
// make sure each observer observes only once
if (_observers.contains(obs)) {
Log.info("Attempt to observe sprite already observing " +
"[sprite=" + this + ", obs=" + obs + "].");
return;
}
// add the observer
_observers.add(obs);
}
/**
* Inform all sprite observers of a sprite event.
*
* @param event the sprite event.
*/
protected void notifyObservers (SpriteEvent event)
{
if (_observers != null) {
// we pass this notification off to the sprite manager so that
// it can dispatch all of the notifications at once after all
// ticking has been completed
_spritemgr.notifySpriteObservers(_observers, event);
}
}
/**
* Return a string representation of the sprite.
*/
public String toString ()
{
StringBuffer buf = new StringBuffer();
buf.append("[");
toString(buf);
return buf.append("]").toString();
}
/**
* This should be overridden by derived classes (which should be sure
* to call <code>super.toString()</code>) to append the derived class
* specific sprite information to the string buffer.
*/
protected void toString (StringBuffer buf)
{
buf.append("x=").append(_x);
buf.append(", y=").append(_y);
buf.append(", fidx=").append(_frameIdx);
}
/** The images used to render the sprite. */
protected MultiFrameImage _frames;
/** The current frame being rendered. */
protected Image _frame;
/** The current frame index to render. */
protected int _frameIdx;
/** The orientation of this sprite. */
protected int _orient = DIR_NONE;
/** The location of the sprite in pixel coordinates. */
protected int _x, _y;
/** The offsets from our location to our rendered origin. */
protected int _rxoff, _ryoff;
/** Our rendered bounds in pixel coordinates. */
protected Rectangle _rbounds = new Rectangle();
/** What type of animation is desired for this sprite. */
protected int _animMode;
/** For how many milliseconds to display an animation frame. */
protected long _frameDelay;
/** When moving, the path the sprite is traversing. */
protected Path _path;
/** The sprite observers observing this sprite. */
protected ArrayList _observers;
/** The sprite manager. */
protected SpriteManager _spritemgr;
}