From 3b0b6038dcba99a08bbee2c2fb4f1b430dd6f834 Mon Sep 17 00:00:00 2001 From: Mike Thomas Date: Fri, 13 Aug 2010 20:28:48 +0000 Subject: [PATCH] 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 --- src/as/com/threerings/cast/CharacterSprite.as | 10 +- .../threerings/media/util/AStarPathSearch.as | 131 ++++++ .../media/util/AStarPathSearch_Info.as | 178 +++++++++ .../media/util/AStarPathSearch_Node.as | 99 +++++ .../media/util/AStarPathSearch_Stepper.as | 77 ++++ .../media/util/ExtendedTraversalPred.as | 34 ++ .../threerings/media/util/LineSegmentPath.as | 373 ++++++++++++++++++ src/as/com/threerings/media/util/Path.as | 68 ++++ src/as/com/threerings/media/util/PathNode.as | 52 +++ src/as/com/threerings/media/util/Pathable.as | 68 ++++ .../threerings/media/util/TraversalPred.as | 34 ++ .../threerings/miso/client/MisoScenePanel.as | 57 ++- .../com/threerings/miso/client/SceneBlock.as | 55 ++- 13 files changed, 1213 insertions(+), 23 deletions(-) create mode 100644 src/as/com/threerings/media/util/AStarPathSearch.as create mode 100644 src/as/com/threerings/media/util/AStarPathSearch_Info.as create mode 100644 src/as/com/threerings/media/util/AStarPathSearch_Node.as create mode 100644 src/as/com/threerings/media/util/AStarPathSearch_Stepper.as create mode 100644 src/as/com/threerings/media/util/ExtendedTraversalPred.as create mode 100644 src/as/com/threerings/media/util/LineSegmentPath.as create mode 100644 src/as/com/threerings/media/util/Path.as create mode 100644 src/as/com/threerings/media/util/PathNode.as create mode 100644 src/as/com/threerings/media/util/Pathable.as create mode 100644 src/as/com/threerings/media/util/TraversalPred.as diff --git a/src/as/com/threerings/cast/CharacterSprite.as b/src/as/com/threerings/cast/CharacterSprite.as index c04ffbad..cfe6f044 100644 --- a/src/as/com/threerings/cast/CharacterSprite.as +++ b/src/as/com/threerings/cast/CharacterSprite.as @@ -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 { diff --git a/src/as/com/threerings/media/util/AStarPathSearch.as b/src/as/com/threerings/media/util/AStarPathSearch.as new file mode 100644 index 00000000..eec39439 --- /dev/null +++ b/src/as/com/threerings/media/util/AStarPathSearch.as @@ -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 Point objects representing a path from coordinates + * (ax, by) to (bx, by), 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; +} +} + diff --git a/src/as/com/threerings/media/util/AStarPathSearch_Info.as b/src/as/com/threerings/media/util/AStarPathSearch_Info.as new file mode 100644 index 00000000..07e58939 --- /dev/null +++ b/src/as/com/threerings/media/util/AStarPathSearch_Info.as @@ -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 (n.x, n.y) to (x, y) 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); +} +} \ No newline at end of file diff --git a/src/as/com/threerings/media/util/AStarPathSearch_Node.as b/src/as/com/threerings/media/util/AStarPathSearch_Node.as new file mode 100644 index 00000000..f92069b0 --- /dev/null +++ b/src/as/com/threerings/media/util/AStarPathSearch_Node.as @@ -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 Point 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; +} +} \ No newline at end of file diff --git a/src/as/com/threerings/media/util/AStarPathSearch_Stepper.as b/src/as/com/threerings/media/util/AStarPathSearch_Stepper.as new file mode 100644 index 00000000..cff6303a --- /dev/null +++ b/src/as/com/threerings/media/util/AStarPathSearch_Stepper.as @@ -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 (ax, ay) to + * (bx, by). + */ + 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)); +} +} \ No newline at end of file diff --git a/src/as/com/threerings/media/util/ExtendedTraversalPred.as b/src/as/com/threerings/media/util/ExtendedTraversalPred.as new file mode 100644 index 00000000..2a571df3 --- /dev/null +++ b/src/as/com/threerings/media/util/ExtendedTraversalPred.as @@ -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; +} +} \ No newline at end of file diff --git a/src/as/com/threerings/media/util/LineSegmentPath.as b/src/as/com/threerings/media/util/LineSegmentPath.as new file mode 100644 index 00000000..26e906d7 --- /dev/null +++ b/src/as/com/threerings/media/util/LineSegmentPath.as @@ -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 (x1, y1) with (x2, + * y2). 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; +} +} diff --git a/src/as/com/threerings/media/util/Path.as b/src/as/com/threerings/media/util/Path.as new file mode 100644 index 00000000..8169b7ef --- /dev/null +++ b/src/as/com/threerings/media/util/Path.as @@ -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. + * + *

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; +} +} \ No newline at end of file diff --git a/src/as/com/threerings/media/util/PathNode.as b/src/as/com/threerings/media/util/PathNode.as new file mode 100644 index 00000000..508eaba2 --- /dev/null +++ b/src/as/com/threerings/media/util/PathNode.as @@ -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 + "]"; + } +} +} \ No newline at end of file diff --git a/src/as/com/threerings/media/util/Pathable.as b/src/as/com/threerings/media/util/Pathable.as new file mode 100644 index 00000000..62406dd4 --- /dev/null +++ b/src/as/com/threerings/media/util/Pathable.as @@ -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; +} +} \ No newline at end of file diff --git a/src/as/com/threerings/media/util/TraversalPred.as b/src/as/com/threerings/media/util/TraversalPred.as new file mode 100644 index 00000000..c78fc94b --- /dev/null +++ b/src/as/com/threerings/media/util/TraversalPred.as @@ -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; +} +} \ No newline at end of file diff --git a/src/as/com/threerings/miso/client/MisoScenePanel.as b/src/as/com/threerings/miso/client/MisoScenePanel.as index 9d5289e0..dc4582eb 100644 --- a/src/as/com/threerings/miso/client/MisoScenePanel.as +++ b/src/as/com/threerings/miso/client/MisoScenePanel.as @@ -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; diff --git a/src/as/com/threerings/miso/client/SceneBlock.as b/src/as/com/threerings/miso/client/SceneBlock.as index 7c2529fe..13434c5a 100644 --- a/src/as/com/threerings/miso/client/SceneBlock.as +++ b/src/as/com/threerings/miso/client/SceneBlock.as @@ -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;