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.
418 lines
12 KiB
418 lines
12 KiB
//========= Copyright © 2009, Valve Corporation, All rights reserved. ============//
|
|
//
|
|
// Purpose: Implements the regular grid nav as required by DOTA. Builds, renders,
|
|
// and saves out the nav. Traversable edges and cells are determined by
|
|
// picking into the world.
|
|
//
|
|
//=============================================================================//
|
|
|
|
#include "stdafx.h"
|
|
#include "gridnav.h"
|
|
#include "render3dms.h"
|
|
#include "mapdoc.h"
|
|
#include "filesystem.h"
|
|
#include "resource.h"
|
|
#include "progdlg.h"
|
|
|
|
bool CGridNav::sm_bEnabled = false;
|
|
float CGridNav::sm_flEdgeSize = 0.0f;
|
|
float CGridNav::sm_flOffsetX = 0.0f;
|
|
float CGridNav::sm_flOffsetY = 0.0f;
|
|
float CGridNav::sm_flTraceHeight = 0.0f;
|
|
|
|
CGridNav::CGridNav()
|
|
: m_vLatestCameraPos( Vector( 0.0f, 0.0f, 0.0f ) )
|
|
, m_vLatestCameraDir( Vector( 0.0f, 0.0f, 0.0f ) )
|
|
, m_bNeedsCameraRecompute( true )
|
|
, m_flTimeCameraLastMoved( 0.0f )
|
|
, m_nTicksCameraStill( 0 )
|
|
, m_bPreviewActive( false )
|
|
{
|
|
}
|
|
|
|
void CGridNav::Init( bool bEnabled, float flEdgeSize, float flOffsetX, float flOffsetY, float flTraceHeight )
|
|
{
|
|
sm_bEnabled = bEnabled;
|
|
sm_flEdgeSize = flEdgeSize;
|
|
sm_flOffsetX = flOffsetX;
|
|
sm_flOffsetY = flOffsetY;
|
|
sm_flTraceHeight = flTraceHeight;
|
|
}
|
|
|
|
|
|
void CGridNav::Render( CRender3D *pRender, const Vector &vViewPos, const Vector &vViewDir )
|
|
{
|
|
const float flEdgeSize = sm_flEdgeSize;
|
|
const float flHalfEdgeSize = flEdgeSize * 0.5f;
|
|
const float flHalfEdgeSizeBuffered = flHalfEdgeSize * 0.95f;
|
|
|
|
EditorRenderMode_t oldRenderMode = pRender->GetCurrentRenderMode();
|
|
Color oldDrawColor;
|
|
pRender->GetDrawColor( oldDrawColor );
|
|
pRender->SetRenderMode( RENDER_MODE_WIREFRAME );
|
|
|
|
|
|
FOR_EACH_VEC( m_CurrentCells, it )
|
|
{
|
|
const CGridNavCell &cell = m_CurrentCells[it];
|
|
|
|
const Color drawColor = ( cell.m_bTraversable ? Color( 0, 255, 0 ) : Color( 255, 0, 0 ) );
|
|
|
|
const float CELL_DRAW_LEVITATION = 5.0f;
|
|
|
|
const int i = cell.m_nGridPosX;
|
|
const int j = cell.m_nGridPosY;
|
|
const Vector vCenter( i * sm_flEdgeSize + sm_flOffsetX, j * sm_flEdgeSize + sm_flOffsetY, cell.m_flHeight + CELL_DRAW_LEVITATION );
|
|
|
|
const float d = flHalfEdgeSizeBuffered;
|
|
const Vector p1( vCenter.x - d, vCenter.y - d, vCenter.z );
|
|
const Vector p2( vCenter.x + d, vCenter.y - d, vCenter.z );
|
|
const Vector p3( vCenter.x + d, vCenter.y + d, vCenter.z );
|
|
const Vector p4( vCenter.x - d, vCenter.y + d, vCenter.z );
|
|
|
|
pRender->SetDrawColor( drawColor );
|
|
pRender->DrawLine( p1, p2 );
|
|
pRender->DrawLine( p2, p3 );
|
|
pRender->DrawLine( p3, p4 );
|
|
pRender->DrawLine( p4, p1 );
|
|
}
|
|
|
|
pRender->SetRenderMode( oldRenderMode );
|
|
pRender->SetDrawColor( oldDrawColor );
|
|
}
|
|
|
|
|
|
void CGridNav::Update( CMapDoc *pMapDoc, const Vector &vViewPos, const Vector &vViewDir )
|
|
{
|
|
Assert( pMapDoc );
|
|
|
|
bool cameraMoving = ( vViewPos != m_vLatestCameraPos || vViewDir != m_vLatestCameraDir );
|
|
|
|
m_vLatestCameraPos = vViewPos;
|
|
m_vLatestCameraDir = vViewDir;
|
|
|
|
if ( cameraMoving )
|
|
{
|
|
m_bNeedsCameraRecompute = true;
|
|
m_flTimeCameraLastMoved = pMapDoc->GetTime();
|
|
m_nTicksCameraStill = 0;
|
|
return;
|
|
}
|
|
|
|
if ( !m_bNeedsCameraRecompute )
|
|
return;
|
|
|
|
++m_nTicksCameraStill;
|
|
|
|
// don't process until we've been still for long enough both in real time and frame count
|
|
if ( pMapDoc->GetTime() - m_flTimeCameraLastMoved < 0.2f || m_nTicksCameraStill < 10 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// the camera is still, recompute working set of nav cells
|
|
m_bNeedsCameraRecompute = false;
|
|
|
|
Vector vPickHitPos;
|
|
if ( !pMapDoc->PickTrace( vViewPos, vViewDir, &vPickHitPos ) )
|
|
return;
|
|
|
|
m_CurrentCells.RemoveAll();
|
|
|
|
const int CELL_GRAB_RADIUS = 15;
|
|
const float flEdgeSize = sm_flEdgeSize;
|
|
const float flHalfEdgeSize = flEdgeSize * 0.5f;
|
|
const float flCenterToCornerLen = sqrtf( 2.f ) * flHalfEdgeSize;
|
|
|
|
const int nCenterI = CoordToGridPosX( vPickHitPos.x );
|
|
const int nCenterJ = CoordToGridPosY( vPickHitPos.y );
|
|
|
|
for ( int j = -CELL_GRAB_RADIUS; j <= CELL_GRAB_RADIUS; ++j )
|
|
{
|
|
const int curJ = nCenterJ + j;
|
|
const float y = GridPosYToCoordCenter( curJ );
|
|
|
|
for ( int i = -CELL_GRAB_RADIUS; i <= CELL_GRAB_RADIUS; ++i )
|
|
{
|
|
const int curI = nCenterI + i;
|
|
const float x = GridPosXToCoordCenter( curI );
|
|
|
|
const Vector vTracePos( x, y, sm_flTraceHeight );
|
|
Vector vTraceHitPos;
|
|
bool bHitClip = false;
|
|
if ( !pMapDoc->DropTraceOnDisplacementsAndClips( vTracePos, &vTraceHitPos, &bHitClip ) )
|
|
continue;
|
|
|
|
const float centerHeight = vTraceHitPos.z;
|
|
float maxHeight = centerHeight;
|
|
|
|
// reject cells with centers outside a tolerance of the view vector
|
|
const Vector vViewToCenter = vTraceHitPos - vViewPos;
|
|
const Vector vViewToCenterDir = vViewToCenter.Normalized();
|
|
if ( DotProduct( vViewToCenterDir, vViewDir ) < 0.8f )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const float d = flHalfEdgeSize;
|
|
Vector cornerTracePoints[] = {
|
|
Vector( vTracePos.x - d, vTracePos.y + d, vTracePos.z ),
|
|
Vector( vTracePos.x - d, vTracePos.y - d, vTracePos.z ),
|
|
Vector( vTracePos.x + d, vTracePos.y + d, vTracePos.z ),
|
|
Vector( vTracePos.x + d, vTracePos.y - d, vTracePos.z )
|
|
};
|
|
|
|
bool bTracesOk = true;
|
|
bool bSlopesWalkable = true;
|
|
for ( int nCorner = 0; nCorner < 4; ++nCorner )
|
|
{
|
|
Vector vCornerTraceHitPos;
|
|
bool bCornerHitClip;
|
|
if ( !pMapDoc->DropTraceOnDisplacementsAndClips( cornerTracePoints[nCorner], &vCornerTraceHitPos, &bCornerHitClip ) )
|
|
{
|
|
bTracesOk = false;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
bHitClip = bHitClip || bCornerHitClip;
|
|
|
|
maxHeight = max( vCornerTraceHitPos.z, maxHeight );
|
|
|
|
float flDelta = fabs( vCornerTraceHitPos.z - centerHeight );
|
|
if ( flDelta > flCenterToCornerLen ) // slope > 45 degrees
|
|
{
|
|
bSlopesWalkable = false;
|
|
}
|
|
}
|
|
}
|
|
if ( !bTracesOk )
|
|
continue; // this cell is invalid because we encountered a bad trace, try next cell
|
|
|
|
CGridNavCell newCell;
|
|
newCell.m_nGridPosX = curI;
|
|
newCell.m_nGridPosY = curJ;
|
|
newCell.m_bTraversable = !bHitClip && bSlopesWalkable;
|
|
newCell.m_flHeight = maxHeight;
|
|
m_CurrentCells.AddToTail( newCell );
|
|
}
|
|
}
|
|
}
|
|
|
|
void CGridNav::GenerateGridNavFile( const char *pFileFullPath )
|
|
{
|
|
Assert( pFileFullPath );
|
|
|
|
CMapDoc *pMapDoc = CMapDoc::GetActiveMapDoc();
|
|
if ( !pMapDoc )
|
|
return;
|
|
|
|
// Error if we can't open the file for writing
|
|
if ( g_pFullFileSystem->FileExists( pFileFullPath, NULL ) && !g_pFullFileSystem->IsFileWritable( pFileFullPath, NULL ) )
|
|
{
|
|
//AfxMessageBox( NULL, "Grid nav file already exists and is not writable. Unable to generate grid nav.", "Error", MB_OK );
|
|
AfxMessageBox( "Grid nav file already exists and is not writable. Unable to generate grid nav.");
|
|
return;
|
|
}
|
|
|
|
// Progress dialog
|
|
CProgressDlg *pProgress = new CProgressDlg;
|
|
pProgress->Create();
|
|
pProgress->SetStep( 1 );
|
|
pProgress->SetWindowText( "Constructing Navigation Grid..." );
|
|
pProgress->SetRange( 0, 100 );
|
|
|
|
// find the edges. Edge test order: NORTH, EAST, SOUTH, WEST. Assumes origin is on map.
|
|
const float EDGE_TEST_MAX = 100000.0f;
|
|
const float EDGE_TEST_TERMINATE_INTERVAL = 1.0f;
|
|
int nGridMinX, nGridMaxX, nGridMinY, nGridMaxY;
|
|
int *result[4] = { &nGridMaxY, &nGridMaxX, &nGridMinY, &nGridMinX };
|
|
|
|
for ( int nDir = 0; nDir < 4; ++nDir )
|
|
{
|
|
const int nTestAxis = ( nDir + 1 ) % 2;
|
|
const int nStillAxis = 1 - nTestAxis;
|
|
const float flSign = ( nDir <= 1 ? 1.0f : -1.0f );
|
|
|
|
float pos[2];
|
|
float flCurDelta = EDGE_TEST_MAX * 0.5f;
|
|
float flCurDist = EDGE_TEST_MAX * 0.5f;
|
|
float flMaxDist = 0.0f;
|
|
|
|
pos[nStillAxis] = 0.0f;
|
|
|
|
while ( flCurDelta > EDGE_TEST_TERMINATE_INTERVAL )
|
|
{
|
|
pos[nTestAxis] = flCurDist * flSign;
|
|
|
|
// trace here
|
|
Vector vTracePos( pos[0], pos[1], sm_flTraceHeight );
|
|
float moveDir = -1.0f;
|
|
if ( pMapDoc->DropTraceOnDisplacementsAndClips( vTracePos, NULL, NULL ) )
|
|
{
|
|
// we hit something, must move forward
|
|
moveDir = 1.0f;
|
|
flMaxDist = flCurDist;
|
|
}
|
|
|
|
flCurDelta *= 0.5f;
|
|
|
|
flCurDist += flCurDelta * moveDir;
|
|
}
|
|
|
|
(*result[nDir]) = ( nTestAxis == 0 ? CoordToGridPosX( flMaxDist * flSign ) : CoordToGridPosY( flMaxDist * flSign ) );
|
|
}
|
|
|
|
int nGridWidth = nGridMaxX - nGridMinX + 1;
|
|
int nGridHeight = nGridMaxY - nGridMinY + 1;
|
|
|
|
byte writeByte = 0;
|
|
int nWriteBitPos = 0;
|
|
|
|
// open the file for writing
|
|
CUtlBuffer fileBuffer( 1024, 1024 );
|
|
|
|
// .gnv file header
|
|
const unsigned int GRID_NAV_MAGIC_NUMBER = 0xFADEBEAD;
|
|
fileBuffer.PutInt( GRID_NAV_MAGIC_NUMBER );
|
|
fileBuffer.PutFloat( sm_flEdgeSize );
|
|
fileBuffer.PutFloat( sm_flOffsetX );
|
|
fileBuffer.PutFloat( sm_flOffsetY );
|
|
fileBuffer.PutInt( nGridWidth );
|
|
fileBuffer.PutInt( nGridHeight );
|
|
fileBuffer.PutInt( nGridMinX );
|
|
fileBuffer.PutInt( nGridMinY );
|
|
|
|
const int nMaxProgressVal = nGridHeight * nGridWidth - 1;
|
|
|
|
const float flEdgeSize = sm_flEdgeSize;
|
|
const float flHalfEdgeSize = flEdgeSize * 0.5f;
|
|
const float flCenterToCornerLen = sqrtf( 2.f ) * flHalfEdgeSize;
|
|
|
|
for( int j = 0; j < nGridHeight; ++j )
|
|
{
|
|
int curJ = nGridMinY + j;
|
|
float flCenterY = GridPosYToCoordCenter( curJ );
|
|
|
|
float prevHeights[2] = { 0.0f, 0.0f }; // northeast, southeast corners
|
|
bool prevHitClip[2] = { false, false };
|
|
bool bPrevOk = false;
|
|
|
|
for( int i = 0; i < nGridWidth; ++i )
|
|
{
|
|
const int nProgressVal = ( 100 * ( j * nGridWidth + i ) ) / nMaxProgressVal;
|
|
pProgress->SetPos( nProgressVal );
|
|
|
|
int curI = nGridMinX + i;
|
|
float flCenterX = GridPosXToCoordCenter( curI );
|
|
|
|
bool bSlopesWalkable = true;
|
|
bool bTracesOk = true;
|
|
|
|
// trace to find the center of this cell
|
|
const Vector vTracePos( flCenterX, flCenterY, sm_flTraceHeight );
|
|
Vector vTraceHitPos;
|
|
bool bHitClip = false;
|
|
if ( !pMapDoc->DropTraceOnDisplacementsAndClips( vTracePos, &vTraceHitPos, &bHitClip ) )
|
|
{
|
|
bTracesOk = false;
|
|
}
|
|
else
|
|
{
|
|
const float centerHeight = vTraceHitPos.z;
|
|
|
|
const float d = flHalfEdgeSize;
|
|
Vector cornerTracePoints[] = {
|
|
Vector( vTracePos.x - d, vTracePos.y + d, vTracePos.z ),
|
|
Vector( vTracePos.x - d, vTracePos.y - d, vTracePos.z ),
|
|
Vector( vTracePos.x + d, vTracePos.y + d, vTracePos.z ),
|
|
Vector( vTracePos.x + d, vTracePos.y - d, vTracePos.z )
|
|
};
|
|
|
|
for ( int nCorner = 0; nCorner < 4; ++nCorner )
|
|
{
|
|
// after passing west corners that could have been previously computed, reset ok flag
|
|
if ( nCorner == 2 )
|
|
{
|
|
bPrevOk = true;
|
|
}
|
|
|
|
Vector vCornerTraceHitPos;
|
|
bool bCornerHitClip;
|
|
|
|
// see if we already have the trace results from the previous cell
|
|
if ( nCorner <= 1 && bPrevOk )
|
|
{
|
|
vCornerTraceHitPos = cornerTracePoints[nCorner];
|
|
vCornerTraceHitPos.z = prevHeights[nCorner];
|
|
bCornerHitClip = prevHitClip[nCorner];
|
|
}
|
|
else
|
|
{
|
|
// trace
|
|
if ( !pMapDoc->DropTraceOnDisplacementsAndClips( cornerTracePoints[nCorner], &vCornerTraceHitPos, &bCornerHitClip ) )
|
|
{
|
|
bTracesOk = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// on east 2 corners, store result for next cell
|
|
if ( nCorner >= 2 )
|
|
{
|
|
prevHeights[nCorner-2] = vCornerTraceHitPos.z;
|
|
prevHitClip[nCorner-2] = bCornerHitClip;
|
|
}
|
|
|
|
bHitClip = bHitClip || bCornerHitClip;
|
|
|
|
float flDelta = fabs( vCornerTraceHitPos.z - centerHeight );
|
|
if ( flDelta > flCenterToCornerLen ) // slope > 45 degrees
|
|
{
|
|
bSlopesWalkable = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool bTraversable = !bHitClip && bSlopesWalkable;
|
|
|
|
if ( !bTracesOk )
|
|
{
|
|
// this cell is invalid because we encountered a bad trace
|
|
bTraversable = false;
|
|
bPrevOk = false;
|
|
}
|
|
|
|
// add resulting bit
|
|
if ( bTraversable )
|
|
{
|
|
writeByte |= ( 1 << nWriteBitPos );
|
|
}
|
|
++nWriteBitPos;
|
|
|
|
// write when byte is full
|
|
if ( nWriteBitPos >= 8 )
|
|
{
|
|
fileBuffer.PutUnsignedChar( writeByte );
|
|
writeByte = 0;
|
|
nWriteBitPos = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// write any trailing bits
|
|
if ( nWriteBitPos != 0 )
|
|
{
|
|
fileBuffer.PutUnsignedChar( writeByte );
|
|
}
|
|
|
|
// write to file
|
|
if ( !g_pFullFileSystem->WriteFile( pFileFullPath, NULL, fileBuffer ) )
|
|
{
|
|
Warning( "Unable to save %d bytes to %s\n", fileBuffer.Size(), pFileFullPath );
|
|
}
|
|
|
|
// Destroy the progress meter
|
|
pProgress->DestroyWindow();
|
|
delete pProgress;
|
|
}
|