dhewm3/neo/d3xp/ai/AI_pathing.cpp
dhewg 736ec20d4d Untangle the epic precompiled.h mess
Don't include the lazy precompiled.h everywhere, only what's
required for the compilation unit.
platform.h needs to be included instead to provide all essential
defines and types.
All includes use the relative path to the neo or the game
specific root.
Move all idlib related includes from idlib/Lib.h to precompiled.h.
precompiled.h still exists for the MFC stuff in tools/.
Add some missing header guards.
2011-12-19 23:21:47 +01:00

1536 lines
45 KiB
C++

/*
===========================================================================
Doom 3 GPL Source Code
Copyright (C) 1999-2011 id Software LLC, a ZeniMax Media company.
This file is part of the Doom 3 GPL Source Code ("Doom 3 Source Code").
Doom 3 Source Code is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Doom 3 Source Code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Doom 3 Source Code. If not, see <http://www.gnu.org/licenses/>.
In addition, the Doom 3 Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Doom 3 Source Code. If not, please request a copy in writing from id Software at the address below.
If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.
===========================================================================
*/
#include "sys/platform.h"
#include "idlib/containers/Queue.h"
#include "idlib/geometry/Winding2D.h"
#include "gamesys/SysCvar.h"
#include "Moveable.h"
#include "WorldSpawn.h"
#include "ai/AI.h"
/*
===============================================================================
Dynamic Obstacle Avoidance
- assumes the AI lives inside a bounding box aligned with the gravity direction
- obstacles in proximity of the AI are gathered
- if obstacles are found the AAS walls are also considered as obstacles
- every obstacle is represented by an oriented bounding box (OBB)
- an OBB is projected onto a 2D plane orthogonal to AI's gravity direction
- the 2D windings of the projections are expanded for the AI bbox
- a path tree is build using clockwise and counter clockwise edge walks along the winding edges
- the path tree is pruned and optimized
- the shortest path is chosen for navigation
===============================================================================
*/
const float MAX_OBSTACLE_RADIUS = 256.0f;
const float PUSH_OUTSIDE_OBSTACLES = 0.5f;
const float CLIP_BOUNDS_EPSILON = 10.0f;
const int MAX_AAS_WALL_EDGES = 256;
const int MAX_OBSTACLES = 256;
const int MAX_PATH_NODES = 256;
const int MAX_OBSTACLE_PATH = 64;
typedef struct obstacle_s {
idVec2 bounds[2];
idWinding2D winding;
idEntity * entity;
} obstacle_t;
typedef struct pathNode_s {
int dir;
idVec2 pos;
idVec2 delta;
float dist;
int obstacle;
int edgeNum;
int numNodes;
struct pathNode_s * parent;
struct pathNode_s * children[2];
struct pathNode_s * next;
void Init();
} pathNode_t;
void pathNode_s::Init() {
dir = 0;
pos.Zero();
delta.Zero();
obstacle = -1;
edgeNum = -1;
numNodes = 0;
parent = children[0] = children[1] = next = NULL;
}
idBlockAlloc<pathNode_t, 128> pathNodeAllocator;
/*
============
LineIntersectsPath
============
*/
bool LineIntersectsPath( const idVec2 &start, const idVec2 &end, const pathNode_t *node ) {
float d0, d1, d2, d3;
idVec3 plane1, plane2;
plane1 = idWinding2D::Plane2DFromPoints( start, end );
d0 = plane1.x * node->pos.x + plane1.y * node->pos.y + plane1.z;
while( node->parent ) {
d1 = plane1.x * node->parent->pos.x + plane1.y * node->parent->pos.y + plane1.z;
if ( FLOATSIGNBITSET( d0 ) ^ FLOATSIGNBITSET( d1 ) ) {
plane2 = idWinding2D::Plane2DFromPoints( node->pos, node->parent->pos );
d2 = plane2.x * start.x + plane2.y * start.y + plane2.z;
d3 = plane2.x * end.x + plane2.y * end.y + plane2.z;
if ( FLOATSIGNBITSET( d2 ) ^ FLOATSIGNBITSET( d3 ) ) {
return true;
}
}
d0 = d1;
node = node->parent;
}
return false;
}
/*
============
PointInsideObstacle
============
*/
int PointInsideObstacle( const obstacle_t *obstacles, const int numObstacles, const idVec2 &point ) {
int i;
for ( i = 0; i < numObstacles; i++ ) {
const idVec2 *bounds = obstacles[i].bounds;
if ( point.x < bounds[0].x || point.y < bounds[0].y || point.x > bounds[1].x || point.y > bounds[1].y ) {
continue;
}
if ( !obstacles[i].winding.PointInside( point, 0.1f ) ) {
continue;
}
return i;
}
return -1;
}
/*
============
GetPointOutsideObstacles
============
*/
void GetPointOutsideObstacles( const obstacle_t *obstacles, const int numObstacles, idVec2 &point, int *obstacle, int *edgeNum ) {
int i, j, k, n, bestObstacle, bestEdgeNum, queueStart, queueEnd, edgeNums[2];
float d, bestd, scale[2];
idVec3 plane, bestPlane;
idVec2 newPoint, dir, bestPoint;
int *queue;
bool *obstacleVisited;
idWinding2D w1, w2;
if ( obstacle ) {
*obstacle = -1;
}
if ( edgeNum ) {
*edgeNum = -1;
}
bestObstacle = PointInsideObstacle( obstacles, numObstacles, point );
if ( bestObstacle == -1 ) {
return;
}
const idWinding2D &w = obstacles[bestObstacle].winding;
bestd = idMath::INFINITY;
bestEdgeNum = 0;
for ( i = 0; i < w.GetNumPoints(); i++ ) {
plane = idWinding2D::Plane2DFromPoints( w[(i+1)%w.GetNumPoints()], w[i], true );
d = plane.x * point.x + plane.y * point.y + plane.z;
if ( d < bestd ) {
bestd = d;
bestPlane = plane;
bestEdgeNum = i;
}
// if this is a wall always try to pop out at the first edge
if ( obstacles[bestObstacle].entity == NULL ) {
break;
}
}
if (i == 0)
return;
newPoint = point - ( bestd + PUSH_OUTSIDE_OBSTACLES ) * bestPlane.ToVec2();
if ( PointInsideObstacle( obstacles, numObstacles, newPoint ) == -1 ) {
point = newPoint;
if ( obstacle ) {
*obstacle = bestObstacle;
}
if ( edgeNum ) {
*edgeNum = bestEdgeNum;
}
return;
}
queue = (int *) _alloca( numObstacles * sizeof( queue[0] ) );
obstacleVisited = (bool *) _alloca( numObstacles * sizeof( obstacleVisited[0] ) );
queueStart = 0;
queueEnd = 1;
queue[0] = bestObstacle;
memset( obstacleVisited, 0, numObstacles * sizeof( obstacleVisited[0] ) );
obstacleVisited[bestObstacle] = true;
bestd = idMath::INFINITY;
for ( i = queue[0]; queueStart < queueEnd; i = queue[++queueStart] ) {
w1 = obstacles[i].winding;
w1.Expand( PUSH_OUTSIDE_OBSTACLES );
for ( j = 0; j < numObstacles; j++ ) {
// if the obstacle has been visited already
if ( obstacleVisited[j] ) {
continue;
}
// if the bounds do not intersect
if ( obstacles[j].bounds[0].x > obstacles[i].bounds[1].x || obstacles[j].bounds[0].y > obstacles[i].bounds[1].y ||
obstacles[j].bounds[1].x < obstacles[i].bounds[0].x || obstacles[j].bounds[1].y < obstacles[i].bounds[0].y ) {
continue;
}
queue[queueEnd++] = j;
obstacleVisited[j] = true;
w2 = obstacles[j].winding;
w2.Expand( 0.2f );
for ( k = 0; k < w1.GetNumPoints(); k++ ) {
dir = w1[(k+1)%w1.GetNumPoints()] - w1[k];
if ( !w2.RayIntersection( w1[k], dir, scale[0], scale[1], edgeNums ) ) {
continue;
}
for ( n = 0; n < 2; n++ ) {
newPoint = w1[k] + scale[n] * dir;
if ( PointInsideObstacle( obstacles, numObstacles, newPoint ) == -1 ) {
d = ( newPoint - point ).LengthSqr();
if ( d < bestd ) {
bestd = d;
bestPoint = newPoint;
bestEdgeNum = edgeNums[n];
bestObstacle = j;
}
}
}
}
}
if ( bestd < idMath::INFINITY ) {
point = bestPoint;
if ( obstacle ) {
*obstacle = bestObstacle;
}
if ( edgeNum ) {
*edgeNum = bestEdgeNum;
}
return;
}
}
gameLocal.Warning( "GetPointOutsideObstacles: no valid point found" );
}
/*
============
GetFirstBlockingObstacle
============
*/
bool GetFirstBlockingObstacle( const obstacle_t *obstacles, int numObstacles, int skipObstacle, const idVec2 &startPos, const idVec2 &delta, float &blockingScale, int &blockingObstacle, int &blockingEdgeNum ) {
int i, edgeNums[2];
float dist, scale1, scale2;
idVec2 bounds[2];
// get bounds for the current movement delta
bounds[0] = startPos - idVec2( CM_BOX_EPSILON, CM_BOX_EPSILON );
bounds[1] = startPos + idVec2( CM_BOX_EPSILON, CM_BOX_EPSILON );
bounds[FLOATSIGNBITNOTSET(delta.x)].x += delta.x;
bounds[FLOATSIGNBITNOTSET(delta.y)].y += delta.y;
// test for obstacles blocking the path
blockingScale = idMath::INFINITY;
dist = delta.Length();
for ( i = 0; i < numObstacles; i++ ) {
if ( i == skipObstacle ) {
continue;
}
if ( bounds[0].x > obstacles[i].bounds[1].x || bounds[0].y > obstacles[i].bounds[1].y ||
bounds[1].x < obstacles[i].bounds[0].x || bounds[1].y < obstacles[i].bounds[0].y ) {
continue;
}
if ( obstacles[i].winding.RayIntersection( startPos, delta, scale1, scale2, edgeNums ) ) {
if ( scale1 < blockingScale && scale1 * dist > -0.01f && scale2 * dist > 0.01f ) {
blockingScale = scale1;
blockingObstacle = i;
blockingEdgeNum = edgeNums[0];
}
}
}
return ( blockingScale < 1.0f );
}
/*
============
GetObstacles
============
*/
int GetObstacles( const idPhysics *physics, const idAAS *aas, const idEntity *ignore, int areaNum, const idVec3 &startPos, const idVec3 &seekPos, obstacle_t *obstacles, int maxObstacles, idBounds &clipBounds ) {
int i, j, numListedClipModels, numObstacles, numVerts, clipMask, blockingObstacle, blockingEdgeNum;
int wallEdges[MAX_AAS_WALL_EDGES], numWallEdges, verts[2], lastVerts[2], nextVerts[2];
float stepHeight, headHeight, blockingScale, min, max;
idVec3 seekDelta, silVerts[32], start, end, nextStart, nextEnd;
idVec2 expBounds[2], edgeDir, edgeNormal, nextEdgeDir, nextEdgeNormal, lastEdgeNormal;
idVec2 obDelta;
idPhysics *obPhys;
idBox box;
idEntity *obEnt;
idClipModel *clipModel;
idClipModel *clipModelList[ MAX_GENTITIES ];
numObstacles = 0;
seekDelta = seekPos - startPos;
expBounds[0] = physics->GetBounds()[0].ToVec2() - idVec2( CM_BOX_EPSILON, CM_BOX_EPSILON );
expBounds[1] = physics->GetBounds()[1].ToVec2() + idVec2( CM_BOX_EPSILON, CM_BOX_EPSILON );
physics->GetAbsBounds().AxisProjection( -physics->GetGravityNormal(), stepHeight, headHeight );
stepHeight += aas->GetSettings()->maxStepHeight;
// clip bounds for the obstacle search space
clipBounds[0] = clipBounds[1] = startPos;
clipBounds.AddPoint( seekPos );
clipBounds.ExpandSelf( MAX_OBSTACLE_RADIUS );
clipMask = physics->GetClipMask();
// find all obstacles touching the clip bounds
numListedClipModels = gameLocal.clip.ClipModelsTouchingBounds( clipBounds, clipMask, clipModelList, MAX_GENTITIES );
for ( i = 0; i < numListedClipModels && numObstacles < MAX_OBSTACLES; i++ ) {
clipModel = clipModelList[i];
obEnt = clipModel->GetEntity();
if ( !clipModel->IsTraceModel() ) {
continue;
}
if ( obEnt->IsType( idActor::Type ) ) {
obPhys = obEnt->GetPhysics();
// ignore myself, my enemy, and dead bodies
if ( ( obPhys == physics ) || ( obEnt == ignore ) || ( obEnt->health <= 0 ) ) {
continue;
}
// if the actor is moving
idVec3 v1 = obPhys->GetLinearVelocity();
if ( v1.LengthSqr() > Square( 10.0f ) ) {
idVec3 v2 = physics->GetLinearVelocity();
if ( v2.LengthSqr() > Square( 10.0f ) ) {
// if moving in about the same direction
if ( v1 * v2 > 0.0f ) {
continue;
}
}
}
} else if ( obEnt->IsType( idMoveable::Type ) ) {
// moveables are considered obstacles
} else {
// ignore everything else
continue;
}
// check if we can step over the object
clipModel->GetAbsBounds().AxisProjection( -physics->GetGravityNormal(), min, max );
if ( max < stepHeight || min > headHeight ) {
// can step over this one
continue;
}
// project a box containing the obstacle onto the floor plane
box = idBox( clipModel->GetBounds(), clipModel->GetOrigin(), clipModel->GetAxis() );
numVerts = box.GetParallelProjectionSilhouetteVerts( physics->GetGravityNormal(), silVerts );
// create a 2D winding for the obstacle;
obstacle_t &obstacle = obstacles[numObstacles++];
obstacle.winding.Clear();
for ( j = 0; j < numVerts; j++ ) {
obstacle.winding.AddPoint( silVerts[j].ToVec2() );
}
if ( ai_showObstacleAvoidance.GetBool() ) {
for ( j = 0; j < numVerts; j++ ) {
silVerts[j].z = startPos.z;
}
for ( j = 0; j < numVerts; j++ ) {
gameRenderWorld->DebugArrow( colorWhite, silVerts[j], silVerts[(j+1)%numVerts], 4 );
}
}
// expand the 2D winding for collision with a 2D box
obstacle.winding.ExpandForAxialBox( expBounds );
obstacle.winding.GetBounds( obstacle.bounds );
obstacle.entity = obEnt;
}
// if there are no dynamic obstacles the path should be through valid AAS space
if ( numObstacles == 0 ) {
return 0;
}
// if the current path doesn't intersect any dynamic obstacles the path should be through valid AAS space
if ( PointInsideObstacle( obstacles, numObstacles, startPos.ToVec2() ) == -1 ) {
if ( !GetFirstBlockingObstacle( obstacles, numObstacles, -1, startPos.ToVec2(), seekDelta.ToVec2(), blockingScale, blockingObstacle, blockingEdgeNum ) ) {
return 0;
}
}
// create obstacles for AAS walls
if ( aas ) {
float halfBoundsSize = ( expBounds[ 1 ].x - expBounds[ 0 ].x ) * 0.5f;
numWallEdges = aas->GetWallEdges( areaNum, clipBounds, TFL_WALK, wallEdges, MAX_AAS_WALL_EDGES );
aas->SortWallEdges( wallEdges, numWallEdges );
lastVerts[0] = lastVerts[1] = 0;
lastEdgeNormal.Zero();
nextEdgeNormal.Zero();
nextVerts[0] = nextVerts[1] = 0;
for ( i = 0; i < numWallEdges && numObstacles < MAX_OBSTACLES; i++ ) {
aas->GetEdge( wallEdges[i], start, end );
aas->GetEdgeVertexNumbers( wallEdges[i], verts );
edgeDir = end.ToVec2() - start.ToVec2();
edgeDir.Normalize();
edgeNormal.x = edgeDir.y;
edgeNormal.y = -edgeDir.x;
if ( i < numWallEdges-1 ) {
aas->GetEdge( wallEdges[i+1], nextStart, nextEnd );
aas->GetEdgeVertexNumbers( wallEdges[i+1], nextVerts );
nextEdgeDir = nextEnd.ToVec2() - nextStart.ToVec2();
nextEdgeDir.Normalize();
nextEdgeNormal.x = nextEdgeDir.y;
nextEdgeNormal.y = -nextEdgeDir.x;
}
obstacle_t &obstacle = obstacles[numObstacles++];
obstacle.winding.Clear();
obstacle.winding.AddPoint( end.ToVec2() );
obstacle.winding.AddPoint( start.ToVec2() );
obstacle.winding.AddPoint( start.ToVec2() - edgeDir - edgeNormal * halfBoundsSize );
obstacle.winding.AddPoint( end.ToVec2() + edgeDir - edgeNormal * halfBoundsSize );
if ( lastVerts[1] == verts[0] ) {
obstacle.winding[2] -= lastEdgeNormal * halfBoundsSize;
} else {
obstacle.winding[1] -= edgeDir;
}
if ( verts[1] == nextVerts[0] ) {
obstacle.winding[3] -= nextEdgeNormal * halfBoundsSize;
} else {
obstacle.winding[0] += edgeDir;
}
obstacle.winding.GetBounds( obstacle.bounds );
obstacle.entity = NULL;
memcpy( lastVerts, verts, sizeof( lastVerts ) );
lastEdgeNormal = edgeNormal;
}
}
// show obstacles
if ( ai_showObstacleAvoidance.GetBool() ) {
for ( i = 0; i < numObstacles; i++ ) {
obstacle_t &obstacle = obstacles[i];
for ( j = 0; j < obstacle.winding.GetNumPoints(); j++ ) {
silVerts[j].ToVec2() = obstacle.winding[j];
silVerts[j].z = startPos.z;
}
for ( j = 0; j < obstacle.winding.GetNumPoints(); j++ ) {
gameRenderWorld->DebugArrow( colorGreen, silVerts[j], silVerts[(j+1)%obstacle.winding.GetNumPoints()], 4 );
}
}
}
return numObstacles;
}
/*
============
FreePathTree_r
============
*/
void FreePathTree_r( pathNode_t *node ) {
if ( node->children[0] ) {
FreePathTree_r( node->children[0] );
}
if ( node->children[1] ) {
FreePathTree_r( node->children[1] );
}
pathNodeAllocator.Free( node );
}
/*
============
DrawPathTree
============
*/
void DrawPathTree( const pathNode_t *root, const float height ) {
int i;
idVec3 start, end;
const pathNode_t *node;
for ( node = root; node; node = node->next ) {
for ( i = 0; i < 2; i++ ) {
if ( node->children[i] ) {
start.ToVec2() = node->pos;
start.z = height;
end.ToVec2() = node->children[i]->pos;
end.z = height;
gameRenderWorld->DebugArrow( node->edgeNum == -1 ? colorYellow : i ? colorBlue : colorRed, start, end, 1 );
break;
}
}
}
}
/*
============
GetPathNodeDelta
============
*/
bool GetPathNodeDelta( pathNode_t *node, const obstacle_t *obstacles, const idVec2 &seekPos, bool blocked ) {
int numPoints, edgeNum;
bool facing;
idVec2 seekDelta, dir;
pathNode_t *n;
numPoints = obstacles[node->obstacle].winding.GetNumPoints();
// get delta along the current edge
while( 1 ) {
edgeNum = ( node->edgeNum + node->dir ) % numPoints;
node->delta = obstacles[node->obstacle].winding[edgeNum] - node->pos;
if ( node->delta.LengthSqr() > 0.01f ) {
break;
}
node->edgeNum = ( node->edgeNum + numPoints + ( 2 * node->dir - 1 ) ) % numPoints;
}
// if not blocked
if ( !blocked ) {
// test if the current edge faces the goal
seekDelta = seekPos - node->pos;
facing = ( ( 2 * node->dir - 1 ) * ( node->delta.x * seekDelta.y - node->delta.y * seekDelta.x ) ) >= 0.0f;
// if the current edge faces goal and the line from the current
// position to the goal does not intersect the current path
if ( facing && !LineIntersectsPath( node->pos, seekPos, node->parent ) ) {
node->delta = seekPos - node->pos;
node->edgeNum = -1;
}
}
// if the delta is along the obstacle edge
if ( node->edgeNum != -1 ) {
// if the edge is found going from this node to the root node
for ( n = node->parent; n; n = n->parent ) {
if ( node->obstacle != n->obstacle || node->edgeNum != n->edgeNum ) {
continue;
}
// test whether or not the edge segments actually overlap
if ( n->pos * node->delta > ( node->pos + node->delta ) * node->delta ) {
continue;
}
if ( node->pos * node->delta > ( n->pos + n->delta ) * node->delta ) {
continue;
}
break;
}
if ( n ) {
return false;
}
}
return true;
}
/*
============
BuildPathTree
============
*/
pathNode_t *BuildPathTree( const obstacle_t *obstacles, int numObstacles, const idBounds &clipBounds, const idVec2 &startPos, const idVec2 &seekPos, obstaclePath_t &path ) {
int blockingEdgeNum, blockingObstacle, obstaclePoints, bestNumNodes = MAX_OBSTACLE_PATH;
float blockingScale;
pathNode_t *root, *node, *child;
// gcc 4.0
idQueueTemplate<pathNode_t, offsetof( pathNode_t, next ) > pathNodeQueue, treeQueue;
root = pathNodeAllocator.Alloc();
root->Init();
root->pos = startPos;
root->delta = seekPos - root->pos;
root->numNodes = 0;
pathNodeQueue.Add( root );
for ( node = pathNodeQueue.Get(); node && pathNodeAllocator.GetAllocCount() < MAX_PATH_NODES; node = pathNodeQueue.Get() ) {
treeQueue.Add( node );
// if this path has more than twice the number of nodes than the best path so far
if ( node->numNodes > bestNumNodes * 2 ) {
continue;
}
// don't move outside of the clip bounds
idVec2 endPos = node->pos + node->delta;
if ( endPos.x - CLIP_BOUNDS_EPSILON < clipBounds[0].x || endPos.x + CLIP_BOUNDS_EPSILON > clipBounds[1].x ||
endPos.y - CLIP_BOUNDS_EPSILON < clipBounds[0].y || endPos.y + CLIP_BOUNDS_EPSILON > clipBounds[1].y ) {
continue;
}
// if an obstacle is blocking the path
if ( GetFirstBlockingObstacle( obstacles, numObstacles, node->obstacle, node->pos, node->delta, blockingScale, blockingObstacle, blockingEdgeNum ) ) {
if ( path.firstObstacle == NULL ) {
path.firstObstacle = obstacles[blockingObstacle].entity;
}
node->delta *= blockingScale;
if ( node->edgeNum == -1 ) {
node->children[0] = pathNodeAllocator.Alloc();
node->children[0]->Init();
node->children[1] = pathNodeAllocator.Alloc();
node->children[1]->Init();
node->children[0]->dir = 0;
node->children[1]->dir = 1;
node->children[0]->parent = node->children[1]->parent = node;
node->children[0]->pos = node->children[1]->pos = node->pos + node->delta;
node->children[0]->obstacle = node->children[1]->obstacle = blockingObstacle;
node->children[0]->edgeNum = node->children[1]->edgeNum = blockingEdgeNum;
node->children[0]->numNodes = node->children[1]->numNodes = node->numNodes + 1;
if ( GetPathNodeDelta( node->children[0], obstacles, seekPos, true ) ) {
pathNodeQueue.Add( node->children[0] );
}
if ( GetPathNodeDelta( node->children[1], obstacles, seekPos, true ) ) {
pathNodeQueue.Add( node->children[1] );
}
} else {
node->children[node->dir] = child = pathNodeAllocator.Alloc();
child->Init();
child->dir = node->dir;
child->parent = node;
child->pos = node->pos + node->delta;
child->obstacle = blockingObstacle;
child->edgeNum = blockingEdgeNum;
child->numNodes = node->numNodes + 1;
if ( GetPathNodeDelta( child, obstacles, seekPos, true ) ) {
pathNodeQueue.Add( child );
}
}
} else {
node->children[node->dir] = child = pathNodeAllocator.Alloc();
child->Init();
child->dir = node->dir;
child->parent = node;
child->pos = node->pos + node->delta;
child->numNodes = node->numNodes + 1;
// there is a free path towards goal
if ( node->edgeNum == -1 ) {
if ( node->numNodes < bestNumNodes ) {
bestNumNodes = node->numNodes;
}
continue;
}
child->obstacle = node->obstacle;
obstaclePoints = obstacles[node->obstacle].winding.GetNumPoints();
child->edgeNum = ( node->edgeNum + obstaclePoints + ( 2 * node->dir - 1 ) ) % obstaclePoints;
if ( GetPathNodeDelta( child, obstacles, seekPos, false ) ) {
pathNodeQueue.Add( child );
}
}
}
return root;
}
/*
============
PrunePathTree
============
*/
void PrunePathTree( pathNode_t *root, const idVec2 &seekPos ) {
int i;
float bestDist;
pathNode_t *node, *lastNode, *n, *bestNode;
node = root;
while( node ) {
node->dist = ( seekPos - node->pos ).LengthSqr();
if ( node->children[0] ) {
node = node->children[0];
} else if ( node->children[1] ) {
node = node->children[1];
} else {
// find the node closest to the goal along this path
bestDist = idMath::INFINITY;
bestNode = node;
for ( n = node; n; n = n->parent ) {
if ( n->children[0] && n->children[1] ) {
break;
}
if ( n->dist < bestDist ) {
bestDist = n->dist;
bestNode = n;
}
}
// free tree down from the best node
for ( i = 0; i < 2; i++ ) {
if ( bestNode->children[i] ) {
FreePathTree_r( bestNode->children[i] );
bestNode->children[i] = NULL;
}
}
for ( lastNode = bestNode, node = bestNode->parent; node; lastNode = node, node = node->parent ) {
if ( node->children[1] && ( node->children[1] != lastNode ) ) {
node = node->children[1];
break;
}
}
}
}
}
/*
============
OptimizePath
============
*/
int OptimizePath( const pathNode_t *root, const pathNode_t *leafNode, const obstacle_t *obstacles, int numObstacles, idVec2 optimizedPath[MAX_OBSTACLE_PATH] ) {
int i, numPathPoints, edgeNums[2];
const pathNode_t *curNode, *nextNode;
idVec2 curPos, curDelta, bounds[2];
float scale1, scale2, curLength;
optimizedPath[0] = root->pos;
numPathPoints = 1;
for ( nextNode = curNode = root; curNode != leafNode; curNode = nextNode ) {
for ( nextNode = leafNode; nextNode->parent != curNode; nextNode = nextNode->parent ) {
// can only take shortcuts when going from one object to another
if ( nextNode->obstacle == curNode->obstacle ) {
continue;
}
curPos = curNode->pos;
curDelta = nextNode->pos - curPos;
curLength = curDelta.Length();
// get bounds for the current movement delta
bounds[0] = curPos - idVec2( CM_BOX_EPSILON, CM_BOX_EPSILON );
bounds[1] = curPos + idVec2( CM_BOX_EPSILON, CM_BOX_EPSILON );
bounds[FLOATSIGNBITNOTSET(curDelta.x)].x += curDelta.x;
bounds[FLOATSIGNBITNOTSET(curDelta.y)].y += curDelta.y;
// test if the shortcut intersects with any obstacles
for ( i = 0; i < numObstacles; i++ ) {
if ( bounds[0].x > obstacles[i].bounds[1].x || bounds[0].y > obstacles[i].bounds[1].y ||
bounds[1].x < obstacles[i].bounds[0].x || bounds[1].y < obstacles[i].bounds[0].y ) {
continue;
}
if ( obstacles[i].winding.RayIntersection( curPos, curDelta, scale1, scale2, edgeNums ) ) {
if ( scale1 >= 0.0f && scale1 <= 1.0f && ( i != nextNode->obstacle || scale1 * curLength < curLength - 0.5f ) ) {
break;
}
if ( scale2 >= 0.0f && scale2 <= 1.0f && ( i != nextNode->obstacle || scale2 * curLength < curLength - 0.5f ) ) {
break;
}
}
}
if ( i >= numObstacles ) {
break;
}
}
// store the next position along the optimized path
optimizedPath[numPathPoints++] = nextNode->pos;
}
return numPathPoints;
}
/*
============
PathLength
============
*/
float PathLength( idVec2 optimizedPath[MAX_OBSTACLE_PATH], int numPathPoints, const idVec2 &curDir ) {
int i;
float pathLength;
// calculate the path length
pathLength = 0.0f;
for ( i = 0; i < numPathPoints-1; i++ ) {
pathLength += ( optimizedPath[i+1] - optimizedPath[i] ).LengthFast();
}
// add penalty if this path does not go in the current direction
if ( curDir * ( optimizedPath[1] - optimizedPath[0] ) < 0.0f ) {
pathLength += 100.0f;
}
return pathLength;
}
/*
============
FindOptimalPath
Returns true if there is a path all the way to the goal.
============
*/
bool FindOptimalPath( const pathNode_t *root, const obstacle_t *obstacles, int numObstacles, const float height, const idVec3 &curDir, idVec3 &seekPos ) {
int i, numPathPoints, bestNumPathPoints;
const pathNode_t *node, *lastNode, *bestNode;
idVec2 optimizedPath[MAX_OBSTACLE_PATH];
float pathLength, bestPathLength;
bool pathToGoalExists, optimizedPathCalculated;
seekPos.Zero();
seekPos.z = height;
pathToGoalExists = false;
optimizedPathCalculated = false;
bestNode = root;
bestNumPathPoints = 0;
bestPathLength = idMath::INFINITY;
node = root;
while( node ) {
pathToGoalExists |= ( node->dist < 0.1f );
if ( node->dist <= bestNode->dist ) {
if ( idMath::Fabs( node->dist - bestNode->dist ) < 0.1f ) {
if ( !optimizedPathCalculated ) {
bestNumPathPoints = OptimizePath( root, bestNode, obstacles, numObstacles, optimizedPath );
bestPathLength = PathLength( optimizedPath, bestNumPathPoints, curDir.ToVec2() );
seekPos.ToVec2() = optimizedPath[1];
}
numPathPoints = OptimizePath( root, node, obstacles, numObstacles, optimizedPath );
pathLength = PathLength( optimizedPath, numPathPoints, curDir.ToVec2() );
if ( pathLength < bestPathLength ) {
bestNode = node;
bestNumPathPoints = numPathPoints;
bestPathLength = pathLength;
seekPos.ToVec2() = optimizedPath[1];
}
optimizedPathCalculated = true;
} else {
bestNode = node;
optimizedPathCalculated = false;
}
}
if ( node->children[0] ) {
node = node->children[0];
} else if ( node->children[1] ) {
node = node->children[1];
} else {
for ( lastNode = node, node = node->parent; node; lastNode = node, node = node->parent ) {
if ( node->children[1] && node->children[1] != lastNode ) {
node = node->children[1];
break;
}
}
}
}
if ( !pathToGoalExists ) {
seekPos.ToVec2() = root->children[0]->pos;
} else if ( !optimizedPathCalculated ) {
OptimizePath( root, bestNode, obstacles, numObstacles, optimizedPath );
seekPos.ToVec2() = optimizedPath[1];
}
if ( ai_showObstacleAvoidance.GetBool() ) {
idVec3 start, end;
start.z = end.z = height + 4.0f;
numPathPoints = OptimizePath( root, bestNode, obstacles, numObstacles, optimizedPath );
for ( i = 0; i < numPathPoints-1; i++ ) {
start.ToVec2() = optimizedPath[i];
end.ToVec2() = optimizedPath[i+1];
gameRenderWorld->DebugArrow( colorCyan, start, end, 1 );
}
}
return pathToGoalExists;
}
/*
============
idAI::FindPathAroundObstacles
Finds a path around dynamic obstacles using a path tree with clockwise and counter clockwise edge walks.
============
*/
bool idAI::FindPathAroundObstacles( const idPhysics *physics, const idAAS *aas, const idEntity *ignore, const idVec3 &startPos, const idVec3 &seekPos, obstaclePath_t &path ) {
int numObstacles, areaNum, insideObstacle;
obstacle_t obstacles[MAX_OBSTACLES];
idBounds clipBounds;
idBounds bounds;
pathNode_t *root;
bool pathToGoalExists;
path.seekPos = seekPos;
path.firstObstacle = NULL;
path.startPosOutsideObstacles = startPos;
path.startPosObstacle = NULL;
path.seekPosOutsideObstacles = seekPos;
path.seekPosObstacle = NULL;
if ( !aas ) {
return true;
}
bounds[1] = aas->GetSettings()->boundingBoxes[0][1];
bounds[0] = -bounds[1];
bounds[1].z = 32.0f;
// get the AAS area number and a valid point inside that area
areaNum = aas->PointReachableAreaNum( path.startPosOutsideObstacles, bounds, (AREA_REACHABLE_WALK|AREA_REACHABLE_FLY) );
aas->PushPointIntoAreaNum( areaNum, path.startPosOutsideObstacles );
// get all the nearby obstacles
numObstacles = GetObstacles( physics, aas, ignore, areaNum, path.startPosOutsideObstacles, path.seekPosOutsideObstacles, obstacles, MAX_OBSTACLES, clipBounds );
// get a source position outside the obstacles
GetPointOutsideObstacles( obstacles, numObstacles, path.startPosOutsideObstacles.ToVec2(), &insideObstacle, NULL );
if ( insideObstacle != -1 ) {
path.startPosObstacle = obstacles[insideObstacle].entity;
}
// get a goal position outside the obstacles
GetPointOutsideObstacles( obstacles, numObstacles, path.seekPosOutsideObstacles.ToVec2(), &insideObstacle, NULL );
if ( insideObstacle != -1 ) {
path.seekPosObstacle = obstacles[insideObstacle].entity;
}
// if start and destination are pushed to the same point, we don't have a path around the obstacle
if ( ( path.seekPosOutsideObstacles.ToVec2() - path.startPosOutsideObstacles.ToVec2() ).LengthSqr() < Square( 1.0f ) ) {
if ( ( seekPos.ToVec2() - startPos.ToVec2() ).LengthSqr() > Square( 2.0f ) ) {
return false;
}
}
// build a path tree
root = BuildPathTree( obstacles, numObstacles, clipBounds, path.startPosOutsideObstacles.ToVec2(), path.seekPosOutsideObstacles.ToVec2(), path );
// draw the path tree
if ( ai_showObstacleAvoidance.GetBool() ) {
DrawPathTree( root, physics->GetOrigin().z );
}
// prune the tree
PrunePathTree( root, path.seekPosOutsideObstacles.ToVec2() );
// find the optimal path
pathToGoalExists = FindOptimalPath( root, obstacles, numObstacles, physics->GetOrigin().z, physics->GetLinearVelocity(), path.seekPos );
// free the tree
FreePathTree_r( root );
return pathToGoalExists;
}
/*
============
idAI::FreeObstacleAvoidanceNodes
============
*/
void idAI::FreeObstacleAvoidanceNodes( void ) {
pathNodeAllocator.Shutdown();
}
/*
===============================================================================
Path Prediction
Uses the AAS to quickly and accurately predict a path for a certain
period of time based on an initial position and velocity.
===============================================================================
*/
const float OVERCLIP = 1.001f;
const int MAX_FRAME_SLIDE = 5;
typedef struct pathTrace_s {
float fraction;
idVec3 endPos;
idVec3 normal;
const idEntity * blockingEntity;
} pathTrace_t;
/*
============
PathTrace
Returns true if a stop event was triggered.
============
*/
bool PathTrace( const idEntity *ent, const idAAS *aas, const idVec3 &start, const idVec3 &end, int stopEvent, struct pathTrace_s &trace, predictedPath_t &path ) {
trace_t clipTrace;
aasTrace_t aasTrace;
memset( &trace, 0, sizeof( trace ) );
if ( !aas || !aas->GetSettings() ) {
gameLocal.clip.Translation( clipTrace, start, end, ent->GetPhysics()->GetClipModel(),
ent->GetPhysics()->GetClipModel()->GetAxis(), MASK_MONSTERSOLID, ent );
// NOTE: could do (expensive) ledge detection here for when there is no AAS file
trace.fraction = clipTrace.fraction;
trace.endPos = clipTrace.endpos;
trace.normal = clipTrace.c.normal;
trace.blockingEntity = gameLocal.entities[ clipTrace.c.entityNum ];
} else {
aasTrace.getOutOfSolid = true;
if ( stopEvent & SE_ENTER_LEDGE_AREA ) {
aasTrace.flags |= AREA_LEDGE;
}
if ( stopEvent & SE_ENTER_OBSTACLE ) {
aasTrace.travelFlags |= TFL_INVALID;
}
aas->Trace( aasTrace, start, end );
gameLocal.clip.TranslationEntities( clipTrace, start, aasTrace.endpos, ent->GetPhysics()->GetClipModel(),
ent->GetPhysics()->GetClipModel()->GetAxis(), MASK_MONSTERSOLID, ent );
if ( clipTrace.fraction >= 1.0f ) {
trace.fraction = aasTrace.fraction;
trace.endPos = aasTrace.endpos;
trace.normal = aas->GetPlane( aasTrace.planeNum ).Normal();
trace.blockingEntity = gameLocal.world;
if ( aasTrace.fraction < 1.0f ) {
if ( stopEvent & SE_ENTER_LEDGE_AREA ) {
if ( aas->AreaFlags( aasTrace.blockingAreaNum ) & AREA_LEDGE ) {
path.endPos = trace.endPos;
path.endNormal = trace.normal;
path.endEvent = SE_ENTER_LEDGE_AREA;
path.blockingEntity = trace.blockingEntity;
if ( ai_debugMove.GetBool() ) {
gameRenderWorld->DebugLine( colorRed, start, aasTrace.endpos );
}
return true;
}
}
if ( stopEvent & SE_ENTER_OBSTACLE ) {
if ( aas->AreaTravelFlags( aasTrace.blockingAreaNum ) & TFL_INVALID ) {
path.endPos = trace.endPos;
path.endNormal = trace.normal;
path.endEvent = SE_ENTER_OBSTACLE;
path.blockingEntity = trace.blockingEntity;
if ( ai_debugMove.GetBool() ) {
gameRenderWorld->DebugLine( colorRed, start, aasTrace.endpos );
}
return true;
}
}
}
} else {
trace.fraction = clipTrace.fraction;
trace.endPos = clipTrace.endpos;
trace.normal = clipTrace.c.normal;
trace.blockingEntity = gameLocal.entities[ clipTrace.c.entityNum ];
}
}
if ( trace.fraction >= 1.0f ) {
trace.blockingEntity = NULL;
}
return false;
}
/*
============
idAI::PredictPath
Can also be used when there is no AAS file available however ledges are not detected.
============
*/
bool idAI::PredictPath( const idEntity *ent, const idAAS *aas, const idVec3 &start, const idVec3 &velocity, int totalTime, int frameTime, int stopEvent, predictedPath_t &path ) {
int i, j, step, numFrames, curFrameTime;
idVec3 delta, curStart, curEnd, curVelocity, lastEnd, stepUp, tmpStart;
idVec3 gravity, gravityDir, invGravityDir;
float maxStepHeight, minFloorCos;
pathTrace_t trace;
if ( aas && aas->GetSettings() ) {
gravity = aas->GetSettings()->gravity;
gravityDir = aas->GetSettings()->gravityDir;
invGravityDir = aas->GetSettings()->invGravityDir;
maxStepHeight = aas->GetSettings()->maxStepHeight;
minFloorCos = aas->GetSettings()->minFloorCos;
} else {
gravity = DEFAULT_GRAVITY_VEC3;
gravityDir = idVec3( 0, 0, -1 );
invGravityDir = idVec3( 0, 0, 1 );
maxStepHeight = 14.0f;
minFloorCos = 0.7f;
}
path.endPos = start;
path.endVelocity = velocity;
path.endNormal.Zero();
path.endEvent = 0;
path.endTime = 0;
path.blockingEntity = NULL;
curStart = start;
curVelocity = velocity;
numFrames = ( totalTime + frameTime - 1 ) / frameTime;
curFrameTime = frameTime;
for ( i = 0; i < numFrames; i++ ) {
if ( i == numFrames-1 ) {
curFrameTime = totalTime - i * curFrameTime;
}
delta = curVelocity * curFrameTime * 0.001f;
path.endVelocity = curVelocity;
path.endTime = i * frameTime;
// allow sliding along a few surfaces per frame
for ( j = 0; j < MAX_FRAME_SLIDE; j++ ) {
idVec3 lineStart = curStart;
// allow stepping up three times per frame
for ( step = 0; step < 3; step++ ) {
curEnd = curStart + delta;
if ( PathTrace( ent, aas, curStart, curEnd, stopEvent, trace, path ) ) {
return true;
}
if ( step ) {
// step down at end point
tmpStart = trace.endPos;
curEnd = tmpStart - stepUp;
if ( PathTrace( ent, aas, tmpStart, curEnd, stopEvent, trace, path ) ) {
return true;
}
// if not moved any further than without stepping up, or if not on a floor surface
if ( (lastEnd - start).LengthSqr() > (trace.endPos - start).LengthSqr() - 0.1f ||
( trace.normal * invGravityDir ) < minFloorCos ) {
if ( stopEvent & SE_BLOCKED ) {
path.endPos = lastEnd;
path.endEvent = SE_BLOCKED;
if ( ai_debugMove.GetBool() ) {
gameRenderWorld->DebugLine( colorRed, lineStart, lastEnd );
}
return true;
}
curStart = lastEnd;
break;
}
}
path.endNormal = trace.normal;
path.blockingEntity = trace.blockingEntity;
// if the trace is not blocked or blocked by a floor surface
if ( trace.fraction >= 1.0f || ( trace.normal * invGravityDir ) > minFloorCos ) {
curStart = trace.endPos;
break;
}
// save last result
lastEnd = trace.endPos;
// step up
stepUp = invGravityDir * maxStepHeight;
if ( PathTrace( ent, aas, curStart, curStart + stepUp, stopEvent, trace, path ) ) {
return true;
}
stepUp *= trace.fraction;
curStart = trace.endPos;
}
if ( ai_debugMove.GetBool() ) {
gameRenderWorld->DebugLine( colorRed, lineStart, curStart );
}
if ( trace.fraction >= 1.0f ) {
break;
}
delta.ProjectOntoPlane( trace.normal, OVERCLIP );
curVelocity.ProjectOntoPlane( trace.normal, OVERCLIP );
if ( stopEvent & SE_BLOCKED ) {
// if going backwards
if ( (curVelocity - gravityDir * curVelocity * gravityDir ) *
(velocity - gravityDir * velocity * gravityDir) < 0.0f ) {
path.endPos = curStart;
path.endEvent = SE_BLOCKED;
return true;
}
}
}
if ( j >= MAX_FRAME_SLIDE ) {
if ( stopEvent & SE_BLOCKED ) {
path.endPos = curStart;
path.endEvent = SE_BLOCKED;
return true;
}
}
// add gravity
curVelocity += gravity * frameTime * 0.001f;
}
path.endTime = totalTime;
path.endVelocity = curVelocity;
path.endPos = curStart;
path.endEvent = 0;
return false;
}
/*
===============================================================================
Trajectory Prediction
Finds the best collision free trajectory for a clip model based on an
initial position, target position and speed.
===============================================================================
*/
/*
=====================
Ballistics
get the ideal aim pitch angle in order to hit the target
also get the time it takes for the projectile to arrive at the target
=====================
*/
typedef struct ballistics_s {
float angle; // angle in degrees in the range [-180, 180]
float time; // time it takes before the projectile arrives
} ballistics_t;
static int Ballistics( const idVec3 &start, const idVec3 &end, float speed, float gravity, ballistics_t bal[2] ) {
int n, i;
float x, y, a, b, c, d, sqrtd, inva, p[2];
x = ( end.ToVec2() - start.ToVec2() ).Length();
y = end[2] - start[2];
a = 4.0f * y * y + 4.0f * x * x;
b = -4.0f * speed * speed - 4.0f * y * gravity;
c = gravity * gravity;
d = b * b - 4.0f * a * c;
if ( d <= 0.0f || a == 0.0f ) {
return 0;
}
sqrtd = idMath::Sqrt( d );
inva = 0.5f / a;
p[0] = ( - b + sqrtd ) * inva;
p[1] = ( - b - sqrtd ) * inva;
n = 0;
for ( i = 0; i < 2; i++ ) {
if ( p[i] <= 0.0f ) {
continue;
}
d = idMath::Sqrt( p[i] );
bal[n].angle = atan2( 0.5f * ( 2.0f * y * p[i] - gravity ) / d, d * x );
bal[n].time = x / ( cos( bal[n].angle ) * speed );
bal[n].angle = idMath::AngleNormalize180( RAD2DEG( bal[n].angle ) );
n++;
}
return n;
}
/*
=====================
HeightForTrajectory
Returns the maximum hieght of a given trajectory
=====================
*/
#if 0
static float HeightForTrajectory( const idVec3 &start, float zVel, float gravity ) {
float maxHeight, t;
t = zVel / gravity;
// maximum height of projectile
maxHeight = start.z - 0.5f * gravity * ( t * t );
return maxHeight;
}
#endif
/*
=====================
idAI::TestTrajectory
=====================
*/
bool idAI::TestTrajectory( const idVec3 &start, const idVec3 &end, float zVel, float gravity, float time, float max_height, const idClipModel *clip, int clipmask, const idEntity *ignore, const idEntity *targetEntity, int drawtime ) {
int i, numSegments;
float maxHeight, t, t2;
idVec3 points[5];
trace_t trace;
bool result;
t = zVel / gravity;
// maximum height of projectile
maxHeight = start.z - 0.5f * gravity * ( t * t );
// time it takes to fall from the top to the end height
t = idMath::Sqrt( ( maxHeight - end.z ) / ( 0.5f * -gravity ) );
// start of parabolic
points[0] = start;
if ( t < time ) {
numSegments = 4;
// point in the middle between top and start
t2 = ( time - t ) * 0.5f;
points[1].ToVec2() = start.ToVec2() + (end.ToVec2() - start.ToVec2()) * ( t2 / time );
points[1].z = start.z + t2 * zVel + 0.5f * gravity * t2 * t2;
// top of parabolic
t2 = time - t;
points[2].ToVec2() = start.ToVec2() + (end.ToVec2() - start.ToVec2()) * ( t2 / time );
points[2].z = start.z + t2 * zVel + 0.5f * gravity * t2 * t2;
// point in the middel between top and end
t2 = time - t * 0.5f;
points[3].ToVec2() = start.ToVec2() + (end.ToVec2() - start.ToVec2()) * ( t2 / time );
points[3].z = start.z + t2 * zVel + 0.5f * gravity * t2 * t2;
} else {
numSegments = 2;
// point halfway through
t2 = time * 0.5f;
points[1].ToVec2() = start.ToVec2() + ( end.ToVec2() - start.ToVec2() ) * 0.5f;
points[1].z = start.z + t2 * zVel + 0.5f * gravity * t2 * t2;
}
// end of parabolic
points[numSegments] = end;
if ( drawtime ) {
for ( i = 0; i < numSegments; i++ ) {
gameRenderWorld->DebugLine( colorRed, points[i], points[i+1], drawtime );
}
}
// make sure projectile doesn't go higher than we want it to go
for ( i = 0; i < numSegments; i++ ) {
if ( points[i].z > max_height ) {
// goes higher than we want to allow
return false;
}
}
result = true;
for ( i = 0; i < numSegments; i++ ) {
gameLocal.clip.Translation( trace, points[i], points[i+1], clip, mat3_identity, clipmask, ignore );
if ( trace.fraction < 1.0f ) {
if ( gameLocal.GetTraceEntity( trace ) == targetEntity ) {
result = true;
} else {
result = false;
}
break;
}
}
if ( drawtime ) {
if ( clip ) {
gameRenderWorld->DebugBounds( result ? colorGreen : colorYellow, clip->GetBounds().Expand( 1.0f ), trace.endpos, drawtime );
} else {
idBounds bnds( trace.endpos );
bnds.ExpandSelf( 1.0f );
gameRenderWorld->DebugBounds( result ? colorGreen : colorYellow, bnds, vec3_zero, drawtime );
}
}
return result;
}
/*
=====================
idAI::PredictTrajectory
returns true if there is a collision free trajectory for the clip model
aimDir is set to the ideal aim direction in order to hit the target
=====================
*/
bool idAI::PredictTrajectory( const idVec3 &firePos, const idVec3 &target, float projectileSpeed, const idVec3 &projGravity, const idClipModel *clip, int clipmask, float max_height, const idEntity *ignore, const idEntity *targetEntity, int drawtime, idVec3 &aimDir ) {
int n, i, j;
float zVel, a, t, pitch, s, c;
trace_t trace;
ballistics_t ballistics[2];
idVec3 dir[2];
idVec3 velocity;
idVec3 lastPos, pos;
assert( targetEntity );
// check if the projectile starts inside the target
if ( targetEntity->GetPhysics()->GetAbsBounds().IntersectsBounds( clip->GetBounds().Translate( firePos ) ) ) {
aimDir = target - firePos;
aimDir.Normalize();
return true;
}
// if no velocity or the projectile is not affected by gravity
if ( projectileSpeed <= 0.0f || projGravity == vec3_origin ) {
aimDir = target - firePos;
aimDir.Normalize();
gameLocal.clip.Translation( trace, firePos, target, clip, mat3_identity, clipmask, ignore );
if ( drawtime ) {
gameRenderWorld->DebugLine( colorRed, firePos, target, drawtime );
idBounds bnds( trace.endpos );
bnds.ExpandSelf( 1.0f );
gameRenderWorld->DebugBounds( ( trace.fraction >= 1.0f || ( gameLocal.GetTraceEntity( trace ) == targetEntity ) ) ? colorGreen : colorYellow, bnds, vec3_zero, drawtime );
}
return ( trace.fraction >= 1.0f || ( gameLocal.GetTraceEntity( trace ) == targetEntity ) );
}
n = Ballistics( firePos, target, projectileSpeed, projGravity[2], ballistics );
if ( n == 0 ) {
// there is no valid trajectory
aimDir = target - firePos;
aimDir.Normalize();
return false;
}
// make sure the first angle is the smallest
if ( n == 2 ) {
if ( ballistics[1].angle < ballistics[0].angle ) {
a = ballistics[0].angle; ballistics[0].angle = ballistics[1].angle; ballistics[1].angle = a;
t = ballistics[0].time; ballistics[0].time = ballistics[1].time; ballistics[1].time = t;
}
}
// test if there is a collision free trajectory
for ( i = 0; i < n; i++ ) {
pitch = DEG2RAD( ballistics[i].angle );
idMath::SinCos( pitch, s, c );
dir[i] = target - firePos;
dir[i].z = 0.0f;
dir[i] *= c * idMath::InvSqrt( dir[i].LengthSqr() );
dir[i].z = s;
zVel = projectileSpeed * dir[i].z;
if ( ai_debugTrajectory.GetBool() ) {
t = ballistics[i].time / 100.0f;
velocity = dir[i] * projectileSpeed;
lastPos = firePos;
pos = firePos;
for ( j = 1; j < 100; j++ ) {
pos += velocity * t;
velocity += projGravity * t;
gameRenderWorld->DebugLine( colorCyan, lastPos, pos );
lastPos = pos;
}
}
if ( TestTrajectory( firePos, target, zVel, projGravity[2], ballistics[i].time, firePos.z + max_height, clip, clipmask, ignore, targetEntity, drawtime ) ) {
aimDir = dir[i];
return true;
}
}
aimDir = dir[0];
// there is no collision free trajectory
return false;
}