Make a realistic bullets in Unity with C#

Add sniper rifle

In this part of the tutorial you will learn how to add a sniper rifle that can zoom and fire realistic bullets

Part 1. Preparations

First of all we need a sniper rifle, which in this case will consist of a scope, so go to Google and download your favorite scope image. I downloaded this. While you are googling, you should also find an image that we can use as a target. I downloaded Target for Archery.

Then drag both of the images into Unity. The target texture should be a Texture and you should add it to a material. Then create 2 cubes with scale 2. Name one Target and the other Target mirror. We will need a mirror of the target to easier see what we hit so we don't have to run to the target and back again each time we have fired a bullet. Add the target material to both cubes.

The scope image should be a Sprite (2D and UI). To add the scope image to the scene, right-click -> UI -> Image, and add the scope as the source image. I also had to scale it 3 times so it fits the entire screen. Also create a cube at the center of the scene so you have somewhere to fire from. Also create some kind of arrow from 2 cubes so we can show in which direction the wind is coming from. Also add Unity's first person controller. When you are done it should look like this:

Sniper start scene

Part 2. Create a sniper rifle

Create a script called SniperController and add the code below. The mainCamera is the camera attached to the first person controller, the sightImage is our sniper scope, the bulletObj is the bullet we are going to fire, and the targetObj and mirrorTargetObj are the targets we created. The hitMarker is a simple sphere we will add to the position where the bullet hit. So create a sphere, scale it to (0.1, 0.1, 0.1) and add it as a prefab.

We need the "using UnityStandardAssets.Characters.FirstPerson;" so we can change the sensitivity of the mouse when we have zoomed the camera (scope). So we need to access the MouseLook, which is a part of Unity's first person controller. This might be different depending on the Unity version you are using, but you will be able to complete the tutorial if you are not changing the sensitivity. It will just be more difficult to aim.

using UnityEngine;
using System.Collections;
using UnityStandardAssets.Characters.FirstPerson;

public class SniperController : MonoBehaviour 
{
    //Drags
    public Camera mainCamera;
    public GameObject sightImage;
    public GameObject bulletObj;
    public Transform targetObj;
    public Transform mirrorTargetObj;
    public GameObject hitMarker;
    
    //The bullet's initial speed
    //Sniper rifle
    public float bulletSpeed = 850f;

    //Need the initial camera FOV so we can zoom
    float initialFOV;
    //To change the zoom
    int currentZoom = 1;

    //To change sensitivity when zoomed
    MouseLook mouseLook;
    float standardSensitivity;
    float zoomSensitivity = 0.1f;

    bool canFire = true;

    public static Vector3 windSpeed = new Vector3(2f, 0f, 3f);

    void Start() 
    {
        //Lock and hide the mouse cursor 
        UnityEngine.Cursor.visible = false;
        Cursor.lockState = CursorLockMode.Locked;

        initialFOV = mainCamera.fieldOfView;

        mouseLook = GetComponent<RigidbodyFirstPersonController>().mouseLook;

        standardSensitivity = mouseLook.XSensitivity;
    }

    void Update() 
    {        
        ZoomSight();

        FireBullet();
    }
}

Now we will zoom the scope. The sniper rifle I found on Wikipedia can zoom between 3 and 12 times. So add the method ZoomSight(). The basic idea is that we are deactivating the scope image when we are not zooming, we zoom in and out with the mouse wheel, and then we add the new FOV with zoom to the camera.

 //The sniper rifle can zoom between 3 and 12 times
void ZoomSight()
{
	//Remove the scope sight if we are not zooming
	if (currentZoom == 1) 
	{
		sightImage.SetActive(false);

		//Change sensitivity
		mouseLook.XSensitivity = standardSensitivity;
		mouseLook.YSensitivity = standardSensitivity;
	}
	else 
	{
		sightImage.SetActive(true);

		//Change sensitivity
		mouseLook.XSensitivity = zoomSensitivity;
		mouseLook.YSensitivity = zoomSensitivity;
	}

	//Zoom with mouse wheel
	if (Input.GetAxis("Mouse ScrollWheel") > 0)
	{
		currentZoom += 1;
	}
	else if (Input.GetAxis("Mouse ScrollWheel") < 0)
	{
		currentZoom -= 1;
	}

	//Clamp zoom
	//Keep it between 1 and 11, then add 1 when zoom because zoom is between 3 and 12 times
	currentZoom = Mathf.Clamp(currentZoom, 1, 11);

	//No zoom
	if (currentZoom == 1)
	{
		//If the zoom is 6x, then the FOV is FOV / 6 (according to unscientific research on Internet)
		mainCamera.fieldOfView = initialFOV / (float)currentZoom;
	}
	//Zoom the sight
	else
	{
		mainCamera.fieldOfView = initialFOV / ((float)currentZoom + 1f);
	}
}

Let's fire the sniper rifle, so add the method FireBullet(). The problem I had here was that when we aim high, the mouse ends up outside of the webplayer even though we locked it in the beginning. I tried to find a solution, but found that several other people had the same problem and no good solution. It will probably work if you are in full-screen, which might be a good idea because the target will be really small when it's far away.

void FireBullet() 
{
	//Sometimes when we move the mouse is really high in the webplayer, so the mouse cursor ends up outside
	//of the webplayer so we cant fire, despite locking the cursor, so add alternative fire button
	if ((Input.GetMouseButtonDown(0) || Input.GetKeyDown(KeyCode.F)) && canFire)
	{
		//Create a new bullet
		GameObject newBullet = Instantiate(bulletObj, mainCamera.transform.position, mainCamera.transform.rotation) as GameObject;

		//Give it speed
		newBullet.GetComponent<TutorialBullet>().currentVelocity = bulletSpeed * mainCamera.transform.forward;

		canFire = false;
	}

	//Has to release the trigger to fire again
	if (Input.GetMouseButtonUp(0) || Input.GetKeyUp(KeyCode.F))
	{
		canFire = true;
	}
}

We also need to change a few lines in the TutorialBullet script. By the way, don't forget to tag the target cubes you created as "Target," and add the following lines to TutorialBullet:

if (hit.collider.CompareTag("Target"))
{
	Debug.Log("Hit target!");
	
	//Destroy the bullet
	Destroy(gameObject);

	//Add marker where we hit target
	GameObject.FindGameObjectWithTag("Player").GetComponent<SniperController>().AddMarker(hit.point);
}

While working with the bullet, you should also add a Trail Renderer component to the bullet to make it easier to see the fast bullet. It should have a time of 0.5, a start width of 0.2 and an end width of 0.05, or whatever you think looks good. Also give the Trail Renderer an unlit material or it will be pink!

You also need to add another method in SniperController. As I said before, the idea is to mirror the position where we hit the cube that's closer to the position where we fire bullets, so we don't need to spend time running to see where we hit the real target. To make this work we have to translate the coordinate where we hit to the coordinate of the cube that's mirroring the hits.

//Add marker where we hit target
//Called from the bullet script
public void AddMarker(Vector3 hitCoordinates) 
{
	//Add a marker where we hit the target
	Instantiate(hitMarker, hitCoordinates, Quaternion.identity);

	//The coordinates of the hit in localPosition of the target
	Vector3 localHitCoordinates = targetObj.InverseTransformPoint(hitCoordinates);

	//The global coordinates of the hit but in relation to the mirror target
	//The marker has the same local position in relation to both the target and the mirror
	Vector3 globalMirrorHit = mirrorTargetObj.transform.TransformPoint(localHitCoordinates);

	//Add another marker
	Instantiate(hitMarker, globalMirrorHit, Quaternion.identity);
}

Finally, you need to add a script called Wind where we set the rotation of the object that is showing in which way the wind is coming from. It will make it easier to hit the target. Also add the script to the object showing the wind.

using UnityEngine;
using System.Collections;

public class Wind : MonoBehaviour 
{
    void Start() 
    {
        Vector3 windDirection = SniperController.windSpeed.normalized;
        Quaternion rotation = Quaternion.LookRotation(windDirection);
        transform.rotation = rotation;
    }
}

Also change in the script called IterationMethods so the wind comes from the SniperController, so you only have to change the wind velocity in one place.

//Wind velocity
velocityFactor += SniperController.windSpeed;

That's it. Move the target to a z-coordinate of 300, and if you press play you should now be able to fire bullets.

Sniper final scene 1

Sniper final scene 2