//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// // // Purpose: // // $NoKeywords: $ //=============================================================================// #include "cbase.h" #include "ai_movesolver.h" #include "ndebugoverlay.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" //----------------------------------------------------------------------------- inline float round( float f ) { return (float)( (int)( f + 0.5 ) ); } //----------------------------------------------------------------------------- // CAI_MoveSolver //----------------------------------------------------------------------------- // The epsilon used by the solver const float AIMS_EPS = 0.01; //----------------------------------------------------------------------------- // Visualization //----------------------------------------------------------------------------- void CAI_MoveSolver::VisualizeRegulations( const Vector& origin ) { if ( m_Regulations.Count() ) { CAI_MoveSuggestions regulations; regulations.AddVectorToTail( m_Regulations ); NormalizeSuggestions( ®ulations[0], (®ulations[0]) + regulations.Count() ); Vector side1, mid, side2; for (int i = regulations.Count(); --i >= 0; ) { // Compute the positions of the angles... float flMinAngle = regulations[i].arc.center - regulations[i].arc.span * 0.5f; float flMaxAngle = regulations[i].arc.center + regulations[i].arc.span * 0.5f; side1 = UTIL_YawToVector( flMinAngle ); side2 = UTIL_YawToVector( flMaxAngle ); mid = UTIL_YawToVector( regulations[i].arc.center ); // Stronger weighted ones are bigger if ( regulations[i].weight < 0 ) { float flLength = 10 + 40 * ( regulations[i].weight * -1.0); side1 *= flLength; side2 *= flLength; mid *= flLength; side1 += origin; side2 += origin; mid += origin; NDebugOverlay::Triangle(origin, mid, side1, 255, 0, 0, 48, true, 0.1f ); NDebugOverlay::Triangle(origin, side2, mid, 255, 0, 0, 48, true, 0.1f ); } } } } //------------------------------------- // Purpose: The actual solver function. Reweights according to type and sums // all the suggestions, identifying the best. //------------------------------------- bool CAI_MoveSolver::Solve( const AI_MoveSuggestion_t *pSuggestions, int nSuggestions, AI_MoveSolution_t *pResult) { //--------------------------------- // // Quick out // if ( !nSuggestions ) return false; if ( nSuggestions == 1 && m_Regulations.Count() == 0 && pSuggestions->type == AIMST_MOVE ) { pResult->dir = pSuggestions->arc.center; return true; } //--------------------------------- // // Setup // CAI_MoveSuggestions suggestions; suggestions.EnsureCapacity( m_Regulations.Count() + nSuggestions ); suggestions.CopyArray( pSuggestions, nSuggestions); suggestions.AddVectorToTail( m_Regulations ); // Initialize the solver const int NUM_SOLUTIONS = 120; const int SOLUTION_ANG = 360 / NUM_SOLUTIONS; COMPILE_TIME_ASSERT( ( 360 % NUM_SOLUTIONS ) == 0 ); struct Solution_t { // The sum bias float bias; float highBias; AI_MoveSuggestion_t *pHighSuggestion; }; Solution_t solutions[NUM_SOLUTIONS] = { 0 }; //--------------------------------- // The first thing we do is reweight and normalize the weights into a range of [-1..1], where // a negative weight is a repulsion. This becomes a bias for the solver. // @TODO (toml 06-18-02): this can be made sligtly more optimal by precalculating regulation adjusted weights Assert( suggestions.Count() >= 1 ); NormalizeSuggestions( &suggestions[0], (&suggestions[0]) + suggestions.Count() ); // // Add the biased suggestions to the solutions // for ( int iSuggestion = 0; iSuggestion < suggestions.Count(); ++iSuggestion ) { AI_MoveSuggestion_t ¤t = suggestions[iSuggestion]; // Convert arc values to solution indices relative to right post. Right is angle down, left is angle up. float halfSpan = current.arc.span * 0.5; int center = round( ( halfSpan * NUM_SOLUTIONS ) / 360 ); int left = ( current.arc.span * NUM_SOLUTIONS ) / 360; float angRight = current.arc.center - halfSpan; if (angRight < 0.0) angRight += 360; int base = ( angRight * NUM_SOLUTIONS ) / 360; // Sweep from left to right, summing the bias. For positive suggestions, // the bias is further weighted to favor the center of the arc. const float positiveDegradePer180 = 0.05; // i.e., lose 5% of weight by the time hit 180 degrees off center const float positiveDegrade = ( positiveDegradePer180 / ( NUM_SOLUTIONS * 0.5 ) ); for ( int i = 0; i < left + 1; ++i ) { float bias = 0.0; if ( current.weight > 0) { int iOffset = center - i; float degrade = abs( iOffset ) * positiveDegrade; if ( ( (current.flags & AIMS_FAVOR_LEFT ) && i > center ) || ( (current.flags & AIMS_FAVOR_RIGHT) && i < center ) ) { degrade *= 0.9; } bias = current.weight - ( current.weight * degrade ); } else bias = current.weight; int iCurSolution = (base + i) % NUM_SOLUTIONS; solutions[iCurSolution].bias += bias; if ( bias > solutions[iCurSolution].highBias ) { solutions[iCurSolution].highBias = bias; solutions[iCurSolution].pHighSuggestion = ¤t; } } } // // Find the best solution // int best = -1; float biasBest = 0; for ( int i = 0; i < NUM_SOLUTIONS; ++i ) { if ( solutions[i].bias > biasBest ) { best = i; biasBest = solutions[i].bias; } } if ( best == -1 ) return false; // no solution // // Construct the results // float result = best * SOLUTION_ANG; // If the matching suggestion is within the solution, use that as the result, // as it is valid and more precise. const float suggestionCenter = solutions[best].pHighSuggestion->arc.center; if ( suggestionCenter > result && suggestionCenter <= result + SOLUTION_ANG ) result = suggestionCenter; pResult->dir = result; return true; } //------------------------------------- // Purpose: Adjusts the suggestion weights according to the type of the suggestion, // apply the appropriate sign, ensure values are in expected ranges //------------------------------------- struct AI_MoveSuggWeights { float min; float max; }; static AI_MoveSuggWeights g_AI_MoveSuggWeights[] = // @TODO (toml 06-18-02): these numbers need tuning { { 0.20, 1.00 }, // AIMST_MOVE { -0.00, -0.25 }, // AIMST_AVOID_DANGER { -0.00, -0.25 }, // AIMST_AVOID_OBJECT { -0.00, -0.25 }, // AIMST_AVOID_NPC { -0.00, -0.25 }, // AIMST_AVOID_WORLD { -1.00, -1.00 }, // AIMST_NO_KNOWLEDGE { -0.60, -0.60 }, // AIMST_OSCILLATION_DETERRANCE { 0.00, 0.00 }, // AIMST_INVALID }; void CAI_MoveSolver::NormalizeSuggestions( AI_MoveSuggestion_t *pBegin, AI_MoveSuggestion_t *pEnd ) { while ( pBegin != pEnd ) { const float min = g_AI_MoveSuggWeights[pBegin->type].min; const float max = g_AI_MoveSuggWeights[pBegin->type].max; Assert( pBegin->weight >= -AIMS_EPS && pBegin->weight <= 1.0 + AIMS_EPS ); if ( pBegin->weight < AIMS_EPS ) // zero normalizes to zero pBegin->weight = 0.0; else pBegin->weight = ( ( max - min ) * pBegin->weight ) + min; while (pBegin->arc.center < 0) pBegin->arc.center += 360; while (pBegin->arc.center >= 360) pBegin->arc.center -= 360; ++pBegin; } } //------------------------------------- bool CAI_MoveSolver::HaveRegulationForObstacle( CBaseEntity *pEntity) { for ( int i = 0; i < m_Regulations.Count(); ++i ) { if ( m_Regulations[i].hObstacleEntity != NULL && pEntity == m_Regulations[i].hObstacleEntity.Get() ) { return true; } } return false; } //----------------------------------------------------------------------------- // // Commands and tests // #ifdef DEBUG CON_COMMAND(ai_test_move_solver, "Tests the AI move solver system") { #ifdef DEBUG const float EPS = 0.001; #endif DevMsg( "Beginning move solver tests...\n" ); CAI_MoveSolver solver; AI_MoveSolution_t solution; int i; // // Value in, no regulations, should yield value out // { DevMsg( "Simple... " ); for (i = 0; i < 360; ++i) { Assert( solver.Solve( AI_MoveSuggestion_t( AIMST_MOVE, 1, i, 180 ), &solution ) ); Assert( solution.dir == (float)i ); } DevMsg( "pass.\n" ); solver.ClearRegulations(); } // // Two values in, should yield the first // { DevMsg( "Two positive... " ); AI_MoveSuggestion_t suggestions[2]; suggestions[0].Set( AIMST_MOVE, 1.0, 180, 100 ); suggestions[1].Set( AIMST_MOVE, 0.5, 0, 100 ); Assert( solver.Solve( suggestions, 2, &solution ) ); Assert( solution.dir == (float)suggestions[0].arc.center ); DevMsg( "pass.\n" ); solver.ClearRegulations(); } // // Two values in, first regulated, should yield the second // { DevMsg( "Avoid one of two... " ); AI_MoveSuggestion_t suggestions[2]; solver.AddRegulation(AI_MoveSuggestion_t( AIMST_AVOID_OBJECT, 1, 260, 60 ) ); suggestions[0].Set( AIMST_MOVE, 1.0, 270, 45 ); suggestions[1].Set( AIMST_MOVE, 1.0, 0, 45 ); Assert( solver.Solve( suggestions, 2, &solution ) ); Assert( solution.dir == (float)suggestions[1].arc.center ); DevMsg( "pass.\n" ); solver.ClearRegulations(); } // // No solution // { DevMsg( "No solution... " ); AI_MoveSuggestion_t suggestions[2]; suggestions[0].Set( AIMST_MOVE, 1.0, 270, 90 ); suggestions[1].Set( AIMST_AVOID_OBJECT, 1.0, 260, 180 ); Assert( !solver.Solve( suggestions, 2, &solution ) ); DevMsg( "pass.\n" ); solver.ClearRegulations(); } // // Nearest solution, in tolerance // { DevMsg( "Nearest solution, in tolerance... " ); AI_MoveSuggestion_t suggestions[2]; suggestions[0].Set( AIMST_MOVE, 1.0, 278, 90 ); suggestions[1].Set( AIMST_AVOID_OBJECT, 1.0, 260, 24 ); Assert( solver.Solve( suggestions, 2, &solution ) ); Assert( solution.dir == (float)suggestions[0].arc.center ); DevMsg( "pass.\n" ); solver.ClearRegulations(); } // // Nearest solution // { DevMsg( "Nearest solution... " ); AI_MoveSuggestion_t suggestions[2]; suggestions[0].Set( AIMST_MOVE, 1.0, 270, 90 ); suggestions[1].Set( AIMST_AVOID_OBJECT, 1.0, 260, 40 ); Assert( solver.Solve( suggestions, 2, &solution ) ); Assert( solution.dir - 282 < EPS ); // given 60 solutions DevMsg( "pass.\n" ); solver.ClearRegulations(); } } #endif //=============================================================================