You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
480 lines
11 KiB
480 lines
11 KiB
#include "bot_common.h" |
|
|
|
const float updateTimesliceDuration = 0.5f; |
|
|
|
int _navAreaCount = 0; |
|
int _currentIndex = 0; |
|
|
|
inline CNavNode *LadderEndSearch(CBaseEntity *entity, const Vector *pos, NavDirType mountDir) |
|
{ |
|
Vector center = *pos; |
|
AddDirectionVector(¢er, mountDir, HalfHumanWidth); |
|
|
|
// Test the ladder dismount point first, then each cardinal direction one and two steps away |
|
|
|
for (int d = (-1); d < 2 * NUM_DIRECTIONS; ++d) |
|
{ |
|
Vector tryPos = center; |
|
|
|
if (d >= NUM_DIRECTIONS) |
|
AddDirectionVector(&tryPos, (NavDirType)(d - NUM_DIRECTIONS), GenerationStepSize * 2.0f); |
|
else if (d >= 0) |
|
AddDirectionVector(&tryPos, (NavDirType)d, GenerationStepSize); |
|
|
|
// step up a rung, to ensure adjacent floors are below us |
|
tryPos.z += GenerationStepSize; |
|
SnapToGrid(&tryPos); |
|
|
|
// adjust height to account for sloping areas |
|
Vector tryNormal; |
|
if (GetGroundHeight(&tryPos, &tryPos.z, &tryNormal) == false) |
|
continue; |
|
|
|
// make sure this point is not on the other side of a wall |
|
const float fudge = 2.0f; |
|
TraceResult result; |
|
UTIL_TraceLine(center + Vector(0, 0, fudge), tryPos + Vector(0, 0, fudge), ignore_monsters, dont_ignore_glass, ENT(entity->pev), &result); |
|
|
|
if (result.flFraction != 1.0f || result.fStartSolid) |
|
continue; |
|
|
|
// if no node exists here, create one and continue the search |
|
if (CNavNode::GetNode(&tryPos) == NULL) |
|
{ |
|
return new CNavNode(&tryPos, &tryNormal, NULL); |
|
} |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
CNavNode *CCSBot::AddNode(const Vector *destPos, const Vector *normal, NavDirType dir, CNavNode *source) |
|
{ |
|
// check if a node exists at this location |
|
CNavNode *node = const_cast<CNavNode *>(CNavNode::GetNode(destPos)); |
|
|
|
// if no node exists, create one |
|
bool useNew = false; |
|
if (node == NULL) |
|
{ |
|
node = new CNavNode(destPos, normal, source); |
|
useNew = true; |
|
} |
|
|
|
// connect source node to new node |
|
source->ConnectTo(node, dir); |
|
|
|
// optimization: if deltaZ changes very little, assume connection is commutative |
|
const float zTolerance = 10.0f; // 50.0f; |
|
if (fabs(source->GetPosition()->z - destPos->z) < zTolerance) |
|
{ |
|
node->ConnectTo(source, OppositeDirection(dir)); |
|
node->MarkAsVisited(OppositeDirection(dir)); |
|
} |
|
|
|
if (useNew) |
|
{ |
|
// new node becomes current node |
|
m_currentNode = node; |
|
} |
|
|
|
Vector ceiling; |
|
Vector floor; |
|
TraceResult result; |
|
|
|
bool crouch = false; |
|
const float epsilon = 0.1f; |
|
|
|
for (float y = -16.0f; y <= 16.0f + epsilon; y += 16.0f) |
|
{ |
|
for (float x = -16.0f; x <= 16.0f + epsilon; x += 16.0f) |
|
{ |
|
floor = *destPos + Vector(x, y, 5.0f); |
|
ceiling = *destPos + Vector(x, y, 72.0f - epsilon); |
|
|
|
UTIL_TraceLine(floor, ceiling, ignore_monsters, dont_ignore_glass, ENT(pev), &result); |
|
|
|
if (result.flFraction != 1.0f) |
|
{ |
|
crouch = true; |
|
break; |
|
} |
|
} |
|
|
|
if (crouch) |
|
{ |
|
node->SetAttributes(NAV_CROUCH); |
|
break; |
|
|
|
} |
|
} |
|
|
|
return node; |
|
} |
|
|
|
void drawProgressMeter(float progress, char *title) |
|
{ |
|
#if 0 |
|
MESSAGE_BEGIN(MSG_ALL, gmsgBotProgress); |
|
WRITE_BYTE(FLAG_PROGRESS_DRAW); |
|
WRITE_BYTE((int)progress); |
|
WRITE_STRING(title); |
|
MESSAGE_END(); |
|
#endif |
|
} |
|
|
|
void startProgressMeter(const char *title) |
|
{ |
|
#if 0 |
|
MESSAGE_BEGIN(MSG_ALL, gmsgBotProgress); |
|
WRITE_BYTE(FLAG_PROGRESS_START); |
|
WRITE_STRING(title); |
|
MESSAGE_END(); |
|
#endif |
|
} |
|
|
|
void hideProgressMeter() |
|
{ |
|
#if 0 |
|
MESSAGE_BEGIN(MSG_ALL, gmsgBotProgress); |
|
WRITE_BYTE(FLAG_PROGRESS_HIDE); |
|
MESSAGE_END(); |
|
#endif |
|
} |
|
|
|
void CCSBot::StartLearnProcess() |
|
{ |
|
startProgressMeter("Analyzing map geometry..."); |
|
drawProgressMeter(0, "Analyzing map geometry..."); |
|
BuildLadders(); |
|
|
|
Vector normal; |
|
Vector pos = pev->origin; |
|
|
|
SnapToGrid(&pos.x); |
|
SnapToGrid(&pos.y); |
|
|
|
if (!GetGroundHeight(&pos, &pos.z, &normal)) |
|
{ |
|
CONSOLE_ECHO("ERROR: Start position invalid\n\n"); |
|
m_processMode = PROCESS_NORMAL; |
|
return; |
|
} |
|
|
|
m_currentNode = new CNavNode(&pos, &normal); |
|
m_goalPosition = pev->origin; |
|
m_processMode = PROCESS_LEARN; |
|
} |
|
|
|
// Search the world and build a map of possible movements. |
|
// The algorithm begins at the bot's current location, and does a recursive search |
|
// outwards, tracking all valid steps and generating a directed graph of CNavNodes. |
|
// Sample the map one "step" in a cardinal direction to learn the map. |
|
// Returns true if sampling needs to continue, or false if done. |
|
|
|
bool CCSBot::LearnStep() |
|
{ |
|
// take a step |
|
while (true) |
|
{ |
|
if (m_currentNode == NULL) |
|
{ |
|
// search is exhausted - continue search from ends of ladders |
|
FOR_EACH_LL (TheNavLadderList, it) |
|
{ |
|
CNavLadder *ladder = TheNavLadderList[it]; |
|
|
|
// check ladder bottom |
|
if ((m_currentNode = LadderEndSearch(ladder->m_entity, &ladder->m_bottom, ladder->m_dir)) != 0) |
|
break; |
|
|
|
// check ladder top |
|
if ((m_currentNode = LadderEndSearch(ladder->m_entity, &ladder->m_top, ladder->m_dir)) != 0) |
|
break; |
|
} |
|
|
|
if (m_currentNode == NULL) |
|
{ |
|
// all seeds exhausted, sampling complete |
|
GenerateNavigationAreaMesh(); |
|
return false; |
|
} |
|
} |
|
|
|
// Take a step from this node |
|
for (int dir = NORTH; dir < NUM_DIRECTIONS; dir++) |
|
{ |
|
if (!m_currentNode->HasVisited((NavDirType)dir)) |
|
{ |
|
float feetOffset = pev->origin.z - GetFeetZ(); |
|
|
|
// start at current node position |
|
Vector pos = *m_currentNode->GetPosition(); |
|
|
|
// snap to grid |
|
int cx = SnapToGrid(pos.x); |
|
int cy = SnapToGrid(pos.y); |
|
|
|
// attempt to move to adjacent node |
|
switch (dir) |
|
{ |
|
case NORTH: cy -= GenerationStepSize; break; |
|
case SOUTH: cy += GenerationStepSize; break; |
|
case EAST: cx += GenerationStepSize; break; |
|
case WEST: cx -= GenerationStepSize; break; |
|
} |
|
|
|
pos.x = cx; |
|
pos.y = cy; |
|
|
|
m_generationDir = (NavDirType)dir; |
|
|
|
// mark direction as visited |
|
m_currentNode->MarkAsVisited(m_generationDir); |
|
|
|
// test if we can move to new position |
|
TraceResult result; |
|
Vector from, to; |
|
|
|
// modify position to account for change in ground level during step |
|
to.x = pos.x; |
|
to.y = pos.y; |
|
Vector toNormal; |
|
if (GetGroundHeight(&pos, &to.z, &toNormal) == false) |
|
{ |
|
return true; |
|
} |
|
|
|
from = *m_currentNode->GetPosition(); |
|
|
|
Vector fromOrigin = from + Vector(0, 0, feetOffset); |
|
Vector toOrigin = to + Vector(0, 0, feetOffset); |
|
|
|
UTIL_SetOrigin(pev, toOrigin); |
|
UTIL_TraceLine(fromOrigin, toOrigin, ignore_monsters, dont_ignore_glass, ENT(pev), &result); |
|
|
|
bool walkable; |
|
|
|
if (result.flFraction == 1.0f && !result.fStartSolid) |
|
{ |
|
// the trace didnt hit anything - clear |
|
float toGround = to.z; |
|
float fromGround = from.z; |
|
|
|
float epsilon = 0.1f; |
|
|
|
// check if ledge is too high to reach or will cause us to fall to our death |
|
if (toGround - fromGround > JumpCrouchHeight + epsilon || fromGround - toGround > DeathDrop) |
|
{ |
|
walkable = false; |
|
} |
|
else |
|
{ |
|
// check surface normals along this step to see if we would cross any impassable slopes |
|
Vector delta = to - from; |
|
const float inc = 2.0f; |
|
float along = inc; |
|
bool done = false; |
|
float ground; |
|
Vector normal; |
|
|
|
walkable = true; |
|
|
|
while (!done) |
|
{ |
|
Vector p; |
|
|
|
// need to guarantee that we test the exact edges |
|
if (along >= GenerationStepSize) |
|
{ |
|
p = to; |
|
done = true; |
|
} |
|
else |
|
{ |
|
p = from + delta * (along / GenerationStepSize); |
|
} |
|
|
|
if (GetGroundHeight(&p, &ground, &normal) == false) |
|
{ |
|
walkable = false; |
|
break; |
|
} |
|
|
|
// check for maximum allowed slope |
|
if (normal.z < 0.7f) |
|
{ |
|
walkable = false; |
|
break; |
|
} |
|
|
|
along += inc; |
|
} |
|
} |
|
} |
|
// TraceLine hit something... |
|
else |
|
{ |
|
if (IsEntityWalkable(VARS(result.pHit), WALK_THRU_EVERYTHING)) |
|
{ |
|
walkable = true; |
|
} |
|
else |
|
{ |
|
walkable = false; |
|
} |
|
} |
|
|
|
// if we're incrementally generating, don't overlap existing nav areas |
|
CNavArea *overlap = TheNavAreaGrid.GetNavArea(&to, HumanHeight); |
|
if (overlap != NULL) |
|
{ |
|
walkable = false; |
|
} |
|
if (walkable) |
|
{ |
|
// we can move here |
|
// create a new navigation node, and update current node pointer |
|
CNavNode *newNode = AddNode(&to, &toNormal, m_generationDir, m_currentNode); |
|
} |
|
|
|
return true; |
|
} |
|
} |
|
|
|
// all directions have been searched from this node - pop back to its parent and continue |
|
m_currentNode = m_currentNode->GetParent(); |
|
} |
|
} |
|
|
|
void CCSBot::UpdateLearnProcess() |
|
{ |
|
float startTime = g_engfuncs.pfnTime(); |
|
while (g_engfuncs.pfnTime() - startTime < updateTimesliceDuration) |
|
{ |
|
if (LearnStep() == false) |
|
{ |
|
StartAnalyzeAlphaProcess(); |
|
return; |
|
} |
|
} |
|
} |
|
|
|
void CCSBot::StartAnalyzeAlphaProcess() |
|
{ |
|
m_processMode = PROCESS_ANALYZE_ALPHA; |
|
m_analyzeIter = TheNavAreaList.Head (); |
|
|
|
_navAreaCount = TheNavAreaList.Count(); |
|
_currentIndex = 0; |
|
|
|
DestroyHidingSpots(); |
|
|
|
startProgressMeter("Analyzing hiding spots..."); |
|
drawProgressMeter(0, "Analyzing hiding spots..."); |
|
} |
|
|
|
bool CCSBot::AnalyzeAlphaStep() |
|
{ |
|
++_currentIndex; |
|
if (m_analyzeIter == TheNavAreaList.InvalidIndex ()) |
|
return false; |
|
|
|
CNavArea *area = TheNavAreaList.Element (m_analyzeIter); |
|
area->ComputeHidingSpots(); |
|
area->ComputeApproachAreas(); |
|
m_analyzeIter = TheNavAreaList.Next (m_analyzeIter); |
|
|
|
return true; |
|
} |
|
|
|
void CCSBot::UpdateAnalyzeAlphaProcess() |
|
{ |
|
float startTime = g_engfuncs.pfnTime(); |
|
while (g_engfuncs.pfnTime() - startTime < updateTimesliceDuration) |
|
{ |
|
if (AnalyzeAlphaStep() == false) |
|
{ |
|
drawProgressMeter(50, "Analyzing hiding spots..."); |
|
StartAnalyzeBetaProcess(); |
|
return; |
|
} |
|
} |
|
|
|
float progress = (double (_currentIndex) / double (_navAreaCount)) * 0.5f; |
|
drawProgressMeter(progress, "Analyzing hiding spots..."); |
|
} |
|
|
|
void CCSBot::StartAnalyzeBetaProcess() |
|
{ |
|
m_processMode = PROCESS_ANALYZE_BETA; |
|
m_analyzeIter = TheNavAreaList.Head (); |
|
|
|
_navAreaCount = TheNavAreaList.Count (); |
|
_currentIndex = 0; |
|
} |
|
|
|
bool CCSBot::AnalyzeBetaStep() |
|
{ |
|
++_currentIndex; |
|
if (m_analyzeIter == TheNavAreaList.InvalidIndex ()) |
|
return false; |
|
|
|
CNavArea *area = TheNavAreaList.Element (m_analyzeIter); |
|
area->ComputeSpotEncounters(); |
|
area->ComputeSniperSpots(); |
|
m_analyzeIter = TheNavAreaList.Next (m_analyzeIter); |
|
|
|
return true; |
|
} |
|
|
|
void CCSBot::UpdateAnalyzeBetaProcess() |
|
{ |
|
float startTime = g_engfuncs.pfnTime(); |
|
while (g_engfuncs.pfnTime() - startTime < updateTimesliceDuration) |
|
{ |
|
if (AnalyzeBetaStep() == false) |
|
{ |
|
drawProgressMeter(100, "Analyzing approach points..."); |
|
StartSaveProcess(); |
|
return; |
|
} |
|
} |
|
|
|
float progress = (double (_currentIndex) / double (_navAreaCount) + 1.0f) * 0.5f; |
|
drawProgressMeter(progress, "Analyzing approach points..."); |
|
} |
|
|
|
void CCSBot::StartSaveProcess() |
|
{ |
|
m_processMode = PROCESS_SAVE; |
|
} |
|
|
|
void CCSBot::UpdateSaveProcess() |
|
{ |
|
char filename[256]; |
|
char msg[256]; |
|
char cmd[128]; |
|
|
|
GET_GAME_DIR(filename); |
|
|
|
Q_strcat(filename, "\\"); |
|
Q_strcat(filename, TheBots->GetNavMapFilename()); |
|
|
|
HintMessageToAllPlayers("Saving..."); |
|
SaveNavigationMap(filename); |
|
|
|
Q_sprintf(msg, "Navigation file '%s' saved.", filename); |
|
HintMessageToAllPlayers(msg); |
|
|
|
hideProgressMeter(); |
|
StartNormalProcess(); |
|
|
|
Q_sprintf (cmd, "changelevel %s\n", STRING (gpGlobals->mapname)); |
|
SERVER_COMMAND(cmd); |
|
} |
|
|
|
void CCSBot::StartNormalProcess() |
|
{ |
|
m_processMode = PROCESS_NORMAL; |
|
}
|
|
|