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

486 lines
15 KiB
C++

//-----------------------------------------------------------------------------
// V12 Engine
//
// Copyright (c) 2001 GarageGames.Com
// Portions Copyright (c) 2001 by Sierra Online, Inc.
//-----------------------------------------------------------------------------
#include "ai/graph.h"
#define RatingsCloseEnough 0.1
JetManager::JetManager()
{
mFloodInds[0] = NULL;
mFloodInds[1] = NULL;
mArmorPart = NULL;
}
JetManager::~JetManager()
{
clear();
}
void JetManager::clear()
{
for (S32 i = 0; i < AbsMaxBotCount; i++)
mPartitions[i].doConstruct();
delete mArmorPart;
delete [] mFloodInds[0];
delete [] mFloodInds[1];
mArmorPart = NULL;
mFloodInds[0] = NULL;
mFloodInds[1] = NULL;
}
// Used for both passes of the armor partitioning. Basically do a flood fill expansion
// with the armor partition telling us what has been "extracted". We use two flipping
// lists for this, each containing the last round of extractions. This routine is then
// used twice - reused for finding downhill.
S32 JetManager::floodPartition(U32 seed, bool needBoth, F32 ratings[2])
{
// Set up the loop-
S32 numFound = 0;
S32 floodSizes[2] = {1, 0};
S32 curExtract = 0, curDeposit;
mArmorPart->set(mFloodInds[0][0] = seed);
// Keep extracting until nothing gets put into next time list
while (floodSizes[curExtract])
{
U16 * extractFrom = mFloodInds[curExtract];
U16 * depositInto = mFloodInds[curDeposit = (curExtract ^ 1)];
for (S32 i = floodSizes[curExtract] - 1; i >= 0; i--)
{
GraphNode * node = gNavGraph->lookupNode(extractFrom[i]);
GraphEdgeArray edges = node->getEdges(NULL);
while (GraphEdge * edge = edges++) {
if (!mArmorPart->test(edge->mDest)) {
if (!edge->isJetting() || edge->canJet(ratings, needBoth)) {
mArmorPart->set(edge->mDest);
depositInto[floodSizes[curDeposit]++] = edge->mDest;
}
}
}
}
numFound += floodSizes[curExtract];
floodSizes[curExtract] = 0;
// Here's the flip. Note the previous line clears deposit size for next loop.
curExtract = curDeposit;
}
return numFound;
}
// Code to find a partition for one armor / energy configuration.
S32 JetManager::partitionOneArmor(JetPartition& list)
{
U32 memUsedBefore = Memory::getMemoryUsed();
S32 nodeCount = gNavGraph->numNodes();
// First time allocations. Also, newIncarnation() will clear them. Partition size
// spends a couple bytes to include transients so search code doesn't have to check.
if (! mArmorPart) {
mArmorPart = new GraphPartition();
mArmorPart->setSize(gNavGraph->numNodesAll());
mFloodInds[0] = new U16 [nodeCount];
mFloodInds[1] = new U16 [nodeCount];
}
S32 seedNode = 0;
S32 total = 0;
Vector<S32> saveSeedNodes;
// PASS 1
//
// First pass finds the 'stranded' component of each partition- this floods along
// only those edges that can be traversed BOTH WAYS.
//
list.clear();
mArmorPart->clear();
while (seedNode < nodeCount)
{
if (!mArmorPart->test(seedNode))
{
total += floodPartition (seedNode, true, list.ratings);
// Add the partition to the list, and remember the seed for next pass.
list.pushPartition(* mArmorPart);
saveSeedNodes.push_back(seedNode);
if (total == nodeCount)
break;
}
seedNode++;
}
// PASS 2
//
// This pass finds the downhill component of each partition, if different. Note that
// setDownhill() only adds a separate partition if the downhill one is different.
//
for (S32 i = 0; i < list.size(); i++)
{
mArmorPart->clear();
floodPartition (saveSeedNodes[i], false, list.ratings);
list[i].setDownhill(* mArmorPart);
}
U32 memUsedAfter = Memory::getMemoryUsed();
if (memUsedAfter > memUsedBefore)
NavigationGraph::sLoadMemUsed += (memUsedAfter - memUsedBefore);
return list.size();
}
//-------------------------------------------------------------------------------------
JetManager::Ability::Ability()
{
acc = dur = v0 = -111;
}
// Check for close enough abilities. So far the only thing I've seen change is the
// duration number.
bool JetManager::Ability::operator==(const Ability& ability)
{
return( mFabs(acc - ability.acc) < 0.01 &&
mFabs(dur - ability.dur) < 0.5 &&
mFabs(v0 - ability.v0) < 0.1
);
}
JetManager::ID::ID()
{
id = -1;
incarnation = -1;
}
void JetManager::ID::reset()
{
if (NavigationGraph::gotOneWeCanUse())
gNavGraph->jetManager().decRefCount(* this);
id = -1;
}
JetManager::ID::~ID()
{
// Could probably just use reset() here...
if (NavigationGraph::gotOneWeCanUse())
gNavGraph->jetManager().decRefCount(* this);
}
void JetManager::JetPartition::doConstruct()
{
ratings[0] = 0.0;
ratings[1] = 0.0;
users = 0;
used = false;
time = 0;
}
JetManager::JetPartition::JetPartition()
{
doConstruct();
}
//-------------------------------------------------------------------------------------
void JetManager::decRefCount(const ID& id)
{
if (id.id >= 0) {
JetPartition & jetPartition = mPartitions[id.id];
AssertFatal(jetPartition.users > 0, "JetManager ID management is not in sync");
// This is when it starts to grow old-
if (--jetPartition.users == 0)
jetPartition.time = Sim::getCurrentTime();
}
}
// Users update this with an id and a rating. Return true if a new one is created,
// telling consumer (nav path) to try to put off slow operations a little bit.
bool JetManager::update(ID& id, const Ability& ability)
{
// If missions cycles, or graph is rebuilt, id is out of date and should be reset-
if (id.incarnation != gNavGraph->incarnation())
{
id.incarnation = gNavGraph->incarnation();
id.id = -1;
}
// If new entry or no longer valid, find match or create new.
if (id.id < 0 || !(mPartitions[id.id].ability == ability))
{
decRefCount(id);
// Look for existing partition that matches this ability. We keep unused ones-
// If all have had abilities set on them, then take oldest one.
U32 oldAge = U32(-1);
JetPartition * slot = NULL;
JetPartition * oldest = NULL;
JetPartition * j = mPartitions;
for (S32 i = 0; i < AbsMaxBotCount; i++, j++)
{
if (j->ability == ability) {
j->users++;
id.id = i;
return false;
}
else {
// Look for free entry in case it's needed. First choice are those not yet
// in use. Second choice is the oldest one (smallest creation timestamp).
if (!slot && !j->users) {
if (!j->used)
slot = j;
else if (j->time < oldAge)
oldAge = j->time, oldest = j;
}
}
}
// No match found- create a new one with the slot found.
if (!slot)
slot = oldest;
AssertFatal(slot != NULL, "JetManager couldn't find empty slot");
id.id = (slot - mPartitions);
slot->ability = ability;
slot->users = 1;
slot->used = true;
calcJetRatings(slot->ratings, ability);
slot->setType(GraphPartition::Armor);
// Do the work-
partitionOneArmor(* slot);
// Inform caller that we did some slow work-
return true;
}
return false;
}
//-------------------------------------------------------------------------------------
// Here's the math. We've done the phsyics math accurately for the vertical distance,
// but we're just tweaking it for the lateral travel distance, which looks in practice
// to be about the same. That is, a bot can jet a flat XY distance that is close to
// how high straight up it can jet. But we scale down the lateral (increase what it
// _thinks_ it has to do) since this is just a heuristic. I'm not sure what the exact
// math is for estimating how far a bot can jet in any arc...
#define LateralExtra 2.00f
#define LateralScale 1.28f
F32 JetManager::modifiedLateral(F32 lateralDist)
{
return (lateralDist * LateralScale + LateralExtra);
}
F32 JetManager::modifiedDistance(F32 lateralDist, F32 verticalDist)
{
return verticalDist + modifiedLateral(lateralDist);
}
// In one case the public needs the actual XY from the lateral field, so this function
// is the inverse of modifiedLateral().
F32 JetManager::invertLateralMod(F32 lateralField)
{
F32 result = (lateralField - LateralExtra) * (1.0 / LateralScale);
return getMax(0.0f, result);
}
//-------------------------------------------------------------------------------------
// Public function that the aiNavJetting module uses for knowing when it has enough
// energy to launch. That code must use the same adjusted distance that
// the jetting edges use.
F32 JetManager::jetDistance(const Point3F& from, const Point3F& to) const
{
F32 vertical = (to.z - from.z);
Point2F lateral(to.x - from.x, to.y - from.y);
if (vertical > 0)
return modifiedDistance(lateral.len(), vertical);
else
return modifiedLateral(lateral.len());
}
//-------------------------------------------------------------------------------------
//
// All edge initialization comes through here, especially required since the
// partitioning needs at least one invariants holding on the edges, namely that
// the mDist field of a pair of edges between two nodes must be exactly the same.
//
void JetManager::initEdge(GraphEdge& edge, const GraphNode* src, const GraphNode* dst)
{
// The jet scale probably needs a little bit of work (and it seems like it really
// depends on the bot's energy levels. Any hop that uses less than half the
// bot's energy would probably be a good one to take, all else being equal.
if (!edge.isJetting())
edge.setInverse(src->edgeScale(dst));
else
edge.setInverse(calcJetScale(src, dst));
// Must assure that distances are the same both ways, so just to be sure we
// sort the locations. Partitioning flooding relies on this.
const Point3F& srcLoc = src->location();
const Point3F& dstLoc = dst->location();
Point3F high;
Point3F low;
if (srcLoc.z > dstLoc.z)
{
high = srcLoc;
low = dstLoc;
edge.setDown(edge.isJetting());
}
else
{
high = dstLoc;
low = srcLoc;
}
// Distance on edge is sum of horizontal + some extra + vertical. The extra
// is a buffer so they can get the lateral velicity going.
F32 absHeight = (high.z - low.z);
(high -= low).z = 0;
F32 lateral = high.len();
// edge.mDist = (lateral + absHeight + 2.0);
edge.mDist = modifiedDistance(lateral, absHeight);
// Get lateral distance for hops downward, little extra for buffer.
if (edge.isDown())
edge.setLateral(U8(modifiedLateral(lateral)));
// Set flatness. Must be conservative with it so that edges evaluate the
// same both ways for partioning purposes.
if (src->flat() && dst->flat())
edge.setJump(true);
}
// Ok - the indices are backwards from what's intuitive, but it's not a bug
// per se. We'll change to the intuitive if there's a convenient rebuild time...
// cf. The GraphBridgeData constructor.
void JetManager::replaceEdge(GraphBridgeData & B)
{
GraphNode * src = gNavGraph->lookupNode(B.nodes[1]);
GraphNode * dst = gNavGraph->lookupNode(B.nodes[0]);
GraphEdge * edge = src->getEdgeTo(dst);
AssertFatal(src && dst && edge, "JetManager::replaceEdge()");
if (B.isUnreachable())
{
edge->setSteep(true);
edge->setJetting();
edge->setImpossible();
}
else
{
edge->setJetting();
edge->setHop(B.jetClear);
initEdge(* edge, src, dst);
}
}
// Calculates scale factor on jetting edges for deciding when to do them.
F32 JetManager::calcJetScale(const GraphNode* from, const GraphNode* to) const
{
F32 scale;
F32 zdiff = (to->location().z - from->location().z);
if (zdiff > 0) {
// Jetting up adds to factor. Base amount is 3.0, then we
// also add 1 for every 10 meters going up beyond 20.
F32 per10 = mapValueLinear(zdiff, 20.0f, 120.0f, 0.0f, 10.0f);
scale = 3.0 + per10;
}
else {
// We'll make this number better once we have detection of
// which are walkable (walk-off-able) and which aren't.
scale = 1.4;
}
return scale;
}
#define GravityNum 0.625
// Given thrust and duration, see how far we can go.
F32 JetManager::calcJetRating(F32 thrust, F32 duration)
{
F32 tSqrd = duration * duration;
thrust -= GravityNum;
// How far we travel while applying the continuous force-
F32 pureJetDist = (0.5 * thrust * tSqrd * TickSec);
// Once jet is gone- final velocity carries up. More jet remains, but that's our
// reserve so we can be sure to cover the distance.
F32 finalVel = (thrust * duration);
F32 driftUp = (0.5 * finalVel / GravityNum) * finalVel;
return (pureJetDist + driftUp * TickSec);
}
// Calculate both ratings:
// 0 - without jumping
// 1 - can jump
//
// The ratings are a little bit conservative, but we need that buffer since the math
// isn't perfect for long hops. Mainly, we assume duration lasts as long as
// energy, whereas you actually get a little bit more from recharge- that's our buffer.
//
void JetManager::calcJetRatings(F32 * ratings, const Ability& ability)
{
ratings[0] = calcJetRating(ability.acc, ability.dur);
ratings[1] = ratings[0] + (ability.v0 * ability.dur * TickSec);
}
const F32 * JetManager::getRatings(const ID& jetCaps)
{
JetPartition & J = mPartitions[jetCaps.id];
AssertFatal(J.users > 0, "Bad call to JetManager::getRatings()");
return J.ratings;
}
GraphPartition::Answer JetManager::reachable(const ID& jetCaps, S32 from, S32 to)
{
AssertFatal(jetCaps.id >= 0, "Uninitialized ID given to JetManager::reachable()");
return mPartitions[jetCaps.id].reachable(from, to);
}
//-------------------------------------------------------------------------------------
// Estimate what percentage of energy is needed to make the given hop. Actually going
// from distance/ability to energy required is not performed anywhere, and instead of
// trying to invert the math, we'll just do a quick binary search. This only happens
// once when an edge is encountered in a path. And we save square roots! Ok I'm lazy.
F32 JetManager::estimateEnergy(const ID& jetCaps, const GraphEdge * edge)
{
JetPartition & J = mPartitions[jetCaps.id];
AssertFatal(J.users > 0, "JetManager::estimateEnergy()");
Ability interpAbility = J.ability;
F32 lower = 0.0;
F32 upper = 1.0;
F32 ratings[2];
// Eight iterations will get us within 1%
for (S32 i = 0; i < 8; i++)
{
F32 interpPercent = ((lower + upper) * 0.5);
interpAbility.dur = (interpPercent * J.ability.dur);
calcJetRatings(ratings, interpAbility);
if (edge->canJet(ratings))
upper = interpPercent;
else
lower = interpPercent;
}
// This is the lowest percentage we found which still works (or it's 1.0).
return upper;
}