// // Nenya library - tools for developing networked games // Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved // https://github.com/threerings/nenya // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published // by the Free Software Foundation; either version 2.1 of the License, or // (at your option) any later version. // // This library 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. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA package com.threerings.media.sprite; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.Shape; import com.samskivert.util.ObserverList; import com.threerings.util.DirectionCodes; import com.threerings.media.AbstractMedia; import com.threerings.media.util.Path; import com.threerings.media.util.Pathable; /** * The sprite class represents a single movable object in an animated view. A sprite has a * position and orientation within the view, and can be moved along a path. */ public abstract class Sprite extends AbstractMedia implements DirectionCodes, Pathable { /** * Constructs a sprite with an initially invalid location. Because sprite derived classes * generally want to get in on the business when a sprite's location is set, it is not safe to * do so in the constructor because their derived methods will be called before their * constructor has been called. Thus a sprite should be fully constructed and <em>then</em> its * location should be set. */ public Sprite () { this(0, 0); } /** * Constructs a sprite with the supplied dimensions. Because sprite derived classes generally * want to get in on the business when a sprite's location is set, it is not safe to do so in * the constructor because their derived methods will be called before their constructor has * been called. Thus a sprite should be fully constructed and <em>then</em> its location should * be set. */ public Sprite (int width, int height) { super(new Rectangle(0, 0, width, height)); } /** * Returns the sprite's x position in screen coordinates. This is the x coordinate of the * sprite's origin, not the upper left of its bounds. */ public int getX () { return _ox; } /** * Returns the sprite's y position in screen coordinates. This is the y coordinate of the * sprite's origin, not the upper left of its bounds. */ public int getY () { return _oy; } /** * Returns the offset to the sprite's origin from the upper-left of the sprite's image. */ public int getXOffset () { return _oxoff; } /** * Returns the offset to the sprite's origin from the upper-left of the sprite's image. */ public int getYOffset () { return _oyoff; } /** * Returns the sprite's width in pixels. */ public int getWidth () { return _bounds.width; } /** * Returns the sprite's height in pixels. */ public int getHeight () { return _bounds.height; } /** * Sprites have an orientation in one of the eight cardinal directions: * {@link DirectionCodes#NORTH}, {@link DirectionCodes#NORTHEAST}, etc. 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. * * @see DirectionCodes */ public void setOrientation (int orient) { _orient = orient; } /** * Returns the sprite's orientation as one of the eight cardinal directions: * {@link DirectionCodes#NORTH}, {@link DirectionCodes#NORTHEAST}, etc. * * @see DirectionCodes */ public int getOrientation () { return _orient; } @Override public void setLocation (int x, int y) { if (x == _ox && y == _oy) { return; // no-op } // make a note of our current bounds Rectangle obounds = new Rectangle(_bounds); // move ourselves _ox = x; _oy = y; // we need to update our draw position which is based on the size of our current bounds updateRenderOrigin(); // this method will invalidate our old and new bounds efficiently invalidateAfterChange(obounds); } @Override public void paint (Graphics2D gfx) { gfx.drawRect(_bounds.x, _bounds.y, _bounds.width-1, _bounds.height-1); } /** * 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 true if the sprite's bounds contain the specified point, false if not. */ public boolean contains (int x, int y) { return _bounds.contains(x, y); } /** * Returns true if the sprite's bounds contain the specified point, false if not. */ public boolean hitTest (int x, int y) { return _bounds.contains(x, y); } /** * Returns whether the sprite is inside the given shape in pixel coordinates. */ public boolean inside (Shape shape) { return shape.contains(_ox, _oy); } /** * Returns whether the sprite's drawn rectangle intersects the given shape in pixel coordinates. */ public boolean intersects (Shape shape) { return shape.intersects(_bounds); } /** * Returns true if this sprite is currently following a path, false if it is not. */ public boolean isMoving () { return (_path != null); } /** * 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) { // if there's a previous path, let it know that it's going away cancelMove(); // save off this path _path = path; // we'll initialize it on our next tick thanks to a zero path stamp _pathStamp = 0; } /** * Cancels any path that the sprite may currently be moving along. */ public void cancelMove () { if (_path != null) { Path oldpath = _path; _path = null; oldpath.wasRemoved(this); if (_observers != null) { _observers.apply(new CancelledOp(this, oldpath)); } } } /** * Returns the path being followed by this sprite or null if the sprite is not following a path. */ public Path getPath () { return _path; } /** * Called by the active path when it begins. */ public void pathBeginning () { // nothing for now } /** * Called by the active path when it has completed. */ public void pathCompleted (long timestamp) { Path oldpath = _path; _path = null; oldpath.wasRemoved(this); if (_observers != null) { _observers.apply(new CompletedOp(this, oldpath, timestamp)); } } @Override public void tick (long tickStamp) { tickPath(tickStamp); } /** * Ticks any path assigned to this sprite. * * @return true if the path relocated the sprite as a result of this tick, false if it * remained in the same position. */ protected boolean tickPath (long tickStamp) { if (_path == null) { return false; } // initialize the path if we haven't yet if (_pathStamp == 0) { _path.init(this, _pathStamp = tickStamp); } // it's possible that as a result of init() the path completed and removed itself with a // call to pathCompleted(), so we have to be careful here return (_path == null) ? true : _path.tick(this, tickStamp); } @Override public void fastForward (long timeDelta) { // fast forward any path we're following if (_path != null) { _path.fastForward(timeDelta); } } /** * Update the coordinates at which the sprite image is drawn to reflect the sprite's current * position. */ protected void updateRenderOrigin () { // our bounds origin may differ from the sprite's origin _bounds.x = _ox - _oxoff; _bounds.y = _oy - _oyoff; } /** * Add a sprite observer to observe this sprite's events. * * @param obs the sprite observer. */ public void addSpriteObserver (Object obs) { addObserver(obs); } /** * Remove a sprite observer. */ public void removeSpriteObserver (Object obs) { removeObserver(obs); } @Override public void viewLocationDidChange (int dx, int dy) { if (_renderOrder >= HUD_LAYER) { setLocation(_ox + dx, _oy + dy); } } @Override protected void shutdown () { super.shutdown(); cancelMove(); // cancel any active path } @Override protected void toString (StringBuilder buf) { super.toString(buf); buf.append(", ox=").append(_ox); buf.append(", oy=").append(_oy); buf.append(", oxoff=").append(_oxoff); buf.append(", oyoff=").append(_oyoff); } /** Used to dispatch {@link PathObserver#pathCancelled}. */ protected static class CancelledOp implements ObserverList.ObserverOp<Object> { public CancelledOp (Sprite sprite, Path path) { _sprite = sprite; _path = path; } public boolean apply (Object observer) { if (observer instanceof PathObserver) { ((PathObserver)observer).pathCancelled(_sprite, _path); } return true; } protected Sprite _sprite; protected Path _path; } /** Used to dispatch {@link PathObserver#pathCompleted}. */ protected static class CompletedOp implements ObserverList.ObserverOp<Object> { public CompletedOp (Sprite sprite, Path path, long when) { _sprite = sprite; _path = path; _when = when; } public boolean apply (Object observer) { if (observer instanceof PathObserver) { ((PathObserver)observer).pathCompleted(_sprite, _path, _when); } return true; } protected Sprite _sprite; protected Path _path; protected long _when; } /** The location of the sprite's origin in pixel coordinates. If the sprite positions itself * via a hotspot that is not the upper left coordinate of the sprite's bounds, the offset to * the hotspot should be maintained in {@link #_oxoff} and {@link #_oyoff}. */ protected int _ox = Integer.MIN_VALUE, _oy = Integer.MIN_VALUE; /** The offsets from our upper left coordinate to our origin (or hot spot). Derived classes * will need to update these values if the sprite's origin is not coincident with the upper * left coordinate of its bounds. */ protected int _oxoff, _oyoff; /** The orientation of this sprite. */ protected int _orient = NONE; /** When moving, the path the sprite is traversing. */ protected Path _path; /** The timestamp at which we started along our path. */ protected long _pathStamp; }