Ah. That’s mostly just what it says in the thread title: it’s precomputed. Almost all of the heavy lifting happens in a PreinitObject
, which in T3 means it’s executed at compile time instead of at run time (except in debugging builds).
So during a player turn, all that’s happening is some table lookups, basically nothing is ever computed except when a) a zone is flagged for re-evaluation, and b) when something fails for some reason (and I don’t think there are any non-bug cases where this’ll happen, but if it does it should fall back to “regular” pathfinding).
If you’re looking for more of the nuts and bolts, what happens under the hood is, basically:
- The router iterates through all the rooms, identifying which zones have been declared. At the same time it adds a default zone identifier to any rooms that were declared without one.
- The router then creates a graph in which each zone is a vertex and the edges are connections between pairs of rooms that aren’t in the same zone (one room is in one zone, the other is in a different zone). Call each such pair a bridge.
- The router then goes through each declared zone, creating a graph of all of the rooms in the zone, and uses the graph to generate a next-hop table for every node in the zone in relation to every other node in the zone. That is, it loops through the rooms in the zone, and for each one it computes the path between the current room and every other room in the zone, and then it remembers just the next step in the path (after itself).
Then at runtime any time there’s a pathfinding request:
- The router makes the source endpoint the current node
- It looks up the next hop between the current node and the destination endpoint
- If the current node and the destination endpoint are in the same zone, then it looks up the next hop to the destination in the current node’s table, makes that the current node, and loops back to the second step
- If the current node and the destination endpoint aren’t in the same zone, then it figures out which zone is the next step between the current zone and the zone the destination is in (this is cached in precisely the same way as room pathing is), and then it looks up the bridge between the current zone and the next zone
- If the near side bridge location happens to be the current node, then the next hop is the “far side” location in the next zone, make it the current node and go back to step two
- Otherwise we look up the next hop toward the near side bridge location, make it the current node, and go back to step two
This all happens in routeTableRouter.t
, mostly in findpath()
, which builds the return value containing the path:
// Returns the path, if any, between the given two vertices.
findPath(v0, v1) {
local r, v;
// A vector to hold the path.
r = new Vector();
// The first step on the path is always the starting vertex.
v = v0;
// We keep iterating until we get a nil vertex.
while(v != nil) {
// First, add the vertex to our path.
r.append(v);
// If the current vertex is the destination vertex,
// the we're done. Immediately return the path.
if(v == v1)
return(r);
// Get the next step in the path.
v = getRouteTableNextHop(v, v1);
}
// Return the path. We only reach here if pathing failed.
return(r);
}
…and the next hop selection happens in getRouteTableNextHop()
:
// Get the next hop from the first vertex to the second.
getRouteTableNextHop(v0, v1) {
local b, p, l, o, v;
_debug('computing next hop from <<v0.routeTableID>>
to <<v1.routeTableID>>');
// If both rooms are the in same zone, just ask the
// room what the next hop is (it should be precomputed).
if(v0.routeTableZone == v1.routeTableZone) {
_debug('returning precomputed next hop');
o = getRouteTableZone(v0.routeTableZone);
v = o.getVertex(v0.routeTableID);
return(v.getRouteTableNextHop(v1.routeTableID));
}
// Get the path from the zone the source room is in to
// the zone the destination room is in.
l = getRouteTableZonePath(v0.routeTableZone, v1.routeTableZone);
if((l == nil) || (l.length < 2)) {
_debug('no path between zones
<q><<v0.routeTableZone>></q>
and <q><<v1.routeTableZone>></q>');
return(nil);
}
_debug('next hop zone = <<l[2]>>');
// Get the source zone.
v = getRouteTableZone(v0.routeTableZone);
// Look up the bridge between the zone we're in and the
// next zone in the path.
if((b = v.getRouteTableBridge(l[2])) == nil) {
// No bridge, so now we see if there's a static
// route defined.
// First we get the vertex for the source zone.
v = getVertex(v0.routeTableZone);
// We check to see if the source zone has a
// static route for the next zone in the path.
// If not, we're done, fail.
if((p = v.getRouteTableStatic(l[2])) == nil)
return(nil);
// We got a static route, so we try pathing
// from the source vertex to the static
// bridge vertex.
return(getRouteTableNextHop(v0, p));
}
// If there's only one bridge, we use it. Otherwise
// we check the path length through each bridge and
// select the shortest.
// In both cases p will contain a single bridge, which
// is a two-element array: first element is the near-side
// vertex in the bridge, second element is the far-side
// vertex.
if(b.length == 1)
p = b[1];
else
p = selectRouteTableBridge(v0, v1, b);
// We couldn't figure anything out, fail.
if(p == nil)
return(nil);
// If the near-side bridge vertex is the source vertex,
// then the next step is the far-side vertex.
if(p[1] == v0)
return(p[2]);
// We DIDN'T match any bridge endpoints, so instead
// we path to a near-side bridge endpoint.
_debug('pathing to near side of zone bridge');
return(getRouteTableNextHop(v0, p[1]));
}
This also includes a couple bits I didn’t describe above: the “static route” logic, which allows you to declare a place to route to in a source zone if a valid path to the requested destination doesn’t exist, and disambiguating if there are multiple bridges between zones.
This seems like it takes a bit of explaining, but the thing to note is that all of the computational stuff…under the hood it’s plain old Dijkstra…happens at compile time, and all the other stuff is just lookups, which happen pretty quick.
Like the resolution of a timestamp in T3 is around a millisecond, so in order to have pathfinding slow enough that the elapsed time is consistently recorded as more than zero, I have to run tests in batches of thousands of lookups. Doing the same for T3’s buildin roomPathFinder
logic ends up taking tens of seconds.