engine/ai/graphLocate.cc
2024-01-07 04:36:33 +00:00

530 lines
16 KiB
C++

//-----------------------------------------------------------------------------
// V12 Engine
//
// Copyright (c) 2001 GarageGames.Com
// Portions Copyright (c) 2001 by Sierra Online, Inc.
//-----------------------------------------------------------------------------
#include "ai/graph.h"
#define WaitAfterNearbyFound 11
#define WaitAfterNothingFound 16
// Just want to construct a really good proximity for forcing sorting first below-
static struct BestProx: public NodeProximity{ BestProx(){makeGood();} } sBestProximity;
//-------------------------------------------------------------------------------------
bool NodeProximity::possible() const
{
return (mLateral < 4.0);
}
ProximateNode::ProximateNode(const NodeProximity& p, GraphNode* n)
{
mProximity = p;
mNode = n;
}
static S32 QSORT_CALLBACK varCompare(const void* a,const void* b)
{
ProximateNode * proxA = (ProximateNode*)a;
ProximateNode * proxB = (ProximateNode*)b;
if (proxA->mProximity < proxB->mProximity)
return -1;
else if (proxA->mProximity > proxB->mProximity)
return 1;
else
return 0;
}
void ProximateList::sort()
{
dQsort((void *)address(), size(), sizeof(ProximateNode), varCompare);
}
//-------------------------------------------------------------------------------------
// Ok - "dist" searcher is now a misnomer. It's really a depth searcher, and it's
// only used for the locator.
GraphSearchDist::GraphSearchDist()
{
mBestMatch = NULL;
mPoint.set(0,0,0);
mMaxSearchDist = 100;
setOnDepth(false);
}
void GraphSearchDist::setDistCap(F32 d)
{
mMaxSearchDist = d;
}
// For the graph locator, we actually just want to search out to a certain DEPTH,
// rather than on distance (go a few connections removed).
F32 GraphSearchDist::getEdgeTime(const GraphEdge* edge)
{
if (mSearchOnDepth)
return 1.001f;
else
return edge->mDist;
}
// The tracker needs to know travel distance if the location has left it's last node.
void GraphSearchDist::onQExtraction()
{
GraphNode * extractNode = extractedNode();
if (extractNode->indoor())
{
NodeProximity metric = extractNode->containment(mPoint);
if (metric.inside())
{
mBestMatch = extractNode;
mBestMetric = metric;
mProximate.clear();
setEarlyOut(true);
}
else
{
// We need to track if we've had any really close matches, and then
// disregard the high ones. Note that the definition of "really close"
// must probably account for whether the destination has a bounding box
// or not.
if (metric.possible())
if (metric < mCurThreshold)
{
// Add to the list of possibilities.
ProximateNode candidate(metric, extractNode);
mProximate.push_back(candidate);
if (metric < mBestMetric)
{
mBestMetric = metric;
mCurThreshold = getMax(F32(metric), 1.0f);
}
}
}
F32 thusFar = (mSearchOnDepth ? timeSoFar() : distSoFar());
if (thusFar > mMaxSearchDist)
setEarlyOut(true);
}
}
// Find the best indoor node to connect to, return metric for how good.
// The lower the number the better (using distance from planes).
NodeProximity GraphSearchDist::findBestMatch(GraphNode * current, const Point3F& loc)
{
// Set threshold based on how good the initial match is.
mBestMatch = NULL;
mBestMetric.makeBad();
mProximate.clear();
if (!current)
current = gNavGraph->closestNode(loc);
if (current)
{
setEarlyOut(false);
mPoint = loc;
mCurThreshold = 1e13;
if (current->indoor())
setDistCap(3.3);
else
setDistCap(2.2);
setOnDepth(true);
performSearch(current);
setOnDepth(false);
}
return mBestMetric;
}
//-------------------------------------------------------------------------------------
GraphLocate::GraphLocate()
{
reset();
mMounted = false;
}
void GraphLocate::reset()
{
mLocation.set(1e13,1e13,1e13);
mLoc2D = mLocation;
mTraveled = 1e13;
mClosest = NULL;
mCounter = 0;
mTerrain = false;
mUpInAir = false;
}
void GraphLocate::forceCheck()
{
mCounter = 0;
mTraveled = 1e13;
}
//-------------------------------------------------------------------------------------
static const U32 sMask = InteriorObjectType;
static bool haveLOS(Point3F src, Point3F dst, bool indoor)
{
static RayInfo coll;
src.z += 0.13;
dst.z += 0.13;
// Probably don't usually need collide with terrain at all, but there are some cases
// on proximity check to indoor nodes.
U32 mask = indoor ? (sMask|TerrainObjectType) : sMask;
if (!gServerContainer.castRay(src, dst, mask, &coll))
return true;
else {
// check vehicle collision- ?
return false;
}
}
// We allow a slight increase on the lower height, up to step height (see Generators
// down in Masada bunker - top of steps, fail LOS).
static bool checkLOS(const Point3F& src, const Point3F& dst, bool indoor)
{
if (!haveLOS(src, dst, indoor))
{
// Do more liberal check, raising up the lower point by up to 0.7 (~step height).
Point3F high, low;
if (src.z < dst.z)
low = src, high = dst;
else
low = dst, high = src;
low.z += getMin(0.7f, high.z - low.z);
return haveLOS(low, high, indoor);
}
return true;
}
//-------------------------------------------------------------------------------------
bool GraphLocate::canHookTo(const GraphLocate& other, bool& mustJet) const
{
bool canHook = false;
if (mClosest && other.mClosest)
{
if (onTerrain() && other.onTerrain())
{
canHook = (mClosest == other.mClosest || mClosest->neighbors(other.mClosest));
}
else
{
// might want more checking here (which is why the clause is separate...)
canHook = (mClosest == other.mClosest);
}
}
if (canHook)
mustJet = (mUpInAir || other.mUpInAir);
return canHook;
}
void GraphLocate::setMounted(bool b)
{
if (mMounted != b)
{
mMounted = b;
mClosest = NULL;
forceCheck();
}
}
// I think we can get away with doing the 2D check here since it's established that
// we're Ok in 3D and we're not looking at jet connections. (The reason it's needed
// is because guys (especially heavies) are actually far off the edge in Z on slopes!).
bool GraphLocate::checkRegularEdge(Point3F fromLoc, GraphEdge* edge)
{
GraphNode * destNode = gNavGraph->lookupNode(edge->mDest);
Point3F destLoc = destNode->location();
destLoc.z = fromLoc.z = 0.0f;
LineSegment edgeSeg(fromLoc, destLoc);
//==> If we can preprocess a walking width onto these edges, that could be good.
//==> For now the value is hard coded.
F32 dist = edgeSeg.distance(mLoc2D);
return (dist <= 0.8);
}
ProximateNode * GraphLocate::examineCandidates(ProximateList& proximate)
{
ProximateNode * bestIndoor = NULL;
ProximateNode * bestOutdoor = NULL;
mConnections.clear();
if (proximate.size())
{
proximate.sort();
for (ProximateList::iterator i = proximate.begin(); i != proximate.end(); i++)
{
bool isIndoor = i->mNode->indoor();
if (checkLOS(i->mNode->location(), mLocation, isIndoor))
{
pushEdge(i->mNode);
if (bestIndoor == NULL) // check for return value
{
if (isIndoor)
bestIndoor = i;
else
bestOutdoor = i;
}
}
if (mConnections.size() > 3)
break;
}
}
else
{
#if _GRAPH_WARNINGS_
NavigationGraph::warning("No proximate nodes found in graph locator");
#endif
}
// See if we want final jetting connection, and set that only for the best match.
if (mMounted && mConnections.size())
{
mConnections[0].mBorder = -1;
mConnections[0].setJetting();
}
if (bestIndoor)
return bestIndoor;
else
return bestOutdoor;
}
// Fetch neighbors onto list. Be careful with all the different edge types (jet,
// border edges, walkable straight connections). Routine is only called when we're
// inside the node, so we can connect to all except certain straight line edges.
void GraphLocate::getNeighbors(GraphNode* ofNode, bool outdoor, bool makeJet)
{
static GraphEdge edgeBuffer[MaxOnDemandEdges];
GraphEdgeArray edgeList = ofNode->getEdges(edgeBuffer);
S32 outdoorNumber = gNavGraph->numOutdoor();
Point3F fromLoc = ofNode->location();
if (outdoor) {
while(GraphEdge * edge = edgeList++) {
if(edge->mDest < outdoorNumber) {
GraphEdge connection = * edge;
if (!connection.isSteep())
{
if (makeJet || connection.isJetting())
connection.setJetting();
mConnections.push_back(connection);
}
}
else {
// Check if we're near the edge. Note we know this isn't a 'border' edge
// because terrains nodes don't have border edges going out.
if (!edge->isJetting() && checkRegularEdge(fromLoc, edge))
mConnections.push_back(* edge);
}
}
}
else {
// From indoor node we're inside.
while (GraphEdge * edge = edgeList++) {
if (edge->mDest >= outdoorNumber) { // (indoor dest)
if (makeJet) {
// If we're inside the neighbor's top and bottom planes, we can make
// a jet connection.
if (gNavGraph->verticallyInside(edge->mDest, mLocation)) {
GraphNode * destNode = gNavGraph->lookupNode(edge->mDest);
if (gNavGraph->possibleToJet(mLocation, destNode->location())) {
GraphEdge jetEdge;
jetEdge.mDest = edge->mDest;
jetEdge.setJetting();
jetEdge.mDist = edge->mDist;
jetEdge.copyInverse(edge);
mConnections.push_back(jetEdge);
}
}
}
else {
if (!edge->isJetting())
if (edge->isBorder() || checkRegularEdge(fromLoc, edge))
mConnections.push_back(* edge);
}
}
else { // Neighbor is outdoor-
if (makeJet) {
// Since this indoor node has a terrain neighbor, we know the location
// is above the terain. However the loc could be under top of node
// in the case of some shaded nodes.
GraphNode * destNode = gNavGraph->lookupNode(edge->mDest);
Point3F destLoc = destNode->location();
F32 topZ = (destLoc.z + destNode->terrHeight());
if (mLocation.z < topZ) {
if (gNavGraph->possibleToJet(mLocation, destLoc)) {
mConnections.push_back(* edge);
mConnections.last().setJetting();
}
}
}
else {
// Can check near edge here on other types of connections.
if (!edge->isJetting() && checkRegularEdge(fromLoc, edge))
mConnections.push_back(* edge);
}
}
}
}
}
void GraphLocate::pushEdge(GraphNode * to, F32 * getCosine)
{
GraphEdge edge;
edge.mDest = to->getIndex();
Point3F toLoc = to->location();
edge.mDist = (toLoc -= mLocation).len();
// Added for steepness detection (LH 11/12/00).
if (getCosine)
{
if (edge.mDist < 0.01)
* getCosine = 1.0;
else
{
toLoc.z = 0;
* getCosine = (toLoc.len() / edge.mDist);
}
}
mConnections.push_back(edge);
}
// We're inside this one, so we can hook to it and all neighbors.
void GraphLocate::beLikeNode(GraphNode* ourNode)
{
mConnections.clear();
bool tryJet = false;
bool outdoor = !ourNode->indoor();
if (mMounted) {
if (outdoor) {
F32 height;
if (gNavGraph->terrainHeight(mLocation, &height))
tryJet = (mLocation.z - height) > 2.0;
}
else {
// Handle indoor with volume checks. Since we know we're inside, just
// check the height above the floor.
if (gNavGraph->heightAboveFloor(ourNode->getIndex(), mLocation) > 2.0)
tryJet = true;
}
}
// Get all neighbors of the node we're inside of
getNeighbors(mClosest = ourNode, outdoor, tryJet);
F32 cosAngle = 1.0;
pushEdge(mClosest, outdoor ? &cosAngle : NULL);
if (tryJet || cosAngle < gNavGlobs.mPrettySteepDot)
mConnections.last().setJetting();
mTraveled = 0.0f;
mCounter = 0;
mUpInAir = tryJet;
}
void GraphLocate::update(const Point3F& newLoc)
{
bool searchIsReady = ((--mCounter < 0) && (mTraveled > 0));
F32 moveDist = (mLocation - newLoc).len();
// Update the traveled total
mTraveled += moveDist;
// Check for big jumps - redo search.
// if (moveDist > 30.0)
if (moveDist > 5.0)
mClosest = NULL;
if ((moveDist > 0.1) || searchIsReady)
{
mLocation = newLoc;
mLoc2D.set(mLocation.x, mLocation.y, 0.0);
mUpInAir = false;
if (GraphNode * onTerr = gNavGraph->findTerrainNode(mLocation))
{
mTerrain = true;
mMetric.makeBad();
beLikeNode(onTerr);
}
else
{
mTerrain = false;
NodeProximity newMetric;
// If the node match is good, it means we can afford to search as soon as
// the location leaves that node. Note we can move inside a node that
// we were just near to before, so the connections should be remade.
if (mClosest && (newMetric = mClosest->containment(mLocation)).inside())
{
if (!mMetric.inside())
beLikeNode(mClosest);
else if (mCounter < 0)
{
// Want to re-evaluate 'straight-line' edge connections every so often.
if (mCounter < -4)
beLikeNode(mClosest);
}
else
mCounter = 0;
mMetric = newMetric;
}
else if (searchIsReady)
{
GraphSearchDist * searcher = gNavGraph->getDistSearcher();
mMetric = searcher->findBestMatch(mClosest, mLocation);
if (searcher->bestMatch())
{
beLikeNode(searcher->bestMatch());
}
else
{
// Check proximate list.
ProximateList& nearby = searcher->getProximate();
if (GraphNode * terr = gNavGraph->nearbyTerrainNode(mLocation))
{
ProximateNode willSortFirst(sBestProximity, terr);
nearby.push_back(willSortFirst);
}
if (ProximateNode * bestBet = examineCandidates(nearby))
{
mClosest = bestBet->mNode;
mCounter = WaitAfterNearbyFound;
mTraveled = getMax( static_cast<F32>( bestBet->mProximity ), 0.0f );
}
else
{
// When nothing found, we don't reset the travel distance - this
// will increase the search distance gradually in the case where
// we somehow miss a match, get out of system. Uh, should work..
mClosest = NULL;
mCounter = WaitAfterNothingFound;
}
mCounter += (gRandGen.randI() & 3); // Little bit of stagger
}
}
}
}
}