Targeting System in unreleased game

Intended Audience

This tutorial is meant for people who have some experience with C#/C++ programming, and it is a plus if you know Unity but this is not really necessary. There are a few concepts that are intermediary however I believe any beginner programmer/scripter can go through and understand the main concept and understand the ins and outs of a targeting system and where it can fall short. This is NOT an out of the box works for you implementation. If I wanted to do that, I would have to write a tutorial on how to do a camera system and movement system. This will work if you have a generic camera, and movement system with the movement system modified to handling the movement of the player when targeting.

Use Cases For a Targeting System

Most modern games use some form of targeting, may it be to get the player’s attention towards something, targeting enemies within a combat styled game, or just for small hacks such as auto-aiming.

Games that use targeting as a core mechanic: Uncharted, Dark Souls, Sekiro, and Legend of Zelda: Breath of the Wild.

Goals For a Targeting System

  1. Lock onto a target based off of an enemy’s position relative to the center of the camera
  2. Do not lock onto enemy behind you
  3. Can support any number of enemies
  4. Cycle through multiple enemies (left or right)

How To Detect Candidate Targets

First on your player, add an empty script component called TargetingSystem.

Second, create a sphere around the player. It will have a Sphere Collider and should be marked as Is Trigger.

Targeting Sphere on Player

The green box shows the sphere collider on the player and is checked as Is Trigger and the yellow box shows the currently empty script. Disregard the name for the script and call yours Targeting System.

In a targeting system, the first thing you want to know is what is a target and how many targets are within your vicinity. To mark an object as a target, create a capsule collider within the vicinity of the player tagged as “targetable.” To know how many targets are within your vicinity create a local int variable called “NumberOfTargetsWithinRange”. To keep track of all the candidate targets create a list of GameObjects that is a member variable that updates whenever you add/remove a candidate target from the player vicinity.

Since we added the sphere collider on the player, this will indicate to us the range at which enemy is a target from the player. Unity has built in OnTriggerEnter and OnTriggerExit functions. Let’s use those to mark how many enemies are within the player’s zone of targeting and when an enemy leaves the range of targeting.

  private void OnTriggerEnter(Collider other)
  {
    if (other.tag == "Targetable") //if the gameobject is a targetable object
    {
       //add the targetable object in a list of candidateTargets
      m_CandidateTargets.Add(other.gameObject);
      
       //increment the number of targets within range
      ++NumberOfTargetsWithinRanger;
    }
  }

  private void OnTriggerExit(Collider other)
  {
    if (other.tag == "Targetable") //if the gameobject is a targetable object
    {
      //remove the targetable object in a list of candidateTargets
      m_CandidateTargets.Remove(other.gameObject); 
      
      //decrement the number of targets within range
      --NumberOfTargetsWithinRange;
    }
  }

How To Mark an Object as Targeted

Create a GameObject that will be the target you will always look at as a member variable of the script and call it m_ObjectClosestToCamera. Now let’s set a button to target a player. To make a button toggle-able you need to create a bool that is a member variable, and set/unset it depending on the context. I will call mine m_TargetButton.

In the update loop, I’ll use F on the keyboard to capture a player’s input when they press the targeting button. If the m_ObjectClosestToCamera is null make sure to set the target button as false because you should not target a null object. If there are any null objects within the m_CandidateTargets list, remove them from the list. Now you may be wondering, when will an object be null. Well let me tell you, if the player kills an enemy they are targeting, that object is now null.

    //check the targeting button has been pressed
    bool isDown = Input.GetKeyDown(KeyCode.F);

    //if candidate object happens to be null reset targeting
    if (m_ObjectClosestToCamera == null)
    {
      m_TargetButton = false;
    }

    //remove null objects in the list and decrement the counter
    //optimize through some onDelete event system, probably really not worth
    for (int i = m_CandidateTargets.Count - 1; i >= 0; --i)
    {
      if (m_CandidateTargets[i] == null || !m_CandidateTargets[i].activeInHierarchy)
      {
        m_CandidateTargets.RemoveAt(i);
        NumberOfTargetsWithinRange--;
      }
    }

Now what happen’s if you you press the target button and there are targets within your range, what should you do?

First you want to sort the list of objects within the m_CandidateList by angle from the center of the camera versus the enemy location. How do you go about doing that? Math.

Sorting By Angle Math

Here is a diagram I made to explain the logic of sorting objects based off of the angle.

Variable Declarations For Diagram:

  • m_Angle (Member Variable Ignore for Now)
  • Camera Position (C1)
  • Target 1 Position (T1)
  • Target 2 Position (T2)
  • Target 3 Position (T3)

The first thing we want to know is the angle between the Camera Z Direction Vector and the Direction from the Camera Position and Target 1 Position. To do that, we can get the angle between two vectors by the math from the diagram above and we want to get the absolute angle. To get the first vector, it is given to us because we know the direction in which the camera is always facing. In Unity, this variable is given by Camera.main.transform.forward. The second vector is direction from the camera to the target, and to get a specific direction from a point you must minus the point from another point. In this case, the target’s position – camera’s position. In Unity, there is a function called Angle(Vector2 u, Vector2 v); which returns the absolute angle, exactly what we need. We can use a C# lambda to sort objects from the candidate list into a new sorted list. We use OrderBy to sort via ascending order.

Now to show this in Unity:

 if (isDown && NumberOfTargetsWithinRange > 0)
    {
      //sorts objects by angle and stores it into the Sorted_List
      List<GameObject> SortedCandidateList = m_CandidateTargets.OrderBy(gameObjects =>
      {
        //get vector from camera to target
        Vector3 target_direction = gameObjects.transform.position - Camera.main.transform.position; 

        //convert camera forward direction into 2D vector
        var camera_forward = new Vector2(Camera.main.transform.forward.x, Camera.main.transform.forward.z); 

         //convert target_direction into 2D Vector
        var target_dir = new Vector2(target_direction.x, target_direction.z);

        //get the angle between the two vectors 
        float angle = Vector2.Angle(camera_forward, target_dir); 

        return angle;
      }).ToList(); //store the objects based off of the angle into the sorted List

Why did I convert the vectors to 2D? In this case I wanted the math to be in 2D because that is how I derived it on paper and because I did not want height to affect the sorting of angles.

Now we have two lists. The m_CandidateTargets list which contains all potential targets, and now a sorted potential target list by angle called SortedCandidateList. Let us now overwrite the m_CandidateTargets with the SortedCandidateList.

      //copy objects into the main game_object list
      //remove objects that happen to die before selecting next target
      for (var i = 0; i < m_CandidateTargets.Count(); ++i)
      {
        //overwrite the candidate list with the sorted list
        m_CandidateTargets[i] = SortedCandidateList[i];

        if (!m_CandidateTargets[i].activeInHierarchy)
        {
          m_CandidateTargets.RemoveAt(i);
          NumberOfTargetsWithinRange--;
        }
      }

Now finally, after all that we can decide on who the first target is since our main m_CandidateTargets list is sorted by angle, the object with the least angle or in other words, the first object in the m_CandidateTargets list is the object that is closest to the center of the camera meaning the object that the user is looking at.

  //Super cool thing to note,  "float angle = Vector2.Angle(camera_forward, target_dir);" sorts by abs(angle) aka unsigned so the first object you target is always the object you are most looking at
  m_ObjectClosestToCamera = m_CandidateTargets.First(); //set target as the target you are most looking at

  m_TargetButton = !m_TargetButton; //this handles the unlocking / locking
} //this bracket is from all they way above for the closing bracket of the if statement  if (isDown && NumberOfTargetsWithinRange > 0)

Now at this point in the code, m_ObjectClosestToCamera is the most prime candidate. The only problem with this is, what if it is an object outside our field of view? Meaning, what if the the target is behind us. So the last thing we need to do is check if the object’s angle is less than a defined angle set by the designer. To expose this functionality, let’s create a member variable called m_Angle. If the angle we get from the m_ObjectClosestToCamera is greater than our set m_Angle value, then make sure to not target this object whatsoever, however if it is under we want to mark this spot for targeting as true meaning I am in the targeting state so create a member variable bool called m_IsTargeting and set it to false initially.

  //if I am targeting, there are candidate objects within my radius
    if (m_TargetButton && m_Counter > 0 && m_ObjectClosestToCamera != null)
    {
      //gets the angle of the between the current game object and camera
      Vector3 target_direction = m_ObjectClosestToCamera.transform.position - Camera.main.transform.position;

      var camera_forward = new Vector2(Camera.main.transform.forward.x, Camera.main.transform.forward.z);

      var target_dir = new Vector2(target_direction.x, target_direction.z);

      float angle = Vector2.Angle(camera_forward, target_dir);

      //check if the object's angle is within the zone of targeting
      if (angle < Mathf.Abs(m_Angle))
      {
        m_IsTargeting = true; //set targeting to true
      } 
      else
      {
        //reset the target button if there is no valid target
        m_TargetButton = false; 
      }
    }
    else
    {
      //set targeting to false since one of the original if conditions was false
      m_IsTargeting = false; 

      if (m_ObjectClosestToCamera != null) // if object wasn't null
      {
        m_ObjectClosestToCamera = null; //set it to null because conditions failed
      }
    }

Finally after all that, the object is within the camera view and is a candidate object that is now targeted. Goals 1 and 2 are now accomplished at this point. Hurray! At this point we are able to target the object most centered to the camera. The next step from here would be to cycle targeting when there are multiple targets all within range of targeting.

How to Cycle Targets That Are to The Left or to The Right

To start this process, we must first have input for when to go to the target that is left of the current target, and input for the right.

Now where do we start writing this piece of code? Let’s look at the conditions of cycling to determine that.

Conditions For Cycling:

  1. Currently be targeting an object
  2. Input that leads to the target
  3. There must be a target to cycle to
  4. Cycling targets must be treated as togglables (You will know why shortly)
  5. Target that can be cycled to must be within range of m_Angle

The first condition gives away the answer to where we should start writing this code. It should be right after when we set the targeting bool to true. The second condition is what the first line of code will be. Input for cycling. I’ll choose the right arrow key to cycle to enemies that are to the right of the current target and the left arrow key to cycle to enemies that are to the left of the current target.

 //check if the object's angle is within the zone of targeting
      if (angle < Mathf.Abs(m_Angle))
      {
        m_IsTargeting = true; //set targeting to true
        
        //if the right stick was moved to the right or the right arrow key was pressed
        if (Input.GetAxisRaw("Controller Right Stick X") > 0.0f || Input.GetKeyDown(KeyCode.RightArrow)) 
        { 
           //....

We want to treat cycling of targets as togglable because you want to be able to press the arrow keys or the right controller stick multiple times to cycle to the furthest target in range and one time to only go to next target in range. To do this, we need to make two more bools called m_AxisRight and m_AxisLeft. These bools will denote when a cycle to either side of the current target is initiated. Set these bools as false initially.

Next surprisingly enough, you want to sort the objects again when switching targets and copy them back into the m_CandidateTargets list. We want do this because we need to make sure if the targets get shuffled up while you are targeting the current target, the list is still correct. I found this problem when enemies where moving around while I was targeting the current enemy, why when I switched targets, the enemy I was looking at next was not what I expected the next target to be.

          if (m_AxisRight == false) //if axis initally was false
          {
            //sort objects in the list while targeting, yes you want to do this so if enemies move  around it still keeps the list correct and sorts it for appropriate target switching
            List<GameObject> Sorted_List = m_CandidateTargets.OrderBy(target =>
            {
              Vector3 targeDir = target.transform.position - Camera.main.transform.position;

              var cameraFor = new Vector2(Camera.main.transform.forward.x, Camera.main.transform.forward.z);

              var newtarget_dir = new Vector2(targeDir.x, targeDir.z);

              /********NOTICE THE SIGNED ANGLE FUNCTION***********/
              float new_angle = Vector2.SignedAngle(cameraFor, newtarget_dir);

              return new_angle ;
            }).ToList();

            //copy the objects from the sorted list into the main list
            for (var i = 0; i < m_CandidateTargets.Count(); ++i)
            {
              m_CandidateTargets[i] = Sorted_List[i];
            }

Why is the SignedAngle function so important here. This function does not mean what you exactly think it means and here is why.

To the right or going clockwise means you are approaching -180 degrees however going to the left means you are approaching 180 degrees. So when you sort this list for the image above, the list might look like this:

  1. Target 2
  2. current Target
  3. Target 1

So why does that matter? It means, to look at objects to the right, you must start from the current Target and go left in the list and to look left , you must go right in the list.

    //check if there is an object to the right
    if (m_CandidateTargets.IndexOf(m_ObjectClosestToCamera) - 1 >= 0)
    {
      //check its angle
      GameObject nextTargetedObject = 
      m_CandidateTargets[m_CandidateTargets.IndexOf(m_ObjectClosestToCamera) - 1];

      Vector3 targeDir = nextTargetedObject.transform.position - Camera.main.transform.position;

      var cameraFor = new Vector2(Camera.main.transform.forward.x, Camera.main.transform.forward.z);

      var newtarget_dir = new Vector2(targeDir.x, targeDir.z);
      float anglex = Vector2.Angle(cameraFor, newtarget_dir);

      //if the angle is within range of targeting angle
      if (anglex < Mathf.Abs(m_Angle))
      {
        //set the new object as the target
        m_ObjectClosestToCamera = m_CandidateTargets[m_CandidateTargets.IndexOf(m_ObjectClosestToCamera) - 1];
      }

      //treat angle as button type
      m_AxisRight = !m_AxisRight;
    } 
  } //end of  if (m_AxisRight == false) block
} //end of if (Input.GetAxisRaw("Controller Right Stick X") > 0.0f || Input.GetKeyDown(KeyCode.RightArrow)) 

As you can see, I have to – 1 from the m_ObjectClosestToCamera in the candidate target list to look one position to the right because of the ordering supplied from SignedAngle(Vector2 u, Vector2 v);

now we just do the same thing for looking to the left hand side and then we are done. We need to reset the two bools m_AxisRight and m_AxisLeft to false in the else part of this statement. Hitting this else means to reset the ability to cycle left or right. The reason we know that is because once you move the right stick or press the right stick to the side or press right arrow once, the analog stick generally goes back to the origin. This marks the reset point.

Lat but not least, just to show the state where you cycle to the left, here is the left cycle of the implementation in its entirety. A huge chunk of code is up ahead.

 else if (Input.GetAxisRaw("Controller Right Stick X") < 0.0f || Input.GetKeyDown(KeyCode.LeftArrow))
{ 
//initially the leftAxis bool should be false for togglable logic       
  if (m_AxisLeft == false)
  {       
     //sort objects by the SignedAngle  for the same reasons statef for right cycling
     List<GameObject> Sorted_List = m_CandidateTargets.OrderBy(go =>
     {
        //direction from  Camera to the target object indicated by go
        Vector3 targetDir = go.transform.position - Camera.main.transform.position;

       //convert Camera direction into 2D vector
        var cameraForward = new Vector2(Camera.main.transform.forward.x, Camera.main.transform.forward.z);
        //convert target direction into the 2D vector
        var new_target_dir = new Vector2(targetDir.x, targetDir.z);
        
        // calculate angle between the two vectors
        float angle_between_vectors = Vector2.SignedAngle(cameraForward, new_target_dir);
        return angle_between_vectors;
     }).ToList();

    //put the sorted list into the candidate list
    for (var i = 0; i < m_CandidateTargets.Count(); ++i)
    {
      m_CandidateTargets[i] = Sorted_List[i];
    }

    //notice here how I check if there is a next valid object in the list
    if (m_CandidateTargets.IndexOf(m_ObjectClosestToCamera) + 1 < m_CandidateTargets.Count)
    {
      //store this next object into a gameObject 
      GameObject nextTargetedObject = m_CandidateTargets[m_CandidateTargets.IndexOf(m_ObjectClosestToCamera) + 1];

      //calcuate the angle to see if the next object is within defined targeting range
      Vector3 targeDir = nextTargetedObject.transform.position - Camera.main.transform.position;

      var cameraFor = new Vector2(Camera.main.transform.forward.x, Camera.main.transform.forward.z);

      var newtarget_dir = new Vector2(targeDir.x, targeDir.z);
      float anglex = Vector2.Angle(cameraFor, newtarget_dir);

      //if the next targeted object is a valid target
      if (anglex < Mathf.Abs(m_Angle))
      {
        // set the m_ObjectClosestToCamera as the next object in the list
        m_ObjectClosestToCamera = m_CandidateTargets[m_CandidateTargets.IndexOf(m_ObjectClosestToCamera) + 1];
      }
      //for togglable logic
      m_AxisLeft = !m_AxisLeft;
     }
   }
}
else
{
  //once the axis is 0 it means the locking has been reset
  m_AxisRight = false;
  m_AxisLeft = false;
}

Now at this point, you are able to cycle through multiple targets within your range. This is amazing! Goal 3 and 4 are now accomplished which were the system being able to support any number of enemies and being able to cycle through the list of enemies depending on controller/keyboard input.

Some Flaws and Improvements That Can Be Made

Some flaws with this system are if two objects are exactly behind each other from your point of view and both targets are within he radius, the target will go to the object behind as well although your intention might have been something closer to your screen.

Improvements:

  • If I were to go back and improve this system, I would add a weighing system and dependent on the distance and the angle, I would give objects a priority.
  • I would also do a raycast from the camera to the target to make sure the player and target are both visible to each other
  • If the target breaks out of vision, after 1 second unlock the camera from targeting.

Conclusion

If you stuck through and read all of this, I applaud you! This might have been a dry read for some people but I hope this tutorial helped you. A full code dump is here https://amirazmi.net/targeting-system-in-unity/ .

This was indepth overview on a 3rd Person Targeting System which is highly scalable and is used in many AAA games. Being able to demonstrate it in Unity via code is amazing and hopefully this tutorial has helped on your journey of creating a targeting system. This was a beginner guide on a targeting system, if you have ways of optimizing or just have a completely different system that accomplishes the same goals, please tell me about it! I want to know a lot more!

If you have any concerns/comments please feel free to email me at amirazmi0830@gmail.com ! I will reply to you as soon as possible.