コード例 #1
0
ファイル: bot.cpp プロジェクト: DenMSC/qfusion
void Bot::CheckAlertSpots(const StaticVector<edict_t *, MAX_CLIENTS> &visibleTargets)
{
    float scores[MAX_ALERT_SPOTS];

    // First compute scores (good for instruction cache)
    for (unsigned i = 0; i < alertSpots.size(); ++i)
    {
        float score = 0.0f;
        const auto &alertSpot = alertSpots[i];
        const float squareRadius = alertSpot.radius * alertSpot.radius;
        const float invRadius = 1.0f / alertSpot.radius;
        for (const edict_t *ent: visibleTargets)
        {
            float squareDistance = DistanceSquared(ent->s.origin, alertSpot.origin.Data());
            if (squareDistance > squareRadius)
                continue;
            float distance = Q_RSqrt(squareDistance + 0.001f);
            score += 1.0f - distance * invRadius;
            // Put likely case first
            if (!(ent->s.effects & EF_CARRIER))
                score *= alertSpot.regularEnemyInfluenceScale;
            else
                score *= alertSpot.carrierEnemyInfluenceScale;
        }
        // Clamp score by a max value
        clamp_high(score, 3.0f);
        // Convert score to [0, 1] range
        score /= 3.0f;
        // Get a square root of score (values closer to 0 gets scaled more than ones closer to 1)
        score = 1.0f / Q_RSqrt(score + 0.001f);
        // Sanitize
        clamp(score, 0.0f, 1.0f);
        scores[i] = score;
    }

    // Then call callbacks
    const unsigned levelTime = level.time;
    for (unsigned i = 0; i < alertSpots.size(); ++i)
    {
        auto &alertSpot = alertSpots[i];
        unsigned nonReportedFor = levelTime - alertSpot.lastReportedAt;
        if (nonReportedFor >= 1000)
            alertSpot.lastReportedScore = 0.0f;

        // Since scores are sanitized, they are in range [0.0f, 1.0f], and abs(scoreDelta) is in range [-1.0f, 1.0f];
        float scoreDelta = scores[i] - alertSpot.lastReportedScore;
        if (scoreDelta >= 0)
        {
            if (nonReportedFor >= 1000 - scoreDelta * 500)
                alertSpot.Alert(this, scores[i]);
        }
        else
        {
            if (nonReportedFor >= 500 - scoreDelta * 500)
                alertSpot.Alert(this, scores[i]);
        }
    }
}
コード例 #2
0
ファイル: bot.cpp プロジェクト: DenMSC/qfusion
bool Bot::MayKeepRunningInCombat() const
{
    if (!HasEnemy())
        FailWith("MayKeepRunningInCombat(): there is no enemy");

    Vec3 enemyToBotDir = Vec3(self->s.origin) - EnemyOrigin();
    bool enemyMayHit = true;
    if (IsEnemyAStaticSpot())
    {
        enemyMayHit = false;
    }
    else if (EnemyFireDelay() > 300)
    {
        enemyMayHit = false;
    }
    else
    {
        Vec3 enemyLookDir = EnemyLookDir();
        float squaredDistance = enemyToBotDir.SquaredLength();
        if (squaredDistance > 1)
        {
            float distance = 1.0f / Q_RSqrt(squaredDistance);
            enemyToBotDir *= 1.0f / distance;
            // Compute a cosine of angle between enemy look dir and enemy to bot dir
            float cosPhi = enemyLookDir.Dot(enemyToBotDir);
            // Be aware of RL splash on this range
            if (distance < 150.0f)
                enemyMayHit = cosPhi > 0.3;
            else if (cosPhi <= 0.3)
                enemyMayHit = false;
            else
            {
                float cotPhi = Q_RSqrt((1.0f / (cosPhi * cosPhi)) - 1);
                float sideMiss = distance / cotPhi;
                // Use hitbox height plus a bit as a worst case
                float hitboxLargestSectionSide = 8.0f + playerbox_stand_maxs[2] - playerbox_stand_mins[2];
                enemyMayHit = sideMiss < hitboxLargestSectionSide;
            }
        }
    }

    if (enemyMayHit)
        return false;

    vec3_t botLookDir;
    AngleVectors(self->s.angles, botLookDir, nullptr, nullptr);
    // Check whether the bot may hit while running
    return ((-enemyToBotDir).Dot(botLookDir) > 0.99);
}
コード例 #3
0
ファイル: bot_items_selector.cpp プロジェクト: Picmip/qfusion
BotItemsSelector::ItemAndGoalWeights BotItemsSelector::ComputeAmmoWeights( const gsitem_t *item ) const {
	if( Inventory()[item->tag] < item->inventory_max ) {
		float quantityFactor = 1.0f - Inventory()[item->tag] / (float)item->inventory_max;
		if( quantityFactor > 0 ) {
			quantityFactor = 1.0f / Q_RSqrt( quantityFactor );
		}

		for( int weapon = WEAP_GUNBLADE; weapon < WEAP_TOTAL; weapon++ ) {
			// TODO: Preache
			const gsitem_t *weaponItem = GS_FindItemByTag( weapon );
			if( weaponItem->ammo_tag == item->tag ) {
				if( Inventory()[weaponItem->tag] ) {
					switch( weaponItem->tag ) {
						case WEAP_ELECTROBOLT:
							return ItemAndGoalWeights( quantityFactor, quantityFactor );
						case WEAP_LASERGUN:
							return ItemAndGoalWeights( quantityFactor * 1.1f, quantityFactor );
						case WEAP_PLASMAGUN:
							return ItemAndGoalWeights( quantityFactor * 1.1f, quantityFactor );
						case WEAP_ROCKETLAUNCHER:
							return ItemAndGoalWeights( quantityFactor, quantityFactor );
						default:
							return ItemAndGoalWeights( 0.5f * quantityFactor, quantityFactor );
					}
				}
				return ItemAndGoalWeights( quantityFactor * 0.33f, quantityFactor * 0.5f );
			}
		}
	}
	return ItemAndGoalWeights( 0.0, 0.0f );
}
コード例 #4
0
ファイル: bot_items_selector.cpp プロジェクト: Picmip/qfusion
BotItemsSelector::ItemAndGoalWeights BotItemsSelector::ComputeWeaponWeights( const gsitem_t *item, bool onlyGotGB ) const {
	if( Inventory()[item->tag] ) {
		// TODO: Precache
		const gsitem_t *ammo = GS_FindItemByTag( item->ammo_tag );
		if( Inventory()[ammo->tag] >= ammo->inventory_max ) {
			return ItemAndGoalWeights( 0, 0 );
		}

		float ammoQuantityFactor = 1.0f - Inventory()[ammo->tag] / (float)ammo->inventory_max;
		if( ammoQuantityFactor > 0 ) {
			ammoQuantityFactor = 1.0f / Q_RSqrt( ammoQuantityFactor );
		}

		switch( item->tag ) {
			case WEAP_ELECTROBOLT:
				return ItemAndGoalWeights( ammoQuantityFactor, 0.5f * ammoQuantityFactor );
			case WEAP_LASERGUN:
				return ItemAndGoalWeights( ammoQuantityFactor * 1.1f, 0.6f * ammoQuantityFactor );
			case WEAP_PLASMAGUN:
				return ItemAndGoalWeights( ammoQuantityFactor * 1.1f, 0.6f * ammoQuantityFactor );
			case WEAP_ROCKETLAUNCHER:
				return ItemAndGoalWeights( ammoQuantityFactor, 0.5f * ammoQuantityFactor );
			default:
				return ItemAndGoalWeights( 0.75f * ammoQuantityFactor, 0.75f * ammoQuantityFactor );
		}
	}

	// We may consider plasmagun in a bot's hand as a top tier weapon too
	const int topTierWeapons[4] = { WEAP_ELECTROBOLT, WEAP_LASERGUN, WEAP_ROCKETLAUNCHER, WEAP_PLASMAGUN };

	// TODO: Precompute
	float topTierWeaponGreed = 0.0f;
	for( int i = 0; i < 4; ++i ) {
		if( !Inventory()[topTierWeapons[i]] ) {
			topTierWeaponGreed += 1.0f;
		}
	}

	for( int i = 0; i < 4; ++i ) {
		if( topTierWeapons[i] == item->tag ) {
			float weight = ( onlyGotGB ? 2.0f : 0.9f ) + ( topTierWeaponGreed - 1.0f ) / 3.0f;
			return ItemAndGoalWeights( weight, weight );
		}
	}

	return onlyGotGB ? ItemAndGoalWeights( 1.5f, 2.0f ) : ItemAndGoalWeights( 0.75f, 0.75f );
}
コード例 #5
0
void TacticalSpotsDetector::SortByVisAndOtherFactors(const OriginParams &params, TraceCheckedAreas &areas)
{
    const aas_area_t *worldAreas = AiAasWorld::Instance()->Areas();
    const aas_areasettings_t *worldAreaSettings = AiAasWorld::Instance()->AreaSettings();

    const float originZ = params.origin[2];
    const float searchRadius = params.searchRadius;

    // A matrix of trace results:
    // -1 means that trace has not been computed
    // 0 means that trace.fraction < 1.0f
    // 1 means that trace.fraction == 1.0f
    signed char traceResultCache[areas.capacity() * areas.capacity()];
    std::fill(traceResultCache, traceResultCache + areas.capacity() * areas.capacity(), -1);

    // Compute area points to avoid doing it in the trace loop.
    vec3_t areaPoints[areas.capacity()];
    for (unsigned i = 0; i < areas.size(); ++i)
    {
        const aas_area_t &area = worldAreas[areas[i].areaNum];
        VectorCopy(area.center, areaPoints[i]);
        areaPoints[i][2] = area.mins[2] + PLAYER_VIEW_GROUND_OFFSET;
    }

    trace_t trace;
    for (unsigned i = 0; i < areas.size(); ++i)
    {
        // Avoid fp <-> int conversions in a loop
        int numVisAreas = 0;
        for (unsigned j = 0; j < i; ++j)
        {
            int cachedTraceResult = traceResultCache[areas.capacity() * i + j];
            if (cachedTraceResult >= 0)
            {
                numVisAreas += cachedTraceResult;
                continue;
            }
            G_Trace(&trace, areaPoints[i], nullptr, nullptr, areaPoints[j], nullptr, MASK_AISOLID);
            // Omit fractional part by intention
            signed char visibility = (signed char)trace.fraction;
            traceResultCache[areas.capacity() * i + j] = visibility;
            traceResultCache[areas.capacity() * j + i] = visibility;
            numVisAreas += visibility;
        }

        // Skip a trace from an area to itself

        for (unsigned j = i + 1; j < areas.size(); ++j)
        {
            int cachedTraceResult = traceResultCache[areas.capacity() * i + j];
            if (cachedTraceResult >= 0)
            {
                numVisAreas += cachedTraceResult;
                continue;
            }
            G_Trace(&trace, areaPoints[i], nullptr, nullptr, areaPoints[j], nullptr, MASK_AISOLID);
            // Omit fractional part by intention
            signed char visibility = (signed char)trace.fraction;
            traceResultCache[areas.capacity() * i + j] = visibility;
            traceResultCache[areas.capacity() * j + i] = visibility;
            numVisAreas += visibility;
        }

        float visFactor = numVisAreas / (float)areas.size();
        visFactor = 1.0f / Q_RSqrt(visFactor);
        areas[i].score *= 0.1f + 0.9f * visFactor;

        // We should modify the final score by following factors.
        // These factors should be checked in earlier calls but mainly for early rejection of non-suitable areas.

        const int areaNum = areas[i].areaNum;

        float height = (worldAreas[areaNum].mins[2] - originZ - minHeightAdvantage);
        float heightFactor = BoundedFraction(height, searchRadius - minHeightAdvantage);
        areas[i].score = ApplyFactor(areas[i].score, heightFactor, heightInfluence);

        const aas_areasettings_t &areaSettings = worldAreaSettings[areaNum];
        if (areaSettings.areaflags & AREA_WALL)
            areas[i].score *= wallPenalty;
        if (areaSettings.areaflags & AREA_LEDGE)
            areas[i].score *= ledgePenalty;
    }

    // Sort results so best score areas are first
    std::sort(areas.begin(), areas.end());
}
コード例 #6
0
void BotTacticalSpotsCache::FindReachableClassEntities( const Vec3 &origin, float radius, const char *classname,
														BotTacticalSpotsCache::ReachableEntities &result ) {
	int *triggerEntities;
	int numEntities = FindNearbyEntities( origin, radius, &triggerEntities );

	ReachableEntities candidateEntities;
	// Copy to locals for faster access (a compiler might be paranoid about aliasing)
	edict_t *gameEdicts = game.edicts;

	if( numEntities > (int)candidateEntities.capacity() ) {
		for( int i = 0; i < numEntities; ++i ) {
			edict_t *ent = gameEdicts + triggerEntities[i];
			// Specify expected strcmp() result explicitly to avoid misinterpreting the condition
			// (Strings are equal if an strcmp() result is zero)
			if( strcmp( ent->classname, classname ) != 0 ) {
				continue;
			}
			float distance = DistanceFast( origin.Data(), ent->s.origin );
			candidateEntities.push_back( EntAndScore( triggerEntities[i], radius - distance ) );
			if( candidateEntities.size() == candidateEntities.capacity() ) {
				break;
			}
		}
	} else {
		for( int i = 0; i < numEntities; ++i ) {
			edict_t *ent = gameEdicts + triggerEntities[i];
			if( strcmp( ent->classname, classname ) != 0 ) {
				continue;
			}
			float distance = DistanceFast( origin.Data(), ent->s.origin );
			candidateEntities.push_back( EntAndScore( triggerEntities[i], radius - distance ) );
		}
	}

	const AiAasWorld *aasWorld = AiAasWorld::Instance();
	AiAasRouteCache *routeCache = self->ai->botRef->routeCache;

	bool testTwoCurrAreas = false;
	int fromAreaNum = 0;
	// If an origin matches actual bot origin
	if( ( origin - self->s.origin ).SquaredLength() < WorldState::OriginVar::MAX_ROUNDING_SQUARE_DISTANCE_ERROR ) {
		// Try testing both areas
		if( self->ai->botRef->CurrAreaNum() != self->ai->botRef->DroppedToFloorAreaNum() ) {
			testTwoCurrAreas = true;
		} else {
			fromAreaNum = self->ai->botRef->CurrAreaNum();
		}
	} else {
		fromAreaNum = aasWorld->FindAreaNum( origin );
	}

	if( testTwoCurrAreas ) {
		int fromAreaNums[2] = { self->ai->botRef->CurrAreaNum(), self->ai->botRef->DroppedToFloorAreaNum() };
		for( EntAndScore &candidate: candidateEntities ) {
			edict_t *ent = gameEdicts + candidate.entNum;

			int toAreaNum = FindMostFeasibleEntityAasArea( ent, aasWorld );
			if( !toAreaNum ) {
				continue;
			}

			int travelTime = 0;
			for( int i = 0; i < 2; ++i ) {
				travelTime = routeCache->TravelTimeToGoalArea( fromAreaNums[i], toAreaNum, Bot::ALLOWED_TRAVEL_FLAGS );
				if( travelTime ) {
					break;
				}
			}
			if( !travelTime ) {
				continue;
			}

			// AAS travel time is in seconds^-2
			float factor = 1.0f / Q_RSqrt( 1.0001f - BoundedFraction( travelTime, 200 ) );
			result.push_back( EntAndScore( candidate.entNum, candidate.score * factor ) );
		}
	} else {
		for( EntAndScore &candidate: candidateEntities ) {
			edict_t *ent = gameEdicts + candidate.entNum;

			int toAreaNum = FindMostFeasibleEntityAasArea( ent, aasWorld );
			if( !toAreaNum ) {
				continue;
			}

			int travelTime = routeCache->TravelTimeToGoalArea( fromAreaNum, toAreaNum, Bot::ALLOWED_TRAVEL_FLAGS );
			if( !travelTime ) {
				continue;
			}

			float factor = 1.0f / Q_RSqrt( 1.0001f - BoundedFraction( travelTime, 200 ) );
			result.push_back( EntAndScore( candidate.entNum, candidate.score * factor ) );
		}
	}

	// Sort entities so best entities are first
	std::sort( result.begin(), result.end() );
}
コード例 #7
0
ファイル: bot.cpp プロジェクト: DenMSC/qfusion
void Bot::RegisterVisibleEnemies()
{
    if(G_ISGHOSTING(self) || GS_MatchState() == MATCH_STATE_COUNTDOWN || GS_ShootingDisabled())
        return;

    CheckIsInThinkFrame(__FUNCTION__);

    // Compute look dir before loop
    vec3_t lookDir;
    AngleVectors(self->s.angles, lookDir, nullptr, nullptr);

    float fov = 110.0f + 69.0f * Skill();
    float dotFactor = cosf((float)DEG2RAD(fov / 2));

    struct EntAndDistance
    {
        int entNum;
        float distance;

        EntAndDistance(int entNum_, float distance_): entNum(entNum_), distance(distance_) {}
        bool operator<(const EntAndDistance &that) const { return distance < that.distance; }
    };

    // Do not call inPVS() and G_Visible() for potential targets inside a loop for all clients.
    // In worst case when all bots may see each other we get N^2 traces and PVS tests
    // First, select all candidate targets along with distance to a bot.
    // Then choose not more than BotBrain::maxTrackedEnemies nearest enemies for calling OnEnemyViewed()
    // It may cause data loss (far enemies may have higher logical priority),
    // but in a common good case (when there are few visible enemies) it preserves data,
    // and in the worst case mentioned above it does not act weird from player POV and prevents server hang up.
    // Note: non-client entities also may be candidate targets.
    StaticVector<EntAndDistance, MAX_EDICTS> candidateTargets;

    for (int i = 1; i < game.numentities; ++i)
    {
        edict_t *ent = game.edicts + i;
        if (botBrain.MayNotBeFeasibleEnemy(ent))
            continue;

        // Reject targets quickly by fov
        Vec3 toTarget(ent->s.origin);
        toTarget -= self->s.origin;
        float squareDistance = toTarget.SquaredLength();
        if (squareDistance < 1)
            continue;
        float invDistance = Q_RSqrt(squareDistance);
        toTarget *= invDistance;
        if (toTarget.Dot(lookDir) < dotFactor)
            continue;

        // It seams to be more instruction cache-friendly to just add an entity to a plain array
        // and sort it once after the loop instead of pushing an entity in a heap on each iteration
        candidateTargets.emplace_back(EntAndDistance(ENTNUM(ent), 1.0f / invDistance));
    }

    std::sort(candidateTargets.begin(), candidateTargets.end());

    // Select inPVS/visible targets first to aid instruction cache, do not call callbacks in loop
    StaticVector<edict_t *, MAX_CLIENTS> targetsInPVS;
    StaticVector<edict_t *, MAX_CLIENTS> visibleTargets;

    static_assert(AiBaseEnemyPool::MAX_TRACKED_ENEMIES <= MAX_CLIENTS, "targetsInPVS capacity may be exceeded");

    for (int i = 0, end = std::min(candidateTargets.size(), botBrain.MaxTrackedEnemies()); i < end; ++i)
    {
        edict_t *ent = game.edicts + candidateTargets[i].entNum;
        if (trap_inPVS(self->s.origin, ent->s.origin))
            targetsInPVS.push_back(ent);
    }

    for (auto ent: targetsInPVS)
        if (G_Visible(self, ent))
            visibleTargets.push_back(ent);

    // Call bot brain callbacks on visible targets
    for (auto ent: visibleTargets)
        botBrain.OnEnemyViewed(ent);

    botBrain.AfterAllEnemiesViewed();

    CheckAlertSpots(visibleTargets);
}