//========= 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; }