Make a realistic bullets in Unity with C#

Improve precision

In this part of the tutorial you will learn how to improve the accuracy of the trajectory line by using another integration method. You will also learn how to build your own physics engine with these integration methods so we can improve the accuracy of the bullets.

Part 1. Different integration methods

To improve the accuracy you need another better integration method. The first one we are going to add is Euler Forward, which is very similar to Backward Euler. The main difference is that it is much easier to add external forces that might affect the bullet if you are using Euler Forward because of how the methods work. If you are using Backward Euler you have to figure out the forces in the next time step and not this time step, which may be more complicated. If you are using Euler Forward you can use the current values. The drawback is that Euler Forward is not matching Unity's physics engine. Anyway, add the method EulerForward() to our collection of integration methods. Then change which of the integration method you are using in the TutorialBallistics script and observe the result.

//Euler's method - one iteration
//Will not match Unity's physics engine
public static void EulerForward(
	float h,
	Vector3 currentPosition,
	Vector3 currentVelocity,
	out Vector3 newPosition,
	out Vector3 newVelocity)
{
	//Init acceleration
	//Gravity
	Vector3 acceleartionFactor = Physics.gravity;
	//acceleartionFactor += CalculateDrag(currentVelocity);


	//Init velocity
	//Current velocity
	Vector3 velocityFactor = currentVelocity;
	//Wind velocity
	//velocityFactor += new Vector3(2f, 0f, 3f);


	//
	//Main algorithm
	//
	newPosition = currentPosition + h * velocityFactor;

	newVelocity = currentVelocity + h * acceleartionFactor;
}

You will notice that Euler Forward is overshooting the target in a similar way as Backward Euler was undershooting the target.

Bullet accuracy Euler Forward

So we need a better integration method. Another integration method is Heun's method, which is using Euler Forward but is adding another step. It looks like this:

//Heun's method - one iteration
//Will give a better result than Euler forward, but will not match Unity's physics engine
//so the bullets also have to use Heuns method
public static void Heuns(
	float h,
	Vector3 currentPosition,
	Vector3 currentVelocity,
	out Vector3 newPosition,
	out Vector3 newVelocity)
{
	//Init acceleration
	//Gravity
	Vector3 acceleartionFactorEuler = Physics.gravity;
	Vector3 acceleartionFactorHeun = Physics.gravity;


	//Init velocity
	//Current velocity
	Vector3 velocityFactor = currentVelocity;
	//Wind velocity
	//velocityFactor += new Vector3(2f, 0f, 3f);


	//
	//Main algorithm
	//
	//Euler forward
	Vector3 pos_E = currentPosition + h * velocityFactor;

	//acceleartionFactorEuler += CalculateDrag(currentVelocity);

	Vector3 vel_E = currentVelocity + h * acceleartionFactorEuler;


	//Heuns method
	Vector3 pos_H = currentPosition + h * 0.5f * (velocityFactor + vel_E);

	//acceleartionFactorHeun += CalculateDrag(vel_E);

	Vector3 vel_H = currentVelocity + h * 0.5f * (acceleartionFactorEuler + acceleartionFactorHeun);


	newPosition = pos_H;
	newVelocity = vel_H;
}

If you now press play you will see something beautiful. The trajectory line goes straight through the target point we are aiming at.


Bullet accuracy Heuns Method

Heun's method is actually a simplified version of Runge Kutta's method, which is another method you could add to the collection. I've tested to add it and it gives the same result as Heun's method in this environment. Another integration method used when simulating bullets is Adams-Bashforth, but I haven't tested it because Heun's method is fast and accurate.

Part 2. Improving the bullets

But a sniper or an artillery officer will not care if the simulated trajectory line goes through the target. To improve the bullets we are going to use Heun's method in a similar way as when we drew the trajectory line. The difference is that we can't change the time step h. If you've ever wondered why you have to place everything in Unity that's using physics in FixedUpdate() and not in Update(), the reason is this time step. It can't be too small nor too large and should be constant. That's why we are using Time.fixedDeltaTime as the time step h.

To improve the bullets you have to remove the RigidBody from it and replace it with a script called TutorialBullet. Also give the target a Tag called "Target" so we can detect if a bullet hit the target. To do that we fire a raycast every update from the last position to the new position as calculated by an integration method, and see if we have hit something. We also destroy the bullet if it falls below a certain level.

using UnityEngine;
using System.Collections;

public class TutorialBullet : MonoBehaviour
{    
    public Vector3 currentPosition;
    public Vector3 currentVelocity;

    Vector3 newPosition = Vector3.zero;
    Vector3 newVelocity = Vector3.zero;

    void Awake()
    {
        currentPosition = transform.position;
    }

    void Update()
    {
        DestroyBullet();
    }

    void FixedUpdate()
    {
        MoveBullet();
    }

    //Did we hit a target
    void CheckHit() 
    {
        Vector3 fireDirection = (newPosition - currentPosition).normalized;
        float fireDistance = Vector3.Distance(newPosition, currentPosition);

        RaycastHit hit;

        if (Physics.Raycast(currentPosition, fireDirection, out hit, fireDistance))
        {
            if (hit.collider.CompareTag("Target"))
            {
                Debug.Log("Hit target!");
                //Destroy(gameObject);
            }
        }
    }

    void MoveBullet()
    {
        //Use an integration method to calculate the new position of the bullet
        float h = Time.fixedDeltaTime;
        TutorialBallistics.CurrentIntegrationMethod(h, currentPosition, currentVelocity, out newPosition, out newVelocity);

        //First we need these coordinates to check if we have hit something
        CheckHit();

        currentPosition = newPosition;
        currentVelocity = newVelocity;

        //Add the new position to the bullet
        transform.position = currentPosition;
    }

    void DestroyBullet()
    {
        if (transform.position.y < -30f)
        {
            Destroy(gameObject);
        }
    }
}

You also need to change one line in the TutorialFireBullets script. Because we have removed the RigidBody from the bullet and replaced it with our own integration method, we need to add the velocity the bullet has at time zero.

public IEnumerator FireBullet() 
{        
	while (true)
	{
		//Create a new bullet
		GameObject newBullet = Instantiate(bulletObj, transform.position, transform.rotation) as GameObject;

		//Parent it to get a less messy workspace
		newBullet.transform.parent = bulletParent;

		//Add velocity to the non-physics bullet
		newBullet.GetComponent<TutorialBullet>().currentVelocity = TutorialBallistics.bulletSpeed * transform.forward;

		//Add velocity to the bullet with a rigidbody
		//newBullet.GetComponent<Rigidbody>().velocity = TutorialBallistics.bulletSpeed * transform.forward;

		yield return new WaitForSeconds(2f);
	}
}

If you now press play you will see that the bullets are also hitting the center of the target. Success!

Bullet accuracy with Heuns Method