Intended Audience

This tutorial is meant for people who are familiar to C#/C++ and who need a cool neat trick in their arsenal. This can be used for long ranged boss abilities or just a way to make the player feel empowered when they dodge the “bullet tracking” attack.

Games That Use Tracking Bullets

Let’s quickly look at a few games that do this that were successful in doing this. In Dark Souls 3, Alrdich, The Devour of Gods, uses blue magic casts that follow the player after a moment. In Kingdom Hearts 2, the king phases had tracking lasers that followed the player.

What I Call Handler and Projectile Relationship

This may be a programming paradigm, or it may not be but I have found what I believe to be a decent way of handling any spawned object which I call Handler and Projectile Relationship. The idea is you have a Handler for the spawned objects. Let’s start by creating the Handler. The handler is responsible for spawning objects from a specific location and a specific amount of objects and that is its only purpose.

Handler Setup

public class BulletTrackingHandler: MonoBehaviour
{
  //object to be spawned multiple times
  [SerializeField] private GameObject m_FireCastProjectile;
  //number of projectiles to be spawned
  [SerializeField] private int m_NumberOfBullets = 10;

  void Update()
  {
    //testing handler function with a keycode press
    if (Input.GetKeyDown(KeyCode.H) == true)
    {
      //calling the spawning function
      CastRangeBurst(m_NumberOfBullets);
    }
  }

 //function to spaw the projectiles
 void CastRangeBurst(int NumberOfBullets)
  {
    //spawn projectiles from location
    for (int i = 0; i < NumberOfBullets; ++i)
    {
      //spawn the projectile object
      //this object will contain the projectile script 
      //which handles the heavy lifting of the bullet
      GameObject firebullet_spawn = 
      Instantiate(m_FireCastProjectile, this.transform.position,  Quaternion.identity);
    }
  }
}

Projectile Setup

This is the class that hard carries everything. Recently I have been into Finite State Machines for this system. A Finite State Machine is simple to understand and we will be utilizing its capabilities. We are going to use the update loop for the projectile as a state machine. The idea is we are going to have the bullet go through phases which are states in this case. One for linearly interpolating to set positions, another for waiting in place, and lastly going towards the player.

public class BulletTrackingProjectile : MonoBehaviour
{
  //this vector is used for lerping objects to random directions within a unit circle scaled
  [SerializeField] private Vector3 m_Vec3Scale = new Vector3(4.0f, 1.0f, 4.0f);
  //speed of the bullet
  [SerializeField] private float m_BulletSpeedInDirectionOfPlayer = 2.0f;
  //timer for waiting when the projectiles reach its target position
  [SerializeField] private float m_Timer = 1.0f;
  //destroy projectile after it spawns
  [SerializeField] private float m_DestroyAfterTime = 7.5f;
  //this is the special sauce for projectiles to feel good
  [SerializeField] private float m_StopTrackingDistance = 5.0f;
  //this is a scaled unit circle
  private Vector3 random_within_circle_radius;
  //current position of projectile
  private Vector3 currentPos;
  //rigidbody for the projectile
  private Rigidbody m_Rigid;
  //lerp timer
  private float TimeTillTarget = 0.0f;
  //state checking for bullet interpolation
  private bool m_BulletLerpEnded = false;
  //state checking for the timer
  private bool m_TimerDone = false;

  // Start is called before the first frame update
  void Start()
  {
    //this is setting up the scaled unit sphere, this is the point in which the projectiles will go through
    random_within_circle_radius = 
    RandomScaledVector3(Random.insideUnitSphere, m_Vec3Scale);
    //current position of the projectile
    currentPos = this.transform.position;
    //setting up the rigidbody component for the projectile
    m_Rigid = GetComponent<Rigidbody>();
  }
  
  //Vector3 component-wise scaling 
  Vector3 RandomScaledVector3(Vector3 A, Vector3 B)
  {
    return new Vector3(A.x * B.x, A.y * B.y, A.z * B.z);
  }

  // Update is called once per frame
  void Update()
  {
    //bullet state 
    TimeTillTarget += Time.deltaTime;

    if (TimeTillTarget < 1.0f)
    {
      //lerp the bullet to the positions within the random circle
      transform.position = 
      Vector3.Lerp(currentPos, currentPos + random_within_circle_radius, TimeTillTarget);
    }
    else
    {
      //lerp state has finished
      m_BulletLerpEnded = true;
    }

    //timer state
    if (m_Timer > 0.0f)
    {
      //setting the velocity to 0 to be safe but also so the bullet doesnt fly off after the timer is up
      m_Rigid.velocity = Vector3.zero;
      //tick down the timer
      m_Timer -= Time.deltaTime;
    }
    else
    {
      //set the timer state as it is done now
      m_TimerDone = true;
    }

    //add bullet speed state
    if (m_BulletLerpEnded == true && m_TimerDone == true)
    {
      //track the players movements 
      //(the addition of the vector here is for aiming at the center of the player)
      Vector3 direction = Player.BaseInstanceObject.transform.position + 
      new Vector3(0.0f, .6f, 0.0f) - this.transform.position;

      //if the distance between the bullet and player is less than m_StopTrackingDistance
      //direction of the bullet is now its velocity
      if (Vector3.Distance(gameObject.transform.position, 
         Player.BaseInstanceObject.transform.position) < m_StopTrackingDistance)
      {
        //set direction the bullet was facing in (special sauce for making it feel good)
        direction = gameObject.GetComponent<Rigidbody>().velocity;
      }

      //set the velocity of the bullet
      Vector3 accel = (direction).normalized * m_BulletSpeedInDirectionOfPlayer;
      m_Rigid.velocity = accel;

      //destroy after X secodns after some amount of time
      Destroy(this.gameObject, m_DestroyAfterTime);
    }
  }
}

The Special Sauce, Let’s Talk Design

At first when I was writing this piece of code, I had it where the bullets always tracked the player. It was almost impossible for the player to dodge the projectiles. So I thought, how could I make it where it feels threatening without cheating the player? And that is when I figured it out. We want to make the player feel threatened enough until the very last moment. They key here is making them “feel” something at the end of the attack.

Let’s look through the states of dodging this specific attack. The first state is running away either directly left or right. This will be a neutral action for the player. It will be satisfying to dodge the attack this way but there is no risk and no reward. You are exactly in the state as were before the attack. It will not feel empowering. The second state is running away from the attack which is the worst action the player can take. You may or may not dodge the attack this way. The third way is to run through towards the cast. This comes with high risk and high reward however this is one of the most empowering feelings the player could ever feel. And this is what we want.

Programming the Special Sauce

How do we quantify design in lines of code. For each line we write, it may hold paragraph and paragraphs of design work. Putting it into cohesive words that be can be transcribed into code is what makes design hard. In this case, after a lot of trial and error, I conceived 4 magical lines of code that really gave the feeling to this attack.

  if (Vector3.Distance(gameObject.transform.position, Player.BaseInstanceObject.transform.position) < m_StopTrackingDistance)
      {
        //set direction the bullet was facing in (special sauce for making it feel good)
        direction = gameObject.GetComponent<Rigidbody>().velocity;
      }

That’s it. A small distance check was what I needed to change the feeling of the whole attack. Allowing designers to change the m_StopTrackingDistance gave the attack a whole new dynamic and was exactly what we needed.

Improvements That Can Be Made

  • Bullets pathing via Bezier Curve towards the player
  • Rotating bullets in the direction they are going
  • Having an option for bullets to go one at a time instead of all at the same time

These are a few improvements that I would personally like to add for future use. Thank you for reading this tutorial on Tracking Bullets and how to make them feel good! If you have any questions or improvements, email m at amirazmi0830@gmail.com!