
com.threerings.miso.client.DirtyItemList Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of nenya Show documentation
Show all versions of nenya Show documentation
Facilities for making networked multiplayer games.
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.miso.client;
import java.util.ArrayList;
import java.util.Comparator;
import java.awt.Graphics2D;
import com.google.common.collect.Lists;
import com.samskivert.util.SortableArrayList;
import com.threerings.media.sprite.Sprite;
import com.threerings.media.tile.ObjectTile;
import static com.threerings.media.Log.log;
/**
* The dirty item list keeps track of dirty sprites and object tiles in a scene.
*/
public class DirtyItemList
{
/**
* Creates a dirt item list that will handle dirty items for the specified view.
*/
public DirtyItemList ()
{
}
/**
* Appends the dirty sprite at the given coordinates to the dirty item list.
*
* @param sprite the dirty sprite itself.
* @param tx the sprite's x tile position.
* @param ty the sprite's y tile position.
*/
public void appendDirtySprite (Sprite sprite, int tx, int ty)
{
DirtyItem item = getDirtyItem();
item.init(sprite, tx, ty);
_items.add(item);
}
/**
* Appends the dirty object tile at the given coordinates to the dirty item list.
*
* @param scobj the scene object that is dirty.
*/
public void appendDirtyObject (SceneObject scobj)
{
DirtyItem item = getDirtyItem();
item.init(scobj, scobj.info.x, scobj.info.y);
_items.add(item);
}
/**
* Returns the dirty item at the given index in the list.
*/
public DirtyItem get (int idx)
{
return _items.get(idx);
}
/**
* Returns an array of the {@link DirtyItem} objects in the list sorted in proper rendering
* order.
*/
public void sort ()
{
int size = size();
if (DEBUG_SORT) {
log.info("Sorting dirty item list", "size", size);
}
// if we've only got one item, we need to do no sorting
if (size > 1) {
// get items sorted by increasing origin x-coordinate
_xitems.addAll(_items);
_xitems.sort(ORIGIN_X_COMP);
if (DEBUG_SORT) {
log.info("Sorted by x-origin", "items", toString(_xitems));
}
// get items sorted by increasing origin y-coordinate
_yitems.addAll(_items);
_yitems.sort(ORIGIN_Y_COMP);
if (DEBUG_SORT) {
log.info("Sorted by y-origin", "items", toString(_yitems));
}
// sort the items according to the depth of the rear-most tile
_ditems.addAll(_items);
_ditems.sort(REAR_DEPTH_COMP);
if (DEBUG_SORT) {
log.info("Sorted by rear-depth", "items", toString(_ditems));
}
// now insertion sort the items from back to front into the render-sorted array
_items.clear();
POS_LOOP:
for (int ii = 0; ii < size; ii++) {
DirtyItem item = _ditems.get(ii);
for (int rr = _items.size()-1; rr >= 0; rr--) {
DirtyItem pitem = _items.get(rr);
// if we render in front of this item, insert
// ourselves immediately following it
if (_rcomp.compare(item, pitem) > 0) {
_items.add(rr+1, item);
continue POS_LOOP;
}
}
// we don't render in front of anyone, so we go at the front of the list
_items.add(0, item);
}
// clear out our temporary arrays
_xitems.clear();
_yitems.clear();
_ditems.clear();
}
if (DEBUG_SORT) {
log.info("Sorted for render", "items", toString(_items));
for (int ii = 0, ll = _items.size()-1; ii < ll; ii++) {
DirtyItem a = _items.get(ii);
DirtyItem b = _items.get(ii+1);
if (_rcomp.compare(a, b) > 0) {
log.warning("Invalid ordering", "a", a, "b", b);
}
}
}
}
/**
* Paints all the dirty items in this list using the supplied graphics context. The items are
* removed from the dirty list after being painted and the dirty list ends up empty.
*/
public void paintAndClear (Graphics2D gfx)
{
int icount = _items.size();
for (int ii = 0; ii < icount; ii++) {
DirtyItem item = _items.get(ii);
item.paint(gfx);
item.clear();
_freelist.add(item);
}
_items.clear();
}
/**
* Clears out any items that were in this list.
*/
public void clear ()
{
for (int icount = _items.size(); icount > 0; icount--) {
DirtyItem item = _items.remove(0);
item.clear();
_freelist.add(item);
}
}
/**
* Returns the number of items in the dirty item list.
*/
public int size ()
{
return _items.size();
}
/**
* Obtains a new dirty item instance, reusing an old one if possible or creating a new one
* otherwise.
*/
protected DirtyItem getDirtyItem ()
{
if (_freelist.size() > 0) {
return _freelist.remove(0);
} else {
return new DirtyItem();
}
}
/**
* Returns an abbreviated string representation of the given dirty item describing only its
* origin coordinates and render priority. Intended for debugging purposes.
*/
protected static String toString (DirtyItem a)
{
StringBuilder buf = new StringBuilder("[");
toString(buf, a);
return buf.append("]").toString();
}
/**
* Returns an abbreviated string representation of the two given dirty items. See
* {@link #toString(DirtyItem)}.
*/
protected static String toString (DirtyItem a, DirtyItem b)
{
StringBuilder buf = new StringBuilder("[");
toString(buf, a);
toString(buf, b);
return buf.append("]").toString();
}
/**
* Returns an abbreviated string representation of the given dirty items. See
* {@link #toString(DirtyItem)}.
*/
protected static String toString (SortableArrayList items)
{
StringBuilder buf = new StringBuilder();
buf.append("[");
for (int ii = 0; ii < items.size(); ii++) {
DirtyItem item = items.get(ii);
toString(buf, item);
if (ii < (items.size() - 1)) {
buf.append(", ");
}
}
return buf.append("]").toString();
}
/** Helper function for {@link #toString(DirtyItem)}. */
protected static void toString (StringBuilder buf, DirtyItem item)
{
buf.append("(o:+").append(item.ox).append("+").append(item.oy);
buf.append(" p:").append(item.getRenderPriority()).append(")");
}
/**
* A class to hold the items inserted in the dirty list along with all of the information
* necessary to render their dirty regions to the target graphics context when the time comes
* to do so.
*/
public class DirtyItem
{
/** The dirtied object; one of either a sprite or an object tile. */
public Object obj;
/** The origin tile coordinates. */
public int ox, oy;
/** The leftmost tile coordinates. */
public int lx, ly;
/** The rightmost tile coordinates. */
public int rx, ry;
/**
* Initializes a dirty item.
*/
public void init (Object obj, int x, int y) {
this.obj = obj;
this.ox = x;
this.oy = y;
// calculate the item's leftmost and rightmost tiles; note that normal (Non-MultiTile)
// sprites occupy only a single tile, so leftmost and rightmost tiles are equivalent
lx = rx = ox;
ly = ry = oy;
if (obj instanceof SceneObject) {
ObjectTile tile = ((SceneObject)obj).tile;
lx -= (tile.getBaseWidth() - 1);
ry -= (tile.getBaseHeight() - 1);
} else if (obj instanceof MultiTileSprite) {
MultiTileSprite mts = (MultiTileSprite)obj;
lx -= (mts.getBaseWidth() - 1);
ry -= (mts.getBaseHeight() - 1);
}
}
/**
* Paints the dirty item to the given graphics context. Only the portion of the item that
* falls within the given dirty rectangle is actually drawn.
*/
public void paint (Graphics2D gfx) {
if (obj instanceof Sprite) {
((Sprite)obj).paint(gfx);
} else {
((SceneObject)obj).paint(gfx);
}
}
/**
* Returns the "depth" of our rear-most tile.
*/
public int getRearDepth () {
return ry + lx;
}
/**
* Returns the render priority for this dirty item. It will be zero unless this is a
* display object which may have a custom render priority.
*/
public int getRenderPriority () {
if (obj instanceof SceneObject) {
return ((SceneObject)obj).getPriority();
} else {
return 0;
}
}
/**
* Releases all references held by this dirty item so that it doesn't inadvertently hold
* on to any objects while waiting to be reused.
*/
public void clear () {
obj = null;
}
@Override
public boolean equals (Object other) {
// we're never equal to something that's not our kind
if (!(other instanceof DirtyItem)) {
return false;
}
// sprites are equivalent if they're the same sprite
DirtyItem b = (DirtyItem)other;
return obj.equals(b.obj);
}
@Override
public int hashCode () {
return obj.hashCode();
}
@Override
public String toString () {
StringBuilder buf = new StringBuilder();
buf.append("[obj=").append(obj);
buf.append(", ox=").append(ox);
buf.append(", oy=").append(oy);
buf.append(", lx=").append(lx);
buf.append(", ly=").append(ly);
buf.append(", rx=").append(rx);
buf.append(", ry=").append(ry);
return buf.append("]").toString();
}
}
/**
* A comparator class for use in sorting dirty items in ascending origin x- or y-axis
* coordinate order.
*/
protected static class OriginComparator implements Comparator
{
/**
* Constructs an origin comparator that sorts dirty items in ascending order based on
* their origin coordinate on the given axis.
*/
public OriginComparator (int axis) {
_axis = axis;
}
// documentation inherited
public int compare (DirtyItem da, DirtyItem db) {
// if they don't overlap, sort them normally
if (_axis == X_AXIS) {
if (da.ox != db.ox) {
return da.ox - db.ox;
}
} else {
if (da.oy != db.oy) {
return da.oy - db.oy;
}
}
// if they do overlap, incorporate render priority; assume
// non-display objects have a render priority of zero
return da.getRenderPriority() - db.getRenderPriority();
}
/** The axis this comparator sorts on. */
protected int _axis;
}
/**
* A comparator class for use in sorting the dirty sprites and objects in a scene in ascending
* x- and y-coordinate order suitable for rendering in the isometric view with proper visual
* results.
*/
protected class RenderComparator implements Comparator
{
// documentation inherited
public int compare (DirtyItem da, DirtyItem db) {
// if the two objects are scene objects and they overlap, we
// compare them solely based on their human assigned priority
if ((da.obj instanceof SceneObject) &&
(db.obj instanceof SceneObject)) {
SceneObject soa = (SceneObject)da.obj;
SceneObject sob = (SceneObject)db.obj;
if (soa.objectFootprintOverlaps(sob)) {
int result = soa.getPriority() - sob.getPriority();
if (DEBUG_COMPARE) {
String items = DirtyItemList.toString(da, db);
log.info("compare: overlapping", "result", result, "items", items);
}
return result;
}
}
// check for partitioning objects on the y-axis
int result = comparePartitioned(Y_AXIS, da, db);
if (result != 0) {
if (DEBUG_COMPARE) {
String items = DirtyItemList.toString(da, db);
log.info("compare: Y-partitioned", "result", result, "items", items);
}
return result;
}
// check for partitioning objects on the x-axis
result = comparePartitioned(X_AXIS, da, db);
if (result != 0) {
if (DEBUG_COMPARE) {
String items = DirtyItemList.toString(da, db);
log.info("compare: X-partitioned", "result", result, "items", items);
}
return result;
}
// use normal iso-ordering check
result = compareNonPartitioned(da, db);
if (DEBUG_COMPARE) {
String items = DirtyItemList.toString(da, db);
log.info("compare: non-partitioned", "result", result, "items", items);
}
return result;
}
/**
* Returns whether two dirty items have a partitioning object between them on the given
* axis.
*/
protected int comparePartitioned (int axis, DirtyItem da, DirtyItem db) {
// prepare for the partitioning check
SortableArrayList sitems;
Comparator comp;
boolean swapped = false;
switch (axis) {
case X_AXIS:
if (da.ox == db.ox) {
// can't be partitioned if there's no space between
return 0;
}
// order items for proper comparison
if (da.ox > db.ox) {
DirtyItem temp = da;
da = db;
db = temp;
swapped = true;
}
// use the axis-specific sorted array
sitems = _xitems;
comp = ORIGIN_X_COMP;
break;
case Y_AXIS:
default:
if (da.oy == db.oy) {
// can't be partitioned if there's no space between
return 0;
}
// order items for proper comparison
if (da.oy > db.oy) {
DirtyItem temp = da;
da = db;
db = temp;
swapped = true;
}
// use the axis-specific sorted array
sitems = _yitems;
comp = ORIGIN_Y_COMP;
break;
}
// get the bounding item indices and the number of potentially-partitioning dirty items
int aidx = sitems.binarySearch(da, comp);
int bidx = sitems.binarySearch(db, comp);
int size = bidx - aidx - 1;
// check each potentially partitioning item
int startidx = aidx + 1, endidx = startidx + size;
for (int pidx = startidx; pidx < endidx; pidx++) {
DirtyItem dp = sitems.get(pidx);
if (dp.obj instanceof Sprite) {
// sprites can't partition things
continue;
} else if ((dp.obj == da.obj) ||
(dp.obj == db.obj)) {
// can't be partitioned by ourselves
continue;
}
// perform the actual partition check for this object
switch (axis) {
case X_AXIS:
if (dp.ly >= da.ry &&
dp.ry <= db.ly &&
dp.lx >= da.rx &&
dp.rx <= db.lx) {
return (swapped) ? 1 : -1;
}
break;
case Y_AXIS:
default:
if (dp.lx <= db.ox &&
dp.rx >= da.lx &&
dp.ry >= da.oy &&
dp.oy <= db.ry) {
return (swapped) ? 1 : -1;
}
break;
}
}
// no partitioning object found
return 0;
}
/**
* Compares the two dirty items assuming there are no partitioning objects between them.
*/
protected int compareNonPartitioned (DirtyItem da, DirtyItem db) {
if (da.ox == db.ox &&
da.oy == db.oy) {
if (da.equals(db)) {
// render level is equal if we're the same sprite
// or an object at the same location
return 0;
}
boolean aIsSprite = (da.obj instanceof Sprite);
boolean bIsSprite = (db.obj instanceof Sprite);
if (aIsSprite && bIsSprite) {
Sprite as = (Sprite)da.obj, bs = (Sprite)db.obj;
// we're comparing two sprites co-existing on the same
// tile, first check their render order
int rocomp = as.getRenderOrder() - bs.getRenderOrder();
if (rocomp != 0) {
return rocomp;
}
// next sort them by y-position
int ydiff = as.getY() - bs.getY();
if (ydiff != 0) {
return ydiff;
}
// if they're at the same height, just use hashCode()
// to establish a consistent arbitrary ordering
return (as.hashCode() - bs.hashCode());
// otherwise, always put a sprite on top of a non-sprite
} else if (aIsSprite) {
return 1;
} else if (bIsSprite) {
return -1;
}
}
// One is a multi-tile sprite and the two overlap - use render order if it helps.
// Note - Ideally logic like this should probably apply regardless of the type of object
// BUT considering the number of things that already exist that use this code, I suspect
// it would break something...
if ((da.obj instanceof MultiTileSprite || db.obj instanceof MultiTileSprite) &&
(da.lx <= db.rx && da.rx >= db.lx && da.ry <= db.ly && da.ly >= db.ry)) {
int aRender = (da.obj instanceof Sprite) ? ((Sprite)da.obj).getRenderOrder() : 0;
int bRender = (db.obj instanceof Sprite) ? ((Sprite)db.obj).getRenderOrder() : 0;
// we're comparing two sprites co-existing on the same
// tile, first check their render order
int rocomp = aRender - bRender;
if (rocomp != 0) {
return rocomp;
}
}
// otherwise use a consistent ordering for non-overlappers;
// see narya/docs/miso/render_sort_diagram.png for more info
if (db.lx <= da.ox && db.ry <= da.oy) {
return 1;
} else if (db.rx >= da.lx && db.ly >= da.ry) {
return -1;
} else {
return da.oy - db.oy;
}
}
}
/** The list of dirty items. */
protected SortableArrayList _items = new SortableArrayList();
/** The list of dirty items sorted by x-position. */
protected SortableArrayList _xitems = new SortableArrayList();
/** The list of dirty items sorted by y-position. */
protected SortableArrayList _yitems = new SortableArrayList();
/** The list of dirty items sorted by rear-depth. */
protected SortableArrayList _ditems = new SortableArrayList();
/** The render comparator we'll use for our final, magical sort. */
protected Comparator _rcomp = new RenderComparator();
/** Unused dirty items. */
protected ArrayList _freelist = Lists.newArrayList();
/** Whether to log debug info when comparing pairs of dirty items. */
protected static final boolean DEBUG_COMPARE = false;
/** Whether to log debug info for the main dirty item sorting algorithm. */
protected static final boolean DEBUG_SORT = false;
/** Constants used to denote axis sorting constraints. */
protected static final int X_AXIS = 0;
protected static final int Y_AXIS = 1;
/** The comparator used to sort dirty items in ascending origin x-coordinate order. */
protected static final Comparator ORIGIN_X_COMP = new OriginComparator(X_AXIS);
/** The comparator used to sort dirty items in ascending origin y-coordinate order. */
protected static final Comparator ORIGIN_Y_COMP = new OriginComparator(Y_AXIS);
/** The comparator used to sort dirty items in ascending "rear-depth" order. */
protected static final Comparator REAR_DEPTH_COMP = new Comparator() {
public int compare (DirtyItem o1, DirtyItem o2) {
int depthDiff = (o1.getRearDepth() - o2.getRearDepth());
if (depthDiff != 0) {
return depthDiff;
} else {
// If there's a priority difference, break our tie on that.
if (o1.obj instanceof SceneObject && o2.obj instanceof SceneObject) {
int priDiff = ((SceneObject)o1.obj).getPriority() -
((SceneObject)o2.obj).getPriority();
if (priDiff != 0) {
return priDiff;
}
}
// Couldn't break the tie, fallback to the original result.
return depthDiff;
}
}
};
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy