Grandpa,
tell us another story about pathfinding! Well kids, grandma is sleeping in the
kitchen while the gas stove is on, so why not?! Hop on children, but don't sit
there, grandpa had a little accident.
I
already told you a bit about navigation-meshes and A.I. decission making; Where are we? Where to go? What's my purpose in this universe? Finally, there is also the art of actually walking the walk. Like an
Egyptian, 4-legged monster, robot-tank on tracks, Scud Missile, or
whatsoever. Basically I have four more problems to tackle. Yes, quite a bit for
some stupid movement, but don't forget (smart) motion makes up about 50% of the A.I. already,
so it was expected to be tough. Those four challenges are:
- Walking a curvy, smooth, "natural" line
- Not in a robotic zig-zag pattern... unless you are a robot of course...
- Physics
- keeping our boots on the ground, dealing with gravity, making turns, et cetera.
- Dodging dynamic obstacles
- Doors, a crowd, electrified walls, garbage trucks, ...
- Animating our body
- Swinging booty swagger, putting feet on stairsteps using Inverse Kinematics, ...
As for the latter, I didn't do that yet. My friend still hovers like a frozen Rio de Janeiro Jesus statue over the floor. Like Michael Jackson but then frozen in carbonite. The other issues got their attention though. I just "finished" (stuff is never finished, "better" is always around the corner) the "Smooth path" feature for instance.
The
Zig-Zag man
Pathfinding
algorithms often use a grid to perform their search on. Or a navigationMesh,
which again is often grid-shaped, more or less. Then the centers of those
cells/polygons are used as nodePoints; the result of a pathfinding query is
primarily a list of coordinates - if a path was found. If we simply fly
point-to-point, we will reach our target without crossing walls or cheating.
But it will be an utterly clumsy ride, due the nature of the point lay-point:
The not-so-good method: Cheating
I admit, when leaving the bar, I go home in a ZigZag pattern, but those 90 degree turns are deadly. Even though a pathfinding algorithm is supposed to find the shortest path, this doesn't seems to be the shortest. One way to get a more logical result, is cheating. We pedestrians are supposed to cross the road in a straight line, preferably taking the Zebra Crossing. But we all know a diagonal line is shorter...
You can
achieve the same by optimizing your resulting nodeList. Loop through all
points, and see to which points you can shortcut WITHOUT ever leaving the
navigation-mesh. Starting at Node1, let's check if we can reach the last node8
without leaving the mesh... nope. Node 7? No. Node6? Yes we can. So we
can delete Node 2|3|4|5 from our list right away. Notice we cross the very
corner at Node6. That's why its important to "shrink" the
navigation-mesh inwards, away from the walls, to avoid intersections. In other
words, make sure your guy can stand anywhere, in any navigation-mesh polygon, without
bumping the walls or ceilings.
This
will get rid of zome Zig-Zags, but it also introduces a bunch of problems.
First of all, the last 2 nodes still look a bit ackward; we have to go via
(red)#3, because we couldn't shortcut from 2 to 4. And another problem is that
we're leaving our comfort zone (crossing reddish quads). Diagonally crossing the
road might save some meters, but its also more dangerous. Pathfinding isn't
always about finding the *shortest* route! Maybe those green quads in this
picture provide the most covered/stealthy/safest/easiest/bonus-item-filled/awesome/scenic
route... So we spend a lot of effort finding
that, then to throw it all away by skipping nodes? Disgrace! You don't visit London for sight-seeing, then take the Metro only.
Another way: No idea how to call it, but it works
So I
thought of something else, a bit different than you may have red about
smoothing your pathlines with beziers/splines. We move our nodes –within their
original polygons(!)- to generate a optimized path. A shorter, more natural path, but without ever leaving the original found (green)
polygons. Yeah.
I'm not
a math-miracle, so it's all based on simple common-sense, which is a good
thing, I think. We start at Node1. But instead of moving to Node2, we already
try to shortcut, directing towards Node #3:
To avoid
skipping the Quad Node2 was placed on, we have to respect the blue Edge and
cross it, thus getting through that quad. So Node2 gets placed on that blue edge, but as close as possible to
Node3 (and note I shift it a bit back from the very corner). Now repeat the
step; we displace Node3, as we try to find the shortest route from Node2 to #4:
Note how
the turns get a bit more curvy at the quad corners as well. At some points, the
arrow is going to intersect forbidden territory (open space or reddish quads we didn't
enlist in our original search). Whenever that happens, shift the intersection
point back on the edge. That's all really. I didn't try it yet, but possibly you
can smooth a bit further by repeating the whole procedure 1 or more times. So in
pseudo code:
for i=0 to nodeCount-3
// Get the quad our original node was placed on
quad = nodes[i].quad
// Find out via which of the 4 edges we are leaving this quad, to the next node
edge = nodes[i].crossingEdgeToNextNode
e1 = edge.cornerA
e2 = edge.cornerB
// Make a virtual line between the current point, and the node AFTER the next one
p1 = nodes[i].pos
p2 = nodes[i+2].pos
// Check out where we (virtually) intersect - be aware this point may be off the edge-ends!!
// Big Thank you Darel Rex Finley for some math & code: http://alienryderflex.com/intersect/
if ( linesIntersectExtrapolate( e1,e2, p1,p2, @intersectionPoint ) then
// In case the intersectionPoint was "outside" the edge, move it back inwards,
// and stay a bit away from the outer corners
shiftedPoint = lineShiftPointBetweenEnds( intersectionPoint, e1,e2, 0.01 )
// Store in the NEXT node pos - so the next loop iteration will calculate from taht point onward
nodes[i+1].pos = shiftedPoint
end
next
// Get the quad our original node was placed on
quad = nodes[i].quad
// Find out via which of the 4 edges we are leaving this quad, to the next node
edge = nodes[i].crossingEdgeToNextNode
e1 = edge.cornerA
e2 = edge.cornerB
// Make a virtual line between the current point, and the node AFTER the next one
p1 = nodes[i].pos
p2 = nodes[i+2].pos
// Check out where we (virtually) intersect - be aware this point may be off the edge-ends!!
// Big Thank you Darel Rex Finley for some math & code: http://alienryderflex.com/intersect/
if ( linesIntersectExtrapolate( e1,e2, p1,p2, @intersectionPoint ) then
// In case the intersectionPoint was "outside" the edge, move it back inwards,
// and stay a bit away from the outer corners
shiftedPoint = lineShiftPointBetweenEnds( intersectionPoint, e1,e2, 0.01 )
// Store in the NEXT node pos - so the next loop iteration will calculate from taht point onward
nodes[i+1].pos = shiftedPoint
end
next
The
final result is a lot better than the original, though it may still take some
sharp turns here and there. I “fixed” that dynamically, by letting the
character "physics" turn with some delay and speed decreasement,
instead of stopping and taking a perfect turn. Like a car on mud, getting
off-road a bit. Only on very sharp turns the character actually stops, takes a
different "rotation animation", and turns. As you would do yourself
when taking a sudden turn. In other words, the line is not a 100% strict
guider.