/* =========================================================================== Doom 3 BFG Edition GPL Source Code Copyright (C) 1993-2012 id Software LLC, a ZeniMax Media company. This file is part of the Doom 3 BFG Edition GPL Source Code ("Doom 3 BFG Edition Source Code"). Doom 3 BFG Edition 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 BFG Edition 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 BFG Edition Source Code. If not, see . In addition, the Doom 3 BFG Edition 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 BFG Edition 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 "precompiled.h" #pragma hdrstop #include "Game_local.h" /* ================================================================================================ Contains the AimAssist implementation. ================================================================================================ */ idCVar aa_targetAimAssistEnable( "aa_targetAimAssistEnable", "0", CVAR_BOOL | CVAR_ARCHIVE, "Enables/Disables the entire Aim Assist system" ); idCVar aa_targetAdhesionEnable( "aa_targetAdhesionEnable", "1", CVAR_BOOL, "Enables Target Adhesion" ); idCVar aa_targetFrictionEnable( "aa_targetFrictionEnable", "1", CVAR_BOOL, "Enables Target Friction" ); // Selection idCVar aa_targetMaxDistance( "aa_targetMaxDistance", "3000", CVAR_FLOAT, "The Maximum Distance away for a target to be considered for adhesion, friction and target lock-to" ); idCVar aa_targetSelectionRadius( "aa_targetSelectionRadius", "128.0", CVAR_FLOAT, "Radius used to select targets for auto aiming" ); // Adhesion idCVar aa_targetAdhesionRadius( "aa_targetAdhesionRadius", "96.0", CVAR_FLOAT, "Radius used to apply adhesion amount" ); idCVar aa_targetAdhesionYawSpeedMax( "aa_targetAdhesionYawSpeedMax", "0.6", CVAR_FLOAT, "Max Yaw Adhesion Speed" ); idCVar aa_targetAdhesionPitchSpeedMax( "aa_targetAdhesionPitchSpeedMax", "0.6", CVAR_FLOAT, "Max Pitch Adhesion Speed" ); idCVar aa_targetAdhesionContributionPctMax( "aa_targetAdhesionContributionPctMax", "0.25", CVAR_FLOAT, "Max Adhesion Contribution Percentage - Range 0.0 - 1.0" ); idCVar aa_targetAdhesionPlayerSpeedThreshold( "aa_targetAdhesionPlayerSpeedThreshold", "10.0", CVAR_FLOAT, "Speed Threshold that determines how fast the player needs to move before adhesion is allowed to kick in" ); // Friction idCVar aa_targetFrictionMaxDistance( "aa_targetFrictionMaxDistance", "1024.0", CVAR_FLOAT, "Minimum Distance Friction takes effect" ); idCVar aa_targetFrictionOptimalDistance( "aa_targetFrictionOptimalDistance", "768.0", CVAR_FLOAT, "Optimal Distance for Friction to take an effect" ); idCVar aa_targetFrictionRadius( "aa_targetFrictionRadius", "96.0", CVAR_FLOAT, "Friction Collision Sphere Radius" ); idCVar aa_targetFrictionOptimalRadius( "aa_targetFrictionOptimalRadius", "192.0", CVAR_FLOAT, "Friction Collision Sphere Radius when at Optimal Distance" ); idCVar aa_targetFrictionMultiplierMin( "aa_targetFrictionMultiplierMin", "1.0", CVAR_FLOAT, "Minimum Friction Scalar" ); idCVar aa_targetFrictionMultiplierMax( "aa_targetFrictionMultiplierMax", "0.4", CVAR_FLOAT, "Maximum Friction Scalar" ); /* ======================== idAimAssist::Init ======================== */ void idAimAssist::Init( idPlayer* player_ ) { player = player_; angleCorrection = ang_zero; frictionScalar = 1.0f; lastTargetPos = vec3_zero; } /* ======================== idAimAssist::Update ======================== */ void idAimAssist::Update() { angleCorrection = ang_zero; UpdateNewAimAssist(); } /* ======================== idAimAssist::UpdateNewAimAssist ======================== */ void idAimAssist::UpdateNewAimAssist() { angleCorrection = ang_zero; frictionScalar = 1.0f; idEntity* lastTarget = targetEntity; targetEntity = NULL; // is aim assisting allowed? If not then just bail out if( !aa_targetAimAssistEnable.GetBool() ) { return; } bool forceLastTarget = false; idVec3 targetPos; idEntity* entity = NULL; if( forceLastTarget ) { entity = lastTarget; targetPos = lastTargetPos; } else { entity = FindAimAssistTarget( targetPos ); } if( entity != NULL ) { UpdateFriction( entity, targetPos ); // by default we don't allow adhesion when we are standing still const float playerMovementSpeedThreshold = Square( aa_targetAdhesionPlayerSpeedThreshold.GetFloat() ); float playerSpeed = player->GetPhysics()->GetLinearVelocity().LengthSqr(); // only allow adhesion on actors (ai) or players. Disallow adhesion on any static world entities such as explosive barrels if( playerSpeed > playerMovementSpeedThreshold ) { UpdateAdhesion( entity, targetPos ); } targetEntity = entity; } lastTargetPos = targetPos; } /* ======================== idAimAssist::FindAimAssistTarget ======================== */ idEntity* idAimAssist::FindAimAssistTarget( idVec3& targetPos ) { if( player == NULL ) { return NULL; } //TO DO: Make this faster //TO DO: Defer Traces idEntity* optimalTarget = NULL; float currentBestScore = -idMath::INFINITY; targetPos = vec3_zero; idVec3 cameraPos; idMat3 cameraAxis; player->GetViewPos( cameraPos, cameraAxis ); float maxDistanceSquared = Square( aa_targetMaxDistance.GetFloat() ); idVec3 dirToTarget; float distanceToTargetSquared; idVec3 primaryTargetPos; idVec3 secondaryTargetPos; for( idEntity* entity = gameLocal.aimAssistEntities.Next(); entity != NULL; entity = entity->aimAssistNode.Next() ) { if( !entity->IsActive() ) { continue; } if( entity->IsHidden() ) { continue; } if( entity->IsType( idActor::Type ) ) { idActor* actor = static_cast( entity ); if( actor->team == player->team ) { // In DM, LMS, and Tourney, all players are on the same team if( gameLocal.gameType == GAME_CTF || gameLocal.gameType == GAME_TDM || gameLocal.gameType == GAME_SP ) { continue; } } } if( entity->IsType( idAI::Type ) ) { idAI* aiEntity = static_cast( entity ); if( aiEntity->ReactionTo( player ) == ATTACK_IGNORE ) { continue; } } // check whether we have a valid target position for this entity - skip it if we don't if( !ComputeTargetPos( entity, primaryTargetPos, secondaryTargetPos ) ) { continue; } // is it close enough to us dirToTarget = primaryTargetPos - cameraPos; distanceToTargetSquared = dirToTarget.LengthSqr(); if( distanceToTargetSquared > maxDistanceSquared ) { continue; } // Compute a score in the range of 0..1 for how much are looking towards the target. idVec3 forward = cameraAxis[ 0 ]; forward.Normalize(); dirToTarget.Normalize(); float ViewDirDotTargetDir = idMath::ClampFloat( -1.0f, 1.0f, forward * dirToTarget ); // compute the dot and clamp to account for floating point error // throw out anything thats outside of weapon's global FOV. if( ViewDirDotTargetDir < 0.0f ) { continue; } // to be consistent we always use the primaryTargetPos to compute the score for this entity float computedScore = ComputeEntityAimAssistScore( primaryTargetPos, cameraPos, cameraAxis ); // check if the current score beats our current best score and we have line of sight to it. if( computedScore > currentBestScore ) { // determine if the current target is in our line of sight trace_t tr; gameLocal.clip.TracePoint( tr, cameraPos, primaryTargetPos, MASK_MONSTERSOLID, player ); // did our trace fail? if( ( ( tr.fraction < 1.0f ) && ( tr.c.entityNum != entity->entityNumber ) ) || ( tr.fraction >= 1.0f ) ) { // if the collision test failed for the primary position -- check the secondary position trace_t tr2; gameLocal.clip.TracePoint( tr2, cameraPos, secondaryTargetPos, MASK_MONSTERSOLID, player ); if( ( ( tr2.fraction < 1.0f ) && ( tr2.c.entityNum != entity->entityNumber ) ) || ( tr2.fraction >= 1.0f ) ) { // if the secondary position is also not visible then give up continue; } // we can see the secondary target position so we should consider this entity but use // the secondary position as the target position targetPos = secondaryTargetPos; } else { targetPos = primaryTargetPos; } // if we got here then this is our new best score optimalTarget = entity; currentBestScore = computedScore; } } return optimalTarget; } /* ======================== idAimAssist::ComputeEntityAimAssistScore ======================== */ float idAimAssist::ComputeEntityAimAssistScore( const idVec3& targetPos, const idVec3& cameraPos, const idMat3& cameraAxis ) { float score = 0.0f; idVec3 dirToTarget = targetPos - cameraPos; float distanceToTarget = dirToTarget.Length(); // Compute a score in the range of 0..1 for how much are looking towards the target. idVec3 forward = cameraAxis[0]; forward.Normalize(); dirToTarget.Normalize(); float ViewDirDotTargetDir = idMath::ClampFloat( 0.0f, 1.0f, forward * dirToTarget ); // compute the dot and clamp to account for floating point error // the more we look at the target the higher our score score = ViewDirDotTargetDir; // weigh the score from the view angle higher than the distance score static float aimWeight = 0.8f; score *= aimWeight; // Add a score of 0..1 for how close the target is to the player if( distanceToTarget < aa_targetMaxDistance.GetFloat() ) { float distanceScore = 1.0f - ( distanceToTarget / aa_targetMaxDistance.GetFloat() ); float distanceWeight = 1.0f - aimWeight; score += ( distanceScore * distanceWeight ); } return score * 1000.0f; } /* ======================== idAimAssist::UpdateAdhesion ======================== */ void idAimAssist::UpdateAdhesion( idEntity* pTarget, const idVec3& targetPos ) { if( !aa_targetAdhesionEnable.GetBool() ) { return; } if( !pTarget ) { return; } float contributionPctMax = aa_targetAdhesionContributionPctMax.GetFloat(); idVec3 cameraPos; idMat3 cameraAxis; player->GetViewPos( cameraPos, cameraAxis ); idAngles cameraViewAngles = cameraAxis.ToAngles(); cameraViewAngles.Normalize180(); idVec3 cameraViewPos = cameraPos; idVec3 dirToTarget = targetPos - cameraViewPos; float distanceToTarget = dirToTarget.Length(); idAngles aimAngles = dirToTarget.ToAngles(); aimAngles.Normalize180(); // find the delta aimAngles -= cameraViewAngles; // clamp velocities to some max values aimAngles.yaw = idMath::ClampFloat( -aa_targetAdhesionYawSpeedMax.GetFloat(), aa_targetAdhesionYawSpeedMax.GetFloat(), aimAngles.yaw ); aimAngles.pitch = idMath::ClampFloat( -aa_targetAdhesionPitchSpeedMax.GetFloat(), aa_targetAdhesionPitchSpeedMax.GetFloat(), aimAngles.pitch ); idVec3 forward = cameraAxis[0]; forward.Normalize(); dirToTarget.Normalize(); float ViewDirDotTargetDir = idMath::ClampFloat( 0.0f, 1.0f, forward * dirToTarget ); // compute the dot and clamp to account for floating point error float aimLength = ViewDirDotTargetDir * distanceToTarget; idVec3 aimPoint = cameraPos + ( forward * aimLength ); float delta = idMath::Sqrt( Square( distanceToTarget ) - Square( aimLength ) ); float contribution = idMath::ClampFloat( 0.0f, contributionPctMax, 1.0f - ( delta / aa_targetAdhesionRadius.GetFloat() ) ); angleCorrection.yaw = aimAngles.yaw * contribution; angleCorrection.pitch = aimAngles.pitch * contribution; } /* ======================== idAimAssist::ComputeFrictionRadius ======================== */ float idAimAssist::ComputeFrictionRadius( float distanceToTarget ) { if( ( distanceToTarget <= idMath::FLT_SMALLEST_NON_DENORMAL ) || distanceToTarget > aa_targetFrictionMaxDistance.GetFloat() ) { return aa_targetFrictionRadius.GetFloat(); } float distanceContributionScalar = ( aa_targetFrictionOptimalDistance.GetFloat() > 0.0f ) ? ( distanceToTarget / aa_targetFrictionOptimalDistance.GetFloat() ) : 0.0f; if( distanceToTarget > aa_targetFrictionOptimalDistance.GetFloat() ) { const float range = idMath::ClampFloat( 0.0f, aa_targetFrictionMaxDistance.GetFloat(), aa_targetFrictionMaxDistance.GetFloat() - aa_targetFrictionOptimalDistance.GetFloat() ); if( range > idMath::FLT_SMALLEST_NON_DENORMAL ) { distanceContributionScalar = 1.0f - ( ( distanceToTarget - aa_targetFrictionOptimalDistance.GetFloat() ) / range ); } } return Lerp( aa_targetFrictionRadius.GetFloat(), aa_targetFrictionOptimalRadius.GetFloat(), distanceContributionScalar ); } /* ======================== idAimAssist::UpdateFriction ======================== */ void idAimAssist::UpdateFriction( idEntity* pTarget, const idVec3& targetPos ) { if( !aa_targetFrictionEnable.GetBool() ) { return; } if( pTarget == NULL ) { return; } idVec3 cameraPos; idMat3 cameraAxis; player->GetViewPos( cameraPos, cameraAxis ); idVec3 dirToTarget = targetPos - cameraPos; float distanceToTarget = dirToTarget.Length(); idVec3 forward = cameraAxis[0]; forward.Normalize(); dirToTarget.Normalize(); float ViewDirDotTargetDir = idMath::ClampFloat( 0.0f, 1.0f, forward * dirToTarget ); // compute the dot and clamp to account for floating point error float aimLength = ViewDirDotTargetDir * distanceToTarget; idVec3 aimPoint = cameraPos + ( forward * aimLength ); float delta = idMath::Sqrt( Square( distanceToTarget ) - Square( aimLength ) ); const float radius = ComputeFrictionRadius( distanceToTarget ); if( delta < radius ) { float alpha = 1.0f - ( delta / radius ); frictionScalar = Lerp( aa_targetFrictionMultiplierMin.GetFloat(), aa_targetFrictionMultiplierMax.GetFloat(), alpha ); } } /* ======================== idAimAssist::ComputeTargetPos ======================== */ bool idAimAssist::ComputeTargetPos( idEntity* entity, idVec3& primaryTargetPos, idVec3& secondaryTargetPos ) { primaryTargetPos = vec3_zero; secondaryTargetPos = vec3_zero; if( entity == NULL ) { return false; } // The target point on actors can now be either the head or the torso idActor* actor = NULL; if( entity->IsType( idActor::Type ) ) { actor = ( idActor* ) entity; } if( actor != NULL ) { // Actor AimPoint idVec3 torsoPos; idVec3 headPos = actor->GetEyePosition(); if( actor->GetHeadEntity() != NULL ) { torsoPos = actor->GetHeadEntity()->GetPhysics()->GetOrigin(); } else { const float offsetScale = 0.9f; torsoPos = actor->GetPhysics()->GetOrigin() + ( actor->EyeOffset() * offsetScale ); } primaryTargetPos = torsoPos; secondaryTargetPos = headPos; return true; } else if( entity->GetPhysics()->GetClipModel() != NULL ) { const idBounds& box = entity->GetPhysics()->GetClipModel()->GetAbsBounds(); primaryTargetPos = box.GetCenter(); secondaryTargetPos = box.GetCenter(); return true; } return false; }