All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.threerings.media.sprite.Sprite Maven / Gradle / Ivy

The newest version!
//
// 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 then 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 then 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
    {
        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
    {
        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;
}