Search Unity

Resolved First-Person Shooter Aim Assist by "Snapping" the Crosshair on an Object

Discussion in 'Scripting' started by notakamihe, Oct 16, 2020.

  1. notakamihe

    notakamihe

    Joined:
    Jan 14, 2020
    Posts:
    7
    I have an issue figuring out how to implement an certain method of assisting aim into my game. The goal is to "snap" the crosshair to an enemy if the enemy is close enough to the cursor. A close enough example of what I mean is with GTA's assisted/auto aim, where if you even somewhat-aim your firearm at someone, the cursor attracts itself exactly to that person. The method that got me the closest to what I wanted was spherecasting, but I am open to other methods and explanations as well. Below is my current method from a firearm script:

    Code (CSharp):
    1.    
    2.  
    3. Vector3 lookingAt;  
    4.  
    5.  if (Physics.SphereCast(ray, 0.25f, out RaycastHit hit, accuracyRange, ~LayerMask.GetMask("Boundary"),
    6.      QueryTriggerInteraction.Ignore) && hit.collider.GetComponentInParent<PlayerMovement>() == null)
    7.         //ray is the cursor's screen position raycasted forward into 3d space
    8. {
    9.     Vector3 center = ray.origin + (ray.direction * hit.distance); //acts as cursor but translated hit.distance units forward
    10.     Enemy enemy = (Enemy)hit.collider.GetComponentInParent(typeof(Enemy));
    11.  
    12.     if (enemy != null)
    13.     {
    14.         player.camera.transform.rotation = Quaternion.Lerp(
    15.             player.camera.transform.rotation,
    16.             Quaternion.LookRotation(enemy.transform.position, center),
    17.             1f * Time.deltaTime
    18.         );
    19.     }
    20.              
    21.     lookingAt = hit.point;
    22. }

    This method has a few issues: 1) You can't really feel like you're snapping to anything because you have to aim almost exactly at a target for it to even have a chance at working. 2) When I increase the radius to resolve issue 1 so that a target can be hit more easily, I end up with the exact opposite result, meaning that hits seem to be detected less by the SphereCast. 3) The bigger the radius, the more frequent it is for shots to go in completely different and random directions as if they were deflected.
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
    I have never made one of these so I figured I'd try my hand at it. This is what I came up with for the actual auto-aiming engine.

    The methodology is discussed in the source comments.

    In my opinion it has a nice "magnetic" feel to it when the crosshairs go over a visible enemy.

    Sorry I don't have it packaged up in something public but I'll see if I get some time this weekend to stick it in one of my public projects.

    But meanwhile, it's all laid out for you to enjoy here:

    This is the engine script. It has a factory function to help you set it up properly at runtime. It is NOT intended to just be drag-dropped in the editor.

    Code (csharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. // @kurtdekker - cheap and cheerful super-generic FPS aim assister
    6.  
    7. public class AutoAimAssist1 : MonoBehaviour
    8. {
    9.    System.Func<Transform,IEnumerable<Vector3>> GetVisibleTargetLocations;
    10.    IAimAssistable AimAssistable;
    11.    float MaxVectorOffset;
    12.  
    13.    // Factory method to attach to your stuff. Set this up with:
    14.    //
    15.    //    - the viewpoint you are aiming from (Transform). This script is placed there too.
    16.    //    - your mouse look script, which must implement IAimAssistable interface.
    17.    //    - a function this script can call to say "enumerate me some enemies I can see and possibly target"
    18.    //    - the maximum normalized vector offset (the sine of maximum off-boresight)
    19.    //
    20.    public static AutoAimAssist1 Attach(
    21.        Transform viewpoint,
    22.        System.Func<Transform,IEnumerable<Vector3>> GetVisibleTargetLocations,
    23.        IAimAssistable aimAssistable,
    24.        float maxVectorOffset)
    25.    {
    26.        var aaa = viewpoint.gameObject.AddComponent<AutoAimAssist1>();
    27.  
    28.        aaa.GetVisibleTargetLocations = GetVisibleTargetLocations;
    29.        aaa.AimAssistable = aimAssistable;
    30.        aaa.MaxVectorOffset = maxVectorOffset;
    31.  
    32.        return aaa;
    33.    }
    34.  
    35.    void Update ()
    36.    {
    37.        var TargetLocations = GetVisibleTargetLocations( transform);
    38.  
    39.        var MyPosition = transform.position;
    40.        var MyForward = transform.forward;
    41.  
    42.        foreach( var targetLocation in TargetLocations)
    43.        {
    44.            var VectorToEnemy = targetLocation - MyPosition;
    45.  
    46.            VectorToEnemy.Normalize();
    47.  
    48.            // essentially the tangent vector between MyForward and the line to the enemy
    49.            var difference = VectorToEnemy - MyForward;
    50.  
    51.            // how big is that offset along the sphere surface
    52.            float vectorOffset = difference.magnitude;
    53.  
    54.            // if it is within our auto-aim MaxVectorOffset, we care
    55.            if (vectorOffset < MaxVectorOffset)
    56.            {
    57.                // transform it to local offset X,Y plane
    58.                var localDifference = transform.InverseTransformDirection( difference);
    59.  
    60.                // normalize it to full deflection
    61.                localDifference /= MaxVectorOffset;
    62.  
    63.                // scale it according to conical offset from boresight (strongest in middle)
    64.                float conicalStrength = (MaxVectorOffset - vectorOffset) / MaxVectorOffset;
    65.                localDifference *= conicalStrength;
    66.  
    67.                // send it to the aim assist injection point
    68.                AimAssistable.InjectAutoAim( localDifference);
    69.            }
    70.        }
    71.    }
    72. }
    It knows how to call this type of an
    IAimAssistable
    object:

    Code (csharp):
    1. using UnityEngine;
    2.  
    3. // implement this in your main mouse look controller to receive
    4. // steering input from the AutoAimAssist script(s)
    5.  
    6. public interface IAimAssistable
    7. {
    8.     void InjectAutoAim( Vector2 aim);  
    9. }
    In your mouse look script, after all your other calculations are done, you would blend the Vector2 you received via InjectAutoAim() method, something like:

    Code (csharp):
    1.         _mouseAbsolute += LastAimAssist;
    2.         LastAimAssist = Vector2.zero;  // mark it as "processed"
    You would choose scaling values to the above vector, since this is extremely dependent on your mouse movement implementation.

    The auto-aimer also requires a function like this to retrieve visible enemies. You would provide this in your own enemy manager class:

    Code (csharp):
    1.     IEnumerable<Vector3> GetVisibleTargetLocations( Transform viewpoint)
    2.     {
    3.         foreach( var e in Enemies)
    4.         {
    5.             if (e)
    6.             {
    7.                 // TODO: include LOS by considering if viewpoint can even
    8.                 // see this enemy, perhaps with raycast or other method.
    9.  
    10.                 yield return e.transform.position;
    11.             }
    12.         }
    13.     }
     
  3. notakamihe

    notakamihe

    Joined:
    Jan 14, 2020
    Posts:
    7

    Thanks for the reply, Kurt-Dekker and forgive me for the late response. I've taken your most of your code and merged it into my existing code to the best of my ability. There's just one part of your answer I don't understand. The variables (or placeholders) "_mousePosition" and "LastAimAssist" in your second-to-last code section is a bit ambiguous, that is, I don't know what they refer to. Also, is the InjectAutoAim function where the player actually looks at the target?
     
  4. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
    They were the variables in my mouselook code. _mousePosition was the last calculated intended mouse position, and LastAimAssist was a "mailbox" variable (in the mouselook script) where the InjectAutoAim() method (which implements the IAimAssistable interface, which your mouselook must implement) sticks the latest correct.

    Think of LastAimAssist as "the nudge that the auto aim wants to give to your cursor this frame."

    The InjectAutoAim() is the one method your mouselook script must implement as part of implementing the IAimAssistable interface, which it also must implement.

    Here's my mouselook script... I got it off the web at the link indicated, but I probably changed it over the years.

    See how it implements the IAimAssistable interface by providing the InjectAutioAim() method down at the bottom?

    Code (csharp):
    1. using UnityEngine;
    2.  
    3. // 5:18 PM 11/15/2017
    4. //
    5. // From: https://forum.unity.com/threads/a-free-simple-smooth-mouselook.73117/
    6.  
    7. public class SimpleSmoothMouseLook : MonoBehaviour, IAimAssistable
    8. {
    9.     Vector2 _mouseAbsolute;
    10.     Vector2 _smoothMouse;
    11.  
    12.     public Vector2 clampInDegrees = new Vector2(360, 180);
    13.     public bool lockCursor;
    14.     public Vector2 sensitivity = new Vector2(2, 2);
    15.     public Vector2 smoothing = new Vector2(3, 3);
    16.     public Vector2 targetDirection;
    17.     public Vector2 targetCharacterDirection;
    18.  
    19.     // Assign this if there's a parent object controlling motion, such as a Character Controller.
    20.     // Yaw rotation will affect this object instead of the camera if set.
    21.     public GameObject characterBody;
    22.  
    23.     void Start()
    24.     {
    25.         // Set target direction to the camera's initial orientation.
    26.         targetDirection = transform.localRotation.eulerAngles;
    27.  
    28.         // Set target direction for the character body to its inital state.
    29.         if (characterBody)
    30.         {
    31.             targetCharacterDirection = characterBody.transform.localRotation.eulerAngles;
    32.         }
    33.  
    34.         first = true;
    35.     }
    36.  
    37.     bool first;
    38.  
    39.     void Update()
    40.     {
    41.         // Ensure the cursor is always locked when set
    42.         if (lockCursor)
    43.         {
    44.             Cursor.lockState = CursorLockMode.Locked;
    45.         }
    46.  
    47.         // Allow the script to clamp based on a desired target value.
    48.         var targetOrientation = Quaternion.Euler(targetDirection);
    49.         var targetCharacterOrientation = Quaternion.Euler(targetCharacterDirection);
    50.  
    51.         // Get raw mouse input for a cleaner reading on more sensitive mice.
    52.         var mouseDelta = new Vector2(Input.GetAxisRaw("Mouse X"), Input.GetAxisRaw("Mouse Y"));
    53.  
    54.         // Scale input against the sensitivity setting and multiply that against the smoothing value.
    55.         mouseDelta = Vector2.Scale(mouseDelta, new Vector2(sensitivity.x * smoothing.x, sensitivity.y * smoothing.y));
    56.  
    57.         if (first)
    58.         {
    59.             first = false;
    60.             _smoothMouse = mouseDelta;
    61.         }
    62.  
    63.         // Interpolate mouse movement over time to apply smoothing delta.
    64.         _smoothMouse.x = Mathf.Lerp(_smoothMouse.x, mouseDelta.x, 1f / smoothing.x);
    65.         _smoothMouse.y = Mathf.Lerp(_smoothMouse.y, mouseDelta.y, 1f / smoothing.y);
    66.  
    67.         // Find the absolute mouse movement value from point zero.
    68.         _mouseAbsolute += _smoothMouse;
    69.  
    70.         // Clamp and apply the local x value first, so as not to be affected by world transforms.
    71.         if (clampInDegrees.x < 360)
    72.         {
    73.             _mouseAbsolute.x = Mathf.Clamp(_mouseAbsolute.x, -clampInDegrees.x * 0.5f, clampInDegrees.x * 0.5f);
    74.         }
    75.  
    76.         // Then clamp and apply the global y value.
    77.         if (clampInDegrees.y < 360)
    78.         {
    79.             _mouseAbsolute.y = Mathf.Clamp(_mouseAbsolute.y, -clampInDegrees.y * 0.5f, clampInDegrees.y * 0.5f);
    80.         }
    81.  
    82.  
    83.         // 6:41 AM 8/21/2018 - hack in the right joystick sticks
    84.         const string JoystickLookX = "Joy3";
    85.         const string JoystickLookY = "Joy4";
    86.         Vector2 joystickRaw = new Vector2(
    87.             Input.GetAxis( JoystickLookX),
    88.             Input.GetAxis( JoystickLookY)) * Time.deltaTime;
    89.         joystickRaw.x *= 300;
    90.         joystickRaw.y *= 150;
    91.  
    92.         // invert!
    93.         joystickRaw.y *= -1;
    94.  
    95.         _mouseAbsolute += joystickRaw;
    96.  
    97.  
    98.  
    99.         _mouseAbsolute += LastAimAssist;
    100.         LastAimAssist = Vector2.zero;
    101.  
    102.  
    103.  
    104.         transform.localRotation = Quaternion.AngleAxis(-_mouseAbsolute.y, targetOrientation * Vector3.right) * targetOrientation;
    105.  
    106.         // If there's a character body that acts as a parent to the camera
    107.         if (characterBody)
    108.         {
    109.             var yRotation = Quaternion.AngleAxis(_mouseAbsolute.x, Vector3.up);
    110.             characterBody.transform.localRotation = yRotation * targetCharacterOrientation;
    111.         }
    112.         else
    113.         {
    114.             var yRotation = Quaternion.AngleAxis(_mouseAbsolute.x, transform.InverseTransformDirection(Vector3.up));
    115.             transform.localRotation *= yRotation;
    116.         }
    117.     }
    118.  
    119.  
    120. // IAimAssistable area:
    121.     Vector2 LastAimAssist;
    122.  
    123.     const float AutoAimLateralStrength = 1.0f;
    124.     const float AutoAimVerticalStrength = 1.0f;
    125.  
    126.     public void InjectAutoAim (UnityEngine.Vector2 aim)
    127.     {
    128.         var x = aim.x;
    129.         var y = aim.y;
    130.  
    131.         x *= AutoAimLateralStrength;
    132.         y *= AutoAimVerticalStrength;
    133.  
    134.         LastAimAssist = new Vector2( x, y);
    135.     }
    136. }
     
  5. notakamihe

    notakamihe

    Joined:
    Jan 14, 2020
    Posts:
    7
    After hours of trial and error, I've finally developed a solution of sort in my Firearm script. Check it out:

    Code (CSharp):
    1. Enemy targetEnemy; //Stores the current enemy the aim assist script is targeting
    2. MouseLook camera = player.camera.GetComponent<MouseLook>();  //holds a variable of class MouseLook, which handles how the player can look around
    3. Ray ray = player.camera.ScreenPointToRay(player.cursor.position);  //Raycasts from the cursor on the player's camera forward
    4.  
    5.  
    6. if (GamePreferences.AimAssist)
    7. {
    8.     foreach (Enemy enemy in GameSingleton.instance.allEnemies)
    9.     {
    10.         if (Vector.Distance(player.transform.position, enemy.transform.position) <= accuracyRange). //Limits the range of enemies to only those who are near enough
    11.         {
    12.             if (Vector3.Angle(camera.transform.forward, enemy.transform.position - player.transform.position) <= 25f) //If the enemy is essentially in front of the player…
    13.             {
    14.                 //This is basically the enemy’s position shifted upward so that, in this case, the neck is targeted
    15.                 Vector3 enemyPoint = enemy.transform.position + enemy.transform.up * 2.88f;
    16.  
    17.                 /*
    18.                     Gets one point from a set of points equidistant from the player/camera. Because the distance is based on the distance between the player
    19.                     and the enemy, the closer the cursor is to an enemy, the more likely it is for the enemy to be “in range” of the point.
    20.                 */
    21.                 Vector3 aimAssistPoint = ray.GetPoint(Vector3.Distance(enemyPoint, camera.transform.position));
    22.  
    23.                 //If the enemy is within 1.5 units from the point the player is looking at. In other words, if the cursor is close enough to the enemy…
    24.                 if (Vector3.Distance(aimAssistPoint, enemyPoint) <= 1.5f).
    25.                 {
    26.                     if (targetEnemy == null)
    27.                     {
    28.                         //Fix cursor on enemy for one second
    29.                         StartCoroutine(ToggleMouseLook(1f, camera, enemy);  //See: ToggleMouseLook
    30.                         camera.transform.LookAt(enemyPoint);
    31.                     }
    32.                 }
    33.             }
    34.             else
    35.             {
    36.                 //Once the player looks away from the target enemy or the target enemy is dead, reset the targetEnemy var to null so that an enemy can be targeted again
    37.                 if (enemy == targetEnemy || (targetEnemy && targetEnemy.IsDead())
    38.                 {
    39.                     targetEnemy = null;
    40.                 }
    41.             }
    42.         }
    43.     }
    44. }
    And the ToggleMouseLook coroutine that actually restricts the cursor to an enemy....

    Code (CSharp):
    1. IEnumerator ToggleMouseLook (float delay, MouseLook mouseLook, Enemy enemy)
    2. {
    3.     //Disables the player’s ability to look for delay seconds so that the targeting is not disrupted
    4.     mouseLook.enabled = false;
    5.     yield return new WaitForSeconds(delay);
    6.     mouseLook.enable = true;
    7.  
    8.     targetEnemy = enemy;  //Assigns the enemy passed in to the targetEnemy var to end targeting
    9.  
    10. }
    It's not perfect because there are still some inconsistencies I need to correct, but it good enough. If you'd like: you can test this script out for yourself and reply to me with the solution improved.
     
    Rujash likes this.