/*
===========================================================================
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 "../idlib/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;
}