Port Paths to AS from java. Also, port (with modifications) AStarPathUtil -> AStarPathSearch which is no longer a singleton and has fewer hidden dependencies (e.g. the restriction on the "longest" arg to getPath). Also, make MisoScenePanel actually create paths now that we have them.

git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@982 ed5b42cb-e716-0410-a449-f6a68f950b19
This commit is contained in:
Mike Thomas
2010-08-13 20:28:48 +00:00
parent d8cda89ab3
commit 3b0b6038dc
13 changed files with 1213 additions and 23 deletions
@@ -157,6 +157,7 @@ public class CharacterSprite extends Sprite
public function setOrientation (orient :int) :void
{
var oorient :int = _orient;
_orient = orient;
if (orient < 0 || orient >= DirectionCodes.FINE_DIRECTION_COUNT) {
log.info("Refusing to set invalid orientation [sprite=" + this +
@@ -164,12 +165,16 @@ public class CharacterSprite extends Sprite
return;
}
var oorient :int = _orient;
if (_orient != oorient) {
updateMainSprite();
}
}
public function getOrientation () :int
{
return _orient;
}
public function cancelMove () :void
{
halt();
@@ -181,7 +186,7 @@ public class CharacterSprite extends Sprite
setActionSequence(getFollowingPathAction());
}
public function pathCompleted () :void
public function pathCompleted (timestamp :int) :void
{
halt();
}
@@ -231,6 +236,7 @@ public class CharacterSprite extends Sprite
if (_aframes != null) {
_aframes.getFrames(_orient, function(frames :MultiFrameBitmap) :void {
_framesBitmap = frames;
DisplayUtil.removeAllChildren(_mainSprite);
_mainSprite.addChild(frames);
});
} else {
@@ -0,0 +1,131 @@
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.util {
import com.threerings.util.MathUtil;
/**
* Searches for the shortest traversable path between locations.
*/
public class AStarPathSearch
{
/**
* Creates a searching object using tpred to check for traversability and stepper to find
* all the valid adjacent spots and the stepping costs & estimates.
*/
public function AStarPathSearch (tpred :TraversalPred,
stepper :AStarPathSearch_Stepper = null)
{
_tpred = tpred;
if (stepper == null) {
_stepper = new AStarPathSearch_Stepper();
} else {
_stepper = stepper;
}
}
/**
* Return a list of <code>Point</code> objects representing a path from coordinates
* <code>(ax, by)</code> to <code>(bx, by)</code>, inclusive, determined by performing an
* A* search in the given scene's base tile layer. Assumes the starting and destination nodes
* are traversable by the specified traverser.
*
* @param trav the traverser to follow the path.
* @param longest the longest allowable path in tile traversals. This arg must be small enough
* that _stepper.getMaxCost(longest) < Integer.MAX_VALUE
* @param ax the starting x-position in tile coordinates.
* @param ay the starting y-position in tile coordinates.
* @param bx the ending x-position in tile coordinates.
* @param by the ending y-position in tile coordinates.
* @param partial if true, a partial path will be returned that gets us as close as we can to
* the goal in the event that a complete path cannot be located.
*
* @return the list of points in the path, or null if no path could be found.
*/
public function getPath (trav :Object, longest :int, ax :int, ay :int, bx :int, by :int,
partial :Boolean) :Array
{
var info :AStarPathSearch_Info = new AStarPathSearch_Info(_tpred, trav,
_stepper.getMaxCost(longest), bx, by);
// set up the starting node
var s :AStarPathSearch_Node = info.getNode(ax, ay);
s.g = 0;
s.h = _stepper.getDistanceEstimate(ax, ay, bx, by);
s.f = s.g + s.h;
// push starting node on the open list
info.open.add(s);
// track the best path
var bestdist :Number = Number.POSITIVE_INFINITY;
var bestpath :AStarPathSearch_Node = null;
// while there are more nodes on the open list
while (info.open.size() > 0) {
// pop the best node so far from open
var n :AStarPathSearch_Node = null;
info.open.forEach(function (val :AStarPathSearch_Node) :Boolean {
n = val;
return true;
});
info.open.remove(n);
// if node is a goal node
if (n.x == bx && n.y == by) {
// construct and return the acceptable path
return n.getNodePath();
} else if (partial) {
var pathdist :Number = MathUtil.distance(n.x, n.y, bx, by);
if (pathdist < bestdist) {
bestdist = pathdist;
bestpath = n;
}
}
// consider each successor of the node
_stepper.considerSteps(info, n, n.x, n.y);
// push the node on the closed list
info.closed.add(n);
}
// return the best path we could find if we were asked to do so
if (bestpath != null) {
return bestpath.getNodePath();
}
// no path found
return null;
}
/** In charge of determining if we can walk across various bits. */
protected var _tpred :TraversalPred;
/** In charge of finding all the steps from a given spot. */
protected var _stepper :AStarPathSearch_Stepper;
}
}
@@ -0,0 +1,178 @@
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.util {
import com.threerings.util.Maps;
import com.threerings.util.Map;
import com.threerings.util.Set;
import com.threerings.util.Sets;
/**
* A holding class to contain the wealth of information referenced
* while performing an A* search for a path through a tile array.
*/
public class AStarPathSearch_Info
{
/** Knows whether or not tiles are traversable. */
public var tpred :TraversalPred;
/** The tile array dimensions. */
public var tilewid :int;
public var tilehei :int;
/** The traverser moving along the path. */
public var trav :Object;
/** The set of open nodes being searched. */
// TODO - This would benefit from a more efficient implementation of SortedSet, but for now
// it is good enough.
public var open :Set;
/** The set of closed nodes being searched. */
public var closed :Set;
/** The destination coordinates in the tile array. */
public var destx :int;
public var desty :int;
/** The maximum cost of any path that we'll consider. */
public var maxcost :int;
public function AStarPathSearch_Info (tpred :TraversalPred, trav :Object, maxcost :int,
destx :int, desty :int)
{
// save off references
this.tpred = tpred;
this.trav = trav;
this.destx = destx;
this.desty = desty;
// compute our maximum path cost
this.maxcost = maxcost;
// construct the open and closed lists
open = Sets.newSortedSetOf(AStarPathSearch_Node);
closed = Sets.newSetOf(AStarPathSearch_Node);
}
/**
* Returns whether moving from the given source to destination coordinates is a valid
* move.
*/
protected function isStepValid (sx :int, sy :int, dx :int, dy :int) :Boolean
{
// not traversable if the destination itself fails test
if (tpred is ExtendedTraversalPred) {
if (!ExtendedTraversalPred(tpred).canTraverseBetween(trav, sx, sy, dx, dy)) {
return false;
}
} else if (!isTraversable(dx, dy)) {
return false;
}
// if the step is diagonal, make sure the corners don't impede our progress
if ((Math.abs(dx - sx) == 1) && (Math.abs(dy - sy) == 1)) {
return isTraversable(dx, sy) && isTraversable(sx, dy);
}
// non-diagonals are always traversable
return true;
}
/**
* Returns whether the given coordinate is valid and traversable.
*/
protected function isTraversable (x :int, y :int) :Boolean
{
return tpred.canTraverse(trav, x, y);
}
/**
* Get or create the node for the specified point.
*/
public function getNode (x :int, y :int) :AStarPathSearch_Node
{
// note: this _could_ break for unusual values of x and y.
// perhaps use a IntTuple as a key? Bleah.
var key :int = (x << 16) | (y & 0xffff);
var node :AStarPathSearch_Node = _nodes.get(key);
if (node == null) {
node = new AStarPathSearch_Node(x, y);
_nodes.put(key, node);
}
return node;
}
/**
* Consider the step <code>(n.x, n.y)</code> to <code>(x, y)</code> for possible inclusion
* in the path.
*
* @param info the info object.
* @param node the originating node for the step.
* @param x the x-coordinate for the destination step.
* @param y the y-coordinate for the destination step.
*/
public function considerStep (node :AStarPathSearch_Node, x :int, y :int, cost :int,
stepper :AStarPathSearch_Stepper) :void
{
// skip node if it's outside the map bounds or otherwise impassable
if (!isStepValid(node.x, node.y, x, y)) {
return;
}
// calculate the new cost for this node
var newg :int = node.g + cost;
// make sure the cost is reasonable
if (newg > maxcost) {
return;
}
// retrieve the node corresponding to this location
var np :AStarPathSearch_Node = getNode(x, y);
// skip if it's already in the open or closed list or if its
// actual cost is less than the just-calculated cost
if ((open.contains(np) || closed.contains(np)) && np.g <= newg) {
return;
}
// remove the node from the open list since we're about to
// modify its score which determines its placement in the list
open.remove(np);
// update the node's information
np.parent = node;
np.g = newg;
np.h = stepper.getDistanceEstimate(np.x, np.y, destx, desty);
np.f = np.g + np.h;
// remove it from the closed list if it's present
closed.remove(np);
// add it to the open list for further consideration
open.add(np);
}
/** The nodes being considered in the path. */
protected var _nodes :Map = Maps.newMapOf(int);
}
}
@@ -0,0 +1,99 @@
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.util {
import flash.geom.Point;
import com.threerings.util.Comparable;
public class AStarPathSearch_Node
implements Comparable
{
/** The node coordinates. */
public var x :int;
public var y :int;
/** The actual cheapest cost of arriving here from the start. */
public var g :int;
/** The heuristic estimate of the cost to the goal from here. */
public var h :int;
/** The score assigned to this node. */
public var f :int;
/** The node from which we reached this node. */
public var parent :AStarPathSearch_Node;
/** The node's monotonically-increasing unique identifier. */
public var id :int;
public function AStarPathSearch_Node (x :int, y :int)
{
this.x = x;
this.y = y;
id = _nextid++;
}
public function compareTo (o :Object) :int
{
var n :AStarPathSearch_Node = AStarPathSearch_Node(o);
var bf :int = n.f;
// since the set contract is fulfilled using the equality results returned here, and
// we'd like to allow multiple nodes with equivalent scores in our set, we explicitly
// define object equivalence as the result of object.equals(), else we use the unique
// node id since it will return a consistent ordering for the objects.
if (f == bf) {
return (this == n) ? 0 : (id - n.id);
}
return f - bf;
}
/**
* Return an array of <code>Point</code> objects detailing the path from the first node (the
* given node's ultimate parent) to the ending node (the given node itself.)
*
* @param node the ending node in the path.
*
* @return the list detailing the path.
*/
public function getNodePath () :Array
{
var cur :AStarPathSearch_Node = this;
var path :Array = [];
while (cur != null) {
// add to the head of the list since we're traversing from
// the end to the beginning
path.unshift(new Point(cur.x, cur.y));
// advance to the next node in the path
cur = cur.parent;
}
return path;
}
/** The next unique node id. */
protected static var _nextid :int = 0;
}
}
@@ -0,0 +1,77 @@
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.util {
/**
* Considers all the possible steps the piece in question can take.
*/
public class AStarPathSearch_Stepper
{
public function AStarPathSearch_Stepper (considerDiagonals :Boolean = true)
{
_considerDiagonals = considerDiagonals;
}
public function getMaxCost (longest :int) :int
{
return longest * ADJACENT_COST;
}
/**
* Return a heuristic estimate of the cost to get from <code>(ax, ay)</code> to
* <code>(bx, by)</code>.
*/
public function getDistanceEstimate (ax :int, ay :int, bx :int, by :int) :int
{
// we're doing all of our cost calculations based on geometric distance times ten
var dx :int = bx - ax;
var dy :int = by - ay;
return int(Math.floor(ADJACENT_COST * Math.sqrt(dx * dx + dy * dy)));
}
/**
* Should call {@link #considerStep} in turn on all possible steps from the specified
* coordinates. No checking must be done as to whether the step is legal, that will be
* handled later. Just enumerate all possible steps.
*/
public function considerSteps (info :AStarPathSearch_Info, node :AStarPathSearch_Node,
x :int, y :int) :void
{
info.considerStep(node, x, y - 1, ADJACENT_COST, this);
info.considerStep(node, x, y + 1, ADJACENT_COST, this);
info.considerStep(node, x - 1, y, ADJACENT_COST, this);
info.considerStep(node, x + 1, y, ADJACENT_COST, this);
if (_considerDiagonals) {
info.considerStep(node, x - 1, y - 1, DIAGONAL_COST, this);
info.considerStep(node, x + 1, y - 1, DIAGONAL_COST, this);
info.considerStep(node, x - 1, y + 1, DIAGONAL_COST, this);
info.considerStep(node, x + 1, y + 1, DIAGONAL_COST, this);
}
}
protected var _considerDiagonals :Boolean;
/** The standard cost to move between nodes. */
protected static const ADJACENT_COST :int = 10;
/** The cost to move diagonally. */
protected static const DIAGONAL_COST :int = int(Math.sqrt((ADJACENT_COST * ADJACENT_COST) * 2));
}
}
@@ -0,0 +1,34 @@
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.util {
/**
* Provides extended traversibility information when computing paths.
*/
public interface ExtendedTraversalPred extends TraversalPred
{
/**
* Requests to know if the specific traverser (which was provided in the call to
* {@link #getPath(TraversalPred,Object,int,int,int,int,int,boolean)}) can traverse from
* the specified source tile coordinate to the specified destination tile coordinate.
*/
function canTraverseBetween (traverser :Object, sx :int, sy :int, dx :int, dy :int) :Boolean;
}
}
@@ -0,0 +1,373 @@
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.util {
import flash.geom.Point;
import flash.geom.Rectangle;
import com.threerings.util.ArrayIterator;
import com.threerings.util.DirectionCodes;
import com.threerings.util.DirectionUtil;
import com.threerings.util.Iterator;
import com.threerings.util.Log;
import com.threerings.util.MathUtil;
import com.threerings.util.StringUtil;
/**
* The line segment path is used to cause a pathable to follow a path that
* is made up of a sequence of line segments. There must be at least two
* nodes in any worthwhile path. The direction of the first node in the
* path is meaningless since the pathable begins at that node and will
* therefore never be heading towards it.
*/
public class LineSegmentPath
implements Path
{
private var log :Log = Log.getLog(LineSegmentPath);
/**
* Constructs an empty line segment path.
*/
public function LineSegmentPath ()
{
}
/**
* Constructs a line segment path that consists of a single segment
* connecting the point <code>(x1, y1)</code> with <code>(x2,
* y2)</code>. The orientation for the first node is set arbitrarily
* and the second node is oriented based on the vector between the two
* nodes (in top-down coordinates).
*/
public static function createWithInts (x1 :int, y1 :int, x2 :int, y2 :int) :LineSegmentPath
{
var path :LineSegmentPath = new LineSegmentPath();
path.addNode(x1, y1, DirectionCodes.NORTH);
var p1 :Point = new Point(x1, y1);
var p2 :Point = new Point(x2, y2);
var dir :int = DirectionUtil.getDirectionForPts(p1, p2);
path.addNode(x2, y2, dir);
return path;
}
/**
* Construct a line segment path between the two nodes with the
* specified direction.
*/
public static function createWithPoints (p1 :Point, p2 :Point, dir :int) :LineSegmentPath
{
var path :LineSegmentPath = new LineSegmentPath();
path.addNode(p1.x, p1.y, DirectionCodes.NORTH);
path.addNode(p2.x, p2.y, dir);
return path;
}
/**
* Constructs a line segment path with the specified list of points.
* An arbitrary direction will be assigned to the starting node.
*/
public static function createWithList (points :Array) :LineSegmentPath
{
var path :LineSegmentPath = new LineSegmentPath();
path.createPath(points);
return path;
}
/**
* Returns the orientation the sprite will face at the end of the
* path.
*/
public function getFinalOrientation () :int
{
return (_nodes.length == 0) ? DirectionCodes.NORTH : _nodes[_nodes.length-1].dir;
}
/**
* Add a node to the path with the specified destination point and
* facing direction.
*
* @param x the x-position.
* @param y the y-position.
* @param dir the facing direction.
*/
public function addNode (x :Number, y :Number, dir :int) :void
{
_nodes.push(new PathNode(x, y, dir));
}
/**
* Return the requested node index in the path, or null if no such
* index exists.
*
* @param idx the node index.
*
* @return the path node.
*/
public function getNode (idx :int) :PathNode
{
return _nodes[idx];
}
/**
* Return the number of nodes in the path.
*/
public function size () :int
{
return _nodes.length;
}
/**
* Sets the velocity of this pathable in pixels per millisecond. The
* velocity is measured as pixels traversed along the path that the
* pathable is traveling rather than in the x or y directions
* individually. Note that the pathable velocity should not be
* changed while a path is being traversed; doing so may result in the
* pathable position changing unexpectedly.
*
* @param velocity the pathable velocity in pixels per millisecond.
*/
public function setVelocity (velocity :Number) :void
{
_vel = velocity;
}
/**
* Computes the velocity at which the pathable will need to travel
* along this path such that it will arrive at the destination in
* approximately the specified number of milliseconds. Efforts are
* taken to get the pathable there as close to the desired time as
* possible, but framerate variation may prevent it from arriving
* exactly on time.
*/
public function setDuration (millis :int) :void
{
// if we have only zero or one nodes, we don't have enough
// information to compute our velocity
var ncount :int = _nodes.length;
if (ncount < 2) {
log.warning("Requested to set duration of bogus path " +
"[path=" + this + ", duration=" + millis + "].");
return;
}
// compute the total distance along our path
var distance :Number = 0;
var start :PathNode = _nodes[0];
for (var ii :int = 1; ii < ncount; ii++) {
var end :PathNode = _nodes[ii];
distance += MathUtil.distance(start.loc.x, start.loc.y, end.loc.x, end.loc.y);
start = end;
}
// set the velocity accordingly
setVelocity(distance/millis);
}
// documentation inherited
public function init (pable :Pathable, timestamp :int) :void
{
// give the pathable a chance to perform any starting antics
pable.pathBeginning();
// if we have only one node then let the pathable know that we're
// done straight away
if (size() < 2) {
// move the pathable to the location specified by the first
// node (assuming we have a first node)
if (size() == 1) {
var node :PathNode = _nodes[0];
pable.setLocation(node.loc.x, node.loc.y);
}
// and let the pathable know that we're done
pable.pathCompleted(timestamp);
return;
}
// and an enumeration of the path nodes
_niter = new ArrayIterator(_nodes);
// pretend like we were previously heading to our starting position
_dest = getNextNode();
// begin traversing the path
headToNextNode(pable, timestamp, timestamp);
}
// documentation inherited
public function tick (pable :Pathable, timestamp :int) :Boolean
{
// figure out how far along this segment we should be
var msecs :int = timestamp - _nodestamp;
var travpix :Number = msecs * _vel;
var pctdone :Number = travpix / _seglength;
// if we've moved beyond the end of the path, we need to adjust
// the timestamp to determine how much time we used getting to the
// end of this node, then move to the next one
if (pctdone >= 1.0) {
var used :int = int(_seglength / _vel);
return headToNextNode(pable, _nodestamp + used, timestamp);
}
// otherwise we position the pathable along the path
var ox :Number = pable.getX();
var oy :Number = pable.getY();
var nx :Number = _src.loc.x + (_dest.loc.x - _src.loc.x) * pctdone;
var ny :Number = _src.loc.y + (_dest.loc.y - _src.loc.y) * pctdone;
// Log.info("Moving pathable [msecs=" + msecs + ", pctdone=" + pctdone +
// ", travpix=" + travpix + ", seglength=" + _seglength +
// ", dx=" + (nx-ox) + ", dy=" + (ny-oy) + "].");
// only update the pathable's location if it actually moved
if (ox != nx || oy != ny) {
pable.setLocation(nx, ny);
return true;
}
return false;
}
// documentation inherited
public function fastForward (timeDelta :int) :void
{
_nodestamp += timeDelta;
}
// documentation inherited from interface
public function wasRemoved (pable :Pathable) :void
{
// nothing doing
}
/**
* Place the pathable moving along the path at the end of the previous
* path node, face it appropriately for the next node, and start it on
* its way. Returns whether the pathable position moved.
*/
protected function headToNextNode (pable :Pathable, startstamp :int, now :int) :Boolean
{
if (_niter == null) {
throw new Error("headToNextNode() called before init()");
}
// check to see if we've completed our path
if (!_niter.hasNext()) {
// move the pathable to the location of our last destination
pable.setLocation(_dest.loc.x, _dest.loc.y);
pable.pathCompleted(now);
return true;
}
// our previous destination is now our source
_src = _dest;
// pop the next node off the path
_dest = getNextNode();
// adjust the pathable's orientation
if (_dest.dir != DirectionCodes.NONE) {
pable.setOrientation(_dest.dir);
}
// make a note of when we started traversing this node
_nodestamp = startstamp;
// figure out the distance from source to destination
_seglength = MathUtil.distance(_src.loc.x, _src.loc.y, _dest.loc.x, _dest.loc.y);
// if we're already there (the segment length is zero), we skip to
// the next segment
if (_seglength == 0) {
return headToNextNode(pable, startstamp, now);
}
// now update the pathable's position based on our progress thus far
return tick(pable, now);
}
public function toString () :String
{
return StringUtil.toString(_nodes);
}
/**
* Populate the path with the path nodes that lead the pathable from
* its starting position to the given destination coordinates
* following the given list of screen coordinates.
*/
protected function createPath (points :Array) :void
{
var last :Point = null;
var size :int = points.length;
for (var ii :int = 0; ii < size; ii++) {
var p :Point = points[ii];
var dir :int = (ii == 0) ? DirectionCodes.NORTH :
DirectionUtil.getDirectionForPts(last, p);
addNode(p.x, p.y, dir);
last = p;
}
}
/**
* Gets the next node in the path.
*/
protected function getNextNode () :PathNode
{
return PathNode(_niter.next());
}
/** The nodes that make up the path. */
protected var _nodes :Array = [];
/** We use this when moving along this path. */
protected var _niter :Iterator;
/** When moving, the pathable's source path node. */
protected var _src :PathNode;
/** When moving, the pathable's destination path node. */
protected var _dest :PathNode;
/** The time at which we started traversing the current node. */
protected var _nodestamp :int;
/** The length in pixels of the current path segment. */
protected var _seglength :Number;
/** The path velocity in pixels per millisecond. */
protected var _vel :Number = DEFAULT_VELOCITY;
/** When moving, the pathable position including fractional pixels. */
protected var _movex :Number;
protected var _movey :Number;
/** When moving, the distance to move on each axis per tick. */
protected var _incx :Number;
protected var _incy :Number;
/** The distance to move on the straight path line per tick. */
protected var _fracx :Number;
protected var _fracy :Number;
/** Default pathable velocity. */
protected static const DEFAULT_VELOCITY :Number= 0.2;
}
}
+68
View File
@@ -0,0 +1,68 @@
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.util {
/**
* A path is used to cause a {@link Pathable} to follow a particular path along the screen. The
* {@link Pathable} is responsible for calling {@link #tick} on the path with reasonable frequency
* (generally as a part of the frame tick. The path is responsible for updating the position of
* the {@link Pathable} based on the time that has elapsed since the {@link Pathable} started down
* the path.
*
* <p> The path should call the appropriate callbacks on the {@link Pathable} when appropriate
* (e.g. {@link Pathable#pathBeginning}, {@link Pathable#pathCompleted}).
*/
public interface Path
{
/**
* Called once to let the path prepare itself for the process of animating the supplied
* pathable. Path users should also call {@link #tick} after {@link #init} with the same
* initialization timestamp.
*/
function init (pable :Pathable, tickStamp :int) :void;
/**
* Called to request that this path update the position of the specified pathable based on the
* supplied timestamp information. A path should record its initial timestamp and determine
* the progress of the pathable along the path based on the time elapsed since the pathable
* began down the path.
*
* @param pable the pathable whose position should be updated.
* @param tickStamp the timestamp associated with this frame.
*
* @return true if the pathable's position was updated, false if the path determined that the
* pathable should not move at this time.
*/
function tick (pable :Pathable, tickStamp :int) :Boolean;
/**
* This is called if the pathable is paused for some length of time and then unpaused. Paths
* should adjust any time stamps they are maintaining internally by the delta so that time
* maintains the illusion of flowing smoothly forward.
*/
function fastForward (timeDelta :int) :void;
/**
* When a path is removed from a pathable, whether that is because the path was completed or
* because it was replaced by another path, this method will be called to let the path know
* that it is no longer associated with this pathable.
*/
function wasRemoved (pable :Pathable) :void;
}
}
@@ -0,0 +1,52 @@
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.util {
import flash.geom.Point;
/**
* A path node is a single destination point in a {@link Path}.
*/
public class PathNode
{
/** The node coordinates in screen pixels. */
public var loc :Point;
/** The direction to face while heading toward the node. */
public var dir :int;
/**
* Construct a path node object.
*
* @param x the node x-position.
* @param y the node y-position.
* @param dir the facing direction.
*/
public function PathNode (x :Number, y :Number, dir :int)
{
loc = new Point(x, y);
this.dir = dir;
}
public function toString () :String
{
return "[x=" + loc.x + ", y=" + loc.y + ", dir=" + dir + "]";
}
}
}
@@ -0,0 +1,68 @@
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.util {
import flash.geom.Rectangle;
/**
* Used in conjunction with a {@link Path}.
*/
public interface Pathable
{
/**
* Returns the pathable's current x coordinate.
*/
function getX () :Number;
/**
* Returns the pathable's current y coordinate.
*/
function getY () :Number;
/**
* Updates the pathable's current coordinates.
*/
function setLocation (x :Number, y :Number) :void;
/**
* Will be called by a path when it moves the pathable in the
* specified direction. Pathables that wish to face in the direction
* they are moving can take advantage of this callback.
*
* @see DirectionCodes
*/
function setOrientation (orient :int) :void;
/**
* Should return the orientation of the pathable, or {@link
* DirectionCodes#NONE} if the pathable does not support orientation.
*/
function getOrientation () :int;
/**
* Called by a path when this pathable is made to start along a path.
*/
function pathBeginning () :void;
/**
* Called by a path when this pathable finishes moving along its path.
*/
function pathCompleted (timestamp :int) :void;
}
}
@@ -0,0 +1,34 @@
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.util {
/**
* Provides traversibility information when computing paths.
*/
public interface TraversalPred
{
/**
* Requests to know if the specified traverser (which was provided in the call to
* {@link #getPath(TraversalPred,Object,int,int,int,int,int,boolean)}) can traverse the
* specified tile coordinate.
*/
function canTraverse (traverser :Object, x :int, y :int) :Boolean;
}
}
@@ -37,7 +37,7 @@ import flash.events.MouseEvent;
import mx.core.ClassFactory;
import as3isolib.display.primitive.IsoBox;
import as3isolib.core.IsoDisplayObject;
import as3isolib.geom.Pt;
import as3isolib.geom.IsoMath;
import as3isolib.display.scene.IsoScene;
@@ -59,6 +59,11 @@ import com.threerings.media.tile.Colorizer;
import com.threerings.media.tile.NoSuchTileSetError;
import com.threerings.media.tile.TileSet;
import com.threerings.media.tile.TileUtil;
import com.threerings.media.util.AStarPathSearch;
import com.threerings.media.util.AStarPathSearch_Stepper;
import com.threerings.media.util.LineSegmentPath;
import com.threerings.media.util.Path;
import com.threerings.media.util.TraversalPred;
import com.threerings.miso.client.MisoMetricsTransformation;
import com.threerings.miso.client.PrioritizedSceneLayoutRenderer;
import com.threerings.miso.data.MisoSceneModel;
@@ -69,7 +74,7 @@ import com.threerings.miso.util.ObjectSet;
import com.threerings.miso.util.MisoSceneMetrics;
public class MisoScenePanel extends Sprite
implements PlaceView
implements PlaceView, TraversalPred
{
private var log :Log = Log.getLog(MisoScenePanel);
@@ -84,6 +89,8 @@ public class MisoScenePanel extends Sprite
_isoView = new IsoView();
_isoView.setSize(DEF_WIDTH, DEF_HEIGHT);
_vbounds = new Rectangle(0, 0,
_isoView.size.x, _isoView.size.y);
_isoView.addEventListener(MouseEvent.CLICK, onClick);
@@ -279,6 +286,7 @@ public class MisoScenePanel extends Sprite
// First, we add in our new blocks...
for each (var newBlock :SceneBlock in _readyBlocks.toArray()) {
newBlock.render();
newBlock.addToCovered(_covered);
_blocks.put(newBlock.getKey(), newBlock);
}
_readyBlocks = Sets.newSetOf(SceneBlock);
@@ -310,7 +318,7 @@ public class MisoScenePanel extends Sprite
}
}
trace("Scene Block Resolution took: " + (getTimer() - _resStartTime) + "ms");
log.info("Scene Block Resolution took: " + (getTimer() - _resStartTime) + "ms");
// Let's setup some prefetching...
blocks.forEach(function(blockKey :int) :void {
@@ -363,8 +371,10 @@ public class MisoScenePanel extends Sprite
public function canTraverse (traverser :Object, tx :int, ty :int) :Boolean
{
var block :SceneBlock = _blocks.get(SceneBlock.getBlockKey(tx, ty));
return (block == null) ? canTraverseUnresolved(traverser, tx, ty) :
block.canTraverse(traverser, tx, ty);
var baseTraversable :Boolean = (block == null) ? canTraverseUnresolved(traverser, tx, ty) :
block.canTraverseBase(traverser, tx, ty);
return baseTraversable && !_covered.contains(StringUtil.toCoordsString(tx, ty));
}
/**
@@ -384,10 +394,37 @@ public class MisoScenePanel extends Sprite
* sprite in a straight line to its final destination. This is generally only useful if the
* the path goes "off screen".
*/
public function getPath (sprite :Sprite, x :int, y :int, loose :Boolean) :Object // TODO Path
public function getPath (sprite :IsoDisplayObject, x :int, y :int, loose :Boolean) :Path
{
// TODO Path
return null;
// sanity check
if (sprite == null) {
throw new Error("Can't get path for null sprite [x=" + x + ", y=" + y + ".");
}
// compute our longest path from the screen size
var longestPath :int = 3 * (width / _metrics.tilewid);
// get a reasonable tile path through the scene
var start :int = getTimer();
var search :AStarPathSearch = new AStarPathSearch(this, new AStarPathSearch_Stepper());
var points :Array = search.getPath(sprite, longestPath, int(Math.round(sprite.x)),
int(Math.round(sprite.y)), x, y, loose);
// Replace the starting point with the Number values rather than the rounded version...
points[0] = new Point(sprite.x, sprite.y);
var duration :int = getTimer() - start;
// sanity check the number of nodes searched so that we can keep an eye out for bogosity
if (duration > 500) {
log.warning("Considered a lot of nodes for path from " +
StringUtil.toCoordsString(sprite.x, sprite.y) + " to " +
StringUtil.toCoordsString(x, y) +
" [duration=" + duration + "].");
}
// construct a path object to guide the sprite on its merry way
return (points == null) ? null : LineSegmentPath.createWithList(points);
}
protected var _model :MisoSceneModel;
@@ -433,6 +470,10 @@ public class MisoScenePanel extends Sprite
/** If any block happens to resolve should we currently skip calling completion. */
protected var _skipComplete :Boolean
protected var _vbounds :Rectangle;
protected var _covered :Set = Sets.newSetOf(String);
protected const DEF_WIDTH :int = 985;
protected const DEF_HEIGHT :int = 560;
+42 -13
View File
@@ -8,7 +8,10 @@ import as3isolib.display.IsoView;
import com.threerings.util.Log;
import com.threerings.util.MathUtil;
import com.threerings.util.Set;
import com.threerings.util.StringUtil;
import com.threerings.media.tile.BaseTile;
import com.threerings.media.tile.Colorizer;
import com.threerings.media.tile.NoSuchTileSetError;
import com.threerings.media.tile.Tile;
@@ -25,7 +28,7 @@ import com.threerings.miso.util.ObjectSet;
public class SceneBlock
{
private var log :Log = Log.getLog(MisoScenePanel);
private var log :Log = Log.getLog(SceneBlock);
public static const BLOCK_SIZE :int = 4;
@@ -149,6 +152,17 @@ public class SceneBlock
}
}
public function addToCovered (covered :Set) :void
{
for each (var sprite :ObjectTileIsoSprite in _objSprites) {
for (var xx :int = sprite.x; xx < sprite.x + sprite.width; xx++) {
for (var yy :int = sprite.y; yy < sprite.y + sprite.length; yy++) {
covered.add(StringUtil.toCoordsString(xx, yy));
}
}
}
}
/**
* Removes our tiles' sprites from the scenes.
*/
@@ -164,22 +178,29 @@ public class SceneBlock
protected function createBaseSprite (tileSet :TileSet, x :int, y :int, tileId :int,
panel :MisoScenePanel) :void
{
var fringeTile :Tile = panel.computeFringeTile(x, y);
var fringeTile :BaseTile = panel.computeFringeTile(x, y);
var tile :Tile = tileSet.getTile(TileUtil.getTileIndex(tileId));
var tile :BaseTile = BaseTile(tileSet.getTile(TileUtil.getTileIndex(tileId)));
var maybeLoaded :Function = function (loaded :Tile) :void {
if (tile.getImage() != null && (fringeTile == null || fringeTile.getImage() != null)) {
_baseScene.addChild(new BaseTileIsoSprite(x, y, tileId, tile, _metrics,
fringeTile));
noteTileLoaded();
}
};
var bx :int = getBlockX(_key);
var by :int = getBlockY(_key);
_passable[(y - by) * BLOCK_SIZE + (x - bx)] =
tile.isPassable() && (fringeTile == null || fringeTile.isPassable());
if (tile.getImage() != null && (fringeTile == null || fringeTile.getImage() != null)) {
_baseScene.addChild(new BaseTileIsoSprite(x, y, tileId, tile, _metrics, fringeTile));
} else {
noteTileToLoad();
var maybeLoaded :Function = function (loaded :Tile) :void {
if (tile.getImage() != null &&
(fringeTile == null || fringeTile.getImage() != null)) {
_baseScene.addChild(new BaseTileIsoSprite(x, y, tileId, tile, _metrics,
fringeTile));
noteTileLoaded();
}
};
if (tile.getImage() == null) {
tile.notifyOnLoad(maybeLoaded);
}
@@ -209,10 +230,16 @@ public class SceneBlock
}
}
public function canTraverse (traverser :Object, tx :int, ty :int) :Boolean
/**
* Returns whether the base & fringe tiles are traversable here.
*/
public function canTraverseBase (traverser :Object, tx :int, ty :int) :Boolean
{
// TODO Path
return true;
// Only handles the base tiles since the objects cross multiple scene blocks...
var bx :int = getBlockX(_key);
var by :int = getBlockY(_key);
return _passable[(ty - by) * BLOCK_SIZE + (tx - bx)];
}
protected function noteTileToLoad () :void
@@ -234,6 +261,8 @@ public class SceneBlock
}
}
protected var _passable :Array = new Array(BLOCK_SIZE * BLOCK_SIZE);
protected var _baseScene :IsoScene;
protected var _objSprites :Array;