// // $Id: SpriteManager.java,v 1.17 2002/01/11 16:17:33 shaper Exp $ package com.threerings.media.sprite; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Polygon; import java.awt.Rectangle; import java.awt.Shape; import java.util.Arrays; import java.util.ArrayList; import java.util.Comparator; import java.util.Iterator; import java.util.List; import com.samskivert.util.CollectionUtil; import com.samskivert.util.Tuple; import com.threerings.media.Log; /** * The SpriteManager manages the sprites running about in the game. */ public class SpriteManager { /** * Construct and initialize the SpriteManager object. */ public SpriteManager () { _sprites = new ArrayList(); _notify = new ArrayList(); _dirty = new ArrayList(); } /** * Add a rectangle to the dirty rectangle list. * * @param rect the rectangle to add. */ public void addDirtyRect (Rectangle rect) { _dirty.add(rect); } /** * When an animated view processes its dirty rectangles, it may * require an expansion of the dirty region which may in turn * require the invalidation of more sprites than were originally * invalid. In such cases, the animated view can call back to the * sprite manager, asking it to append the sprites that intersect * a particular region to the given list. * * @param list the list to fill with any intersecting sprites. * @param bounds the bounds the intersection of which we have * interest. */ public void getIntersectingSprites (List list, Shape shape) { int size = _sprites.size(); for (int ii = 0; ii < size; ii++) { Sprite sprite = (Sprite)_sprites.get(ii); if (sprite.intersects(shape)) { list.add(sprite); } } } /** * Add a sprite to the set of sprites managed by this manager. * * @param sprite the sprite to add. */ public void addSprite (Sprite sprite) { // let the sprite know about us sprite.setSpriteManager(this); // add the sprite to our list _sprites.add(sprite); // and invalidate the sprite's original position sprite.invalidate(); } /** * Removes the specified sprite from the set of sprites managed by * this manager. * * @param sprite the sprite to remove. */ public void removeSprite (Sprite sprite) { // invalidate the current sprite position sprite.invalidate(); // remove the sprite from our list _sprites.remove(sprite); // clear out our manager reference sprite.setSpriteManager(null); } /** * Render the sprites residing within the given polygon to the given * graphics context. * * @param gfx the graphics context. * @param bounds the bounding polygon. */ public void renderSprites (Graphics2D gfx, Polygon bounds) { // TODO: optimize to store sprites based on quadrants they're // in (or somesuch), and sorted, so that we can more quickly // determine which sprites to draw. int size = _sprites.size(); for (int ii = 0; ii < size; ii++) { Sprite sprite = (Sprite)_sprites.get(ii); if (sprite.inside(bounds)) { sprite.paint(gfx); } } } /** * Render the sprite paths to the given graphics context. * * @param gfx the graphics context. */ public void renderSpritePaths (Graphics2D gfx) { int size = _sprites.size(); for (int ii = 0; ii < size; ii++) { Sprite sprite = (Sprite)_sprites.get(ii); sprite.paintPath(gfx); } } /** * Called periodically by the tick tasks put on the AWT event * queue by the {@link AnimationManager}. Handles moving about of * sprites and reporting of sprite collisions. */ public void tick (long timestamp, List rects) { // tick all sprites tickSprites(timestamp); // re-sort the sprite list to account for potential new positions sortSprites(); // notify sprite observers of any collisions handleCollisions(); // notify sprite observers of any sprite events. note that we // explicitly queue up events while ticking and checking for // collisions, and we only notify observers once we're // finished with all of those antics so that any actions the // observers may take, such as sprite removal, won't screw us // up elsewhere. handleSpriteEvents(); // add all generated dirty rectangles to the passed-in dirty // rectangle list CollectionUtil.addAll(rects, _dirty.iterator()); // clear out our internal dirty rectangle list _dirty.clear(); } /** * Call tick() on all sprite objects to give them a * chance to move themselves about, change their display image, * and so forth. */ protected void tickSprites (long timestamp) { int size = _sprites.size(); for (int ii = 0; ii < size; ii++) { Sprite sprite = (Sprite)_sprites.get(ii); sprite.tick(timestamp); } } /** * Sort the sprite list in order of ascending x-coordinate. */ protected void sortSprites () { Object[] sprites = new Sprite[_sprites.size()]; _sprites.toArray(sprites); Arrays.sort(sprites, SPRITE_COMP); _sprites.clear(); for (int ii = 0; ii < sprites.length; ii++) { _sprites.add(sprites[ii]); } } /** * Check all sprites for collisions with others and inform any * sprite observers. */ protected void handleCollisions () { // gather a list of all sprite collisions int size = _sprites.size(); for (int ii = 0; ii < size; ii++) { Sprite sprite = (Sprite)_sprites.get(ii); checkCollisions(ii, size, sprite); } } /** * Check a sprite for collision with any other sprites in the * sprite list and notify the sprite observers associated with any * sprites that do indeed collide. * * @param idx the starting sprite index. * @param size the total number of sprites. * @param sprite the sprite to check against other sprites for * collisions. */ protected void checkCollisions (int idx, int size, Sprite sprite) { // TODO: make this handle quickly moving objects that may pass // through each other. // if we're the last sprite we know we've already handled any // collisions if (idx == (size - 1)) { return; } // calculate the x-position of the right edge of the sprite // we're checking for collisions Rectangle bounds = sprite.getBounds(); int edgeX = bounds.x + bounds.width; for (int ii = (idx + 1); ii < size; ii++) { Sprite other = (Sprite)_sprites.get(ii); Rectangle obounds = other.getBounds(); if (obounds.x > edgeX) { // since sprites are stored in the list sorted by // ascending x-position, we know this sprite and any // other sprites farther on in the list can't possibly // intersect with the sprite we're checking, so we're // done. return; } if (obounds.intersects(bounds)) { sprite.notifyObservers(new CollisionEvent(sprite, other)); } } } /** * Notify all sprite observers of any sprite events that took * place during our most recent tick(). */ protected void handleSpriteEvents () { int size = _notify.size(); for (int ii = 0; ii < size; ii++) { Tuple tup = (Tuple)_notify.remove(0); ArrayList observers = (ArrayList)tup.left; SpriteEvent evt = (SpriteEvent)tup.right; int osize = observers.size(); for (int jj = 0; jj < osize; jj++) { SpriteObserver obs = (SpriteObserver)observers.get(jj); obs.handleEvent(evt); } } } /** * Called by {@link Sprite#notifyObservers} to note that the * sprite's observers should be informed of a sprite event once * the current tick() is complete. * * @param observers the list of sprite observers. * @param event the sprite event to notify the observers of. */ protected void notifySpriteObservers (ArrayList observers, SpriteEvent event) { // throw it on the list of events for later _notify.add(new Tuple(observers, event)); } /** The comparator used to sort sprites by horizontal position. */ protected static final Comparator SPRITE_COMP = new SpriteComparator(); /** The sprite objects we're managing. */ protected ArrayList _sprites; /** The dirty rectangles created by sprites. */ protected ArrayList _dirty; /** The list of pending sprite notifications. */ protected ArrayList _notify; protected static class SpriteComparator implements Comparator { public int compare (Object o1, Object o2) { Sprite s1 = (Sprite)o1; Sprite s2 = (Sprite)o2; return (s2.getX() - s1.getX()); } public boolean equals (Object obj) { return (obj == this); } } }