How to Make an FPS With the AI Support in Unity

First-person shooter (FPS) is a subgenre of shooter games where the player is controlled from a first-person perspective.

To make an FPS game in Unity we will need a player controller, an array of items (weapons in this case), and the enemies.

Step 1: Create the Player Controller

Here we will create a controller that will be used by our player.

  • Create a new Game Object (Game Object -> Create Empty) and name it "Player"
  • Create a new Capsule (Game Object -> 3D Object -> Capsule) and move it inside the "Player" Object
  • Remove the Capsule Collider component from Capsule and change its position to (0, 1, 0)
  • Move the Main Camera inside the "Player" Object and change its position to (0, 1.64, 0)
  • Create a new script, name it "SC_CharacterController" and paste the code below inside it:

SC_CharacterController.cs

using UnityEngine;

[RequireComponent(typeof(CharacterController))]

public class SC_CharacterController : MonoBehaviour
{
    public float speed = 7.5f;
    public float jumpSpeed = 8.0f;
    public float gravity = 20.0f;
    public Camera playerCamera;
    public float lookSpeed = 2.0f;
    public float lookXLimit = 45.0f;

    CharacterController characterController;
    Vector3 moveDirection = Vector3.zero;
    Vector2 rotation = Vector2.zero;

    [HideInInspector]
    public bool canMove = true;

    void Start()
    {
        characterController = GetComponent<CharacterController>();
        rotation.y = transform.eulerAngles.y;
    }

    void Update()
    {
        if (characterController.isGrounded)
        {
            // We are grounded, so recalculate move direction based on axes
            Vector3 forward = transform.TransformDirection(Vector3.forward);
            Vector3 right = transform.TransformDirection(Vector3.right);
            float curSpeedX = canMove ? speed * Input.GetAxis("Vertical") : 0;
            float curSpeedY = canMove ? speed * Input.GetAxis("Horizontal") : 0;
            moveDirection = (forward * curSpeedX) + (right * curSpeedY);

            if (Input.GetButton("Jump") && canMove)
            {
                moveDirection.y = jumpSpeed;
            }
        }

        // Apply gravity. Gravity is multiplied by deltaTime twice (once here, and once below
        // when the moveDirection is multiplied by deltaTime). This is because gravity should be applied
        // as an acceleration (ms^-2)
        moveDirection.y -= gravity * Time.deltaTime;

        // Move the controller
        characterController.Move(moveDirection * Time.deltaTime);

        // Player and Camera rotation
        if (canMove)
        {
            rotation.y += Input.GetAxis("Mouse X") * lookSpeed;
            rotation.x += -Input.GetAxis("Mouse Y") * lookSpeed;
            rotation.x = Mathf.Clamp(rotation.x, -lookXLimit, lookXLimit);
            playerCamera.transform.localRotation = Quaternion.Euler(rotation.x, 0, 0);
            transform.eulerAngles = new Vector2(0, rotation.y);
        }
    }
}
  • Attach SC_CharacterController script to "Player" Object (You will notice that it also added another component called Character Controller, changing its center value to (0, 1, 0))
  • Assign the Main Camera to the Player Camera variable in SC_CharacterController

The Player controller is now ready:

Step 2: Create the Weapon System

The player weapon system will consist of 3 components: a Weapon manager, a Weapon script, and a Bullet script.

  • Create a new script, name it "SC_WeaponManager" and paste the code below inside it:

SC_WeaponManager.cs

using UnityEngine;

public class SC_WeaponManager : MonoBehaviour
{
    public Camera playerCamera;
    public SC_Weapon primaryWeapon;
    public SC_Weapon secondaryWeapon;

    [HideInInspector]
    public SC_Weapon selectedWeapon;

    // Start is called before the first frame update
    void Start()
    {
        //At the start we enable the primary weapon and disable the secondary
        primaryWeapon.ActivateWeapon(true);
        secondaryWeapon.ActivateWeapon(false);
        selectedWeapon = primaryWeapon;
        primaryWeapon.manager = this;
        secondaryWeapon.manager = this;
    }

    // Update is called once per frame
    void Update()
    {
        //Select secondary weapon when pressing 1
        if (Input.GetKeyDown(KeyCode.Alpha1))
        {
            primaryWeapon.ActivateWeapon(false);
            secondaryWeapon.ActivateWeapon(true);
            selectedWeapon = secondaryWeapon;
        }

        //Select primary weapon when pressing 2
        if (Input.GetKeyDown(KeyCode.Alpha2))
        {
            primaryWeapon.ActivateWeapon(true);
            secondaryWeapon.ActivateWeapon(false);
            selectedWeapon = primaryWeapon;
        }
    }
}
  • Create a new script, name it "SC_Weapon" and paste the code below inside it:

SC_Weapon.cs

using System.Collections;
using UnityEngine;

[RequireComponent(typeof(AudioSource))]

public class SC_Weapon : MonoBehaviour
{
    public bool singleFire = false;
    public float fireRate = 0.1f;
    public GameObject bulletPrefab;
    public Transform firePoint;
    public int bulletsPerMagazine = 30;
    public float timeToReload = 1.5f;
    public float weaponDamage = 15; //How much damage should this weapon deal
    public AudioClip fireAudio;
    public AudioClip reloadAudio;

    [HideInInspector]
    public SC_WeaponManager manager;

    float nextFireTime = 0;
    bool canFire = true;
    int bulletsPerMagazineDefault = 0;
    AudioSource audioSource;

    // Start is called before the first frame update
    void Start()
    {
        bulletsPerMagazineDefault = bulletsPerMagazine;
        audioSource = GetComponent<AudioSource>();
        audioSource.playOnAwake = false;
        //Make sound 3D
        audioSource.spatialBlend = 1f;
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(0) && singleFire)
        {
            Fire();
        }
        if (Input.GetMouseButton(0) && !singleFire)
        {
            Fire();
        }
        if (Input.GetKeyDown(KeyCode.R) && canFire)
        {
            StartCoroutine(Reload());
        }
    }

    void Fire()
    {
        if (canFire)
        {
            if (Time.time > nextFireTime)
            {
                nextFireTime = Time.time + fireRate;

                if (bulletsPerMagazine > 0)
                {
                    //Point fire point at the current center of Camera
                    Vector3 firePointPointerPosition = manager.playerCamera.transform.position + manager.playerCamera.transform.forward * 100;
                    RaycastHit hit;
                    if (Physics.Raycast(manager.playerCamera.transform.position, manager.playerCamera.transform.forward, out hit, 100))
                    {
                        firePointPointerPosition = hit.point;
                    }
                    firePoint.LookAt(firePointPointerPosition);
                    //Fire
                    GameObject bulletObject = Instantiate(bulletPrefab, firePoint.position, firePoint.rotation);
                    SC_Bullet bullet = bulletObject.GetComponent<SC_Bullet>();
                    //Set bullet damage according to weapon damage value
                    bullet.SetDamage(weaponDamage);

                    bulletsPerMagazine--;
                    audioSource.clip = fireAudio;
                    audioSource.Play();
                }
                else
                {
                    StartCoroutine(Reload());
                }
            }
        }
    }

    IEnumerator Reload()
    {
        canFire = false;

        audioSource.clip = reloadAudio;
        audioSource.Play();

        yield return new WaitForSeconds(timeToReload);

        bulletsPerMagazine = bulletsPerMagazineDefault;

        canFire = true;
    }

    //Called from SC_WeaponManager
    public void ActivateWeapon(bool activate)
    {
        StopAllCoroutines();
        canFire = true;
        gameObject.SetActive(activate);
    }
}
  • Create a new script, name it "SC_Bullet" and paste the code below inside it:

SC_Bullet.cs

using System.Collections;
using UnityEngine;

public class SC_Bullet : MonoBehaviour
{
    public float bulletSpeed = 345;
    public float hitForce = 50f;
    public float destroyAfter = 3.5f;

    float currentTime = 0;
    Vector3 newPos;
    Vector3 oldPos;
    bool hasHit = false;

    float damagePoints;

    // Start is called before the first frame update
    IEnumerator Start()
    {
        newPos = transform.position;
        oldPos = newPos;

        while (currentTime < destroyAfter && !hasHit)
        {
            Vector3 velocity = transform.forward * bulletSpeed;
            newPos += velocity * Time.deltaTime;
            Vector3 direction = newPos - oldPos;
            float distance = direction.magnitude;
            RaycastHit hit;

            // Check if we hit anything on the way
            if (Physics.Raycast(oldPos, direction, out hit, distance))
            {
                if (hit.rigidbody != null)
                {
                    hit.rigidbody.AddForce(direction * hitForce);

                    IEntity npc = hit.transform.GetComponent<IEntity>();
                    if (npc != null)
                    {
                        //Apply damage to NPC
                        npc.ApplyDamage(damagePoints);
                    }
                }

                newPos = hit.point; //Adjust new position
                StartCoroutine(DestroyBullet());
            }

            currentTime += Time.deltaTime;
            yield return new WaitForFixedUpdate();

            transform.position = newPos;
            oldPos = newPos;
        }

        if (!hasHit)
        {
            StartCoroutine(DestroyBullet());
        }
    }

    IEnumerator DestroyBullet()
    {
        hasHit = true;
        yield return new WaitForSeconds(0.5f);
        Destroy(gameObject);
    }

    //Set how much damage this bullet will deal
    public void SetDamage(float points)
    {
        damagePoints = points;
    }
}

Now, you will notice that the SC_Bullet script has some errors. That's because we have one last thing to do, which is to define the IEntity interface.

Interfaces in C# are useful when you need to make sure that the script which uses it, has certain methods implemented.

The IEntity interface will have one method which is ApplyDamage, that's later will be used to inflict damage to enemies and our player.

  • Create a new script, name it "SC_InterfaceManager" and paste the code below inside it:

SC_InterfaceManager.cs

//Entity interafce
interface IEntity
{ 
    void ApplyDamage(float points);
}

Setting Up a Weapon Manager

A weapon manager is an Object that will reside under the Main Camera Object and will contain all the weapons.

  • Create a new GameObject and name it "WeaponManager"
  • Move the WeaponManager inside the Player Main Camera and change its position to (0, 0, 0)
  • Attach SC_WeaponManager script to "WeaponManager"
  • Assign the Main Camera to the Player Camera variable in SC_WeaponManager

Setting Up a Rifle

  • Drag and drop your gun model into the scene (or simply create a Cube and stretch it if you do not have a model yet).
  • Scale the model so its size is relative to a Player Capsule

In my case, I will be using a custom-made Rifle model (BERGARA BA13):

BERGARA BA13

  • Create a new GameObject and name it "Rifle" then move the rifle model inside it
  • Move the "Rifle" Object inside the "WeaponManager" Object and place it in front of the Camera like this:

Fix the Camera Clipping Issue in Unity.

To fix the object clipping, simply change the Camera's near clipping plane to something smaller (in my case I set it to 0.15):

BERGARA BA13

Much better.

  • Attach SC_Weapon script to a Rifle Object (You will notice that it also added an Audio Source component, this is needed to play the fire and reload audios).

As you can see, SC_Weapon has 4 variables to assign. You can assign Fire audio and Reload audio variables right away if you have suitable Audio Clips in your project.

The Bullet Prefab variable will be explained later in this tutorial.

For now, we will just assign the Fire point variable:

  • Create a new GameObject, rename it to "FirePoint" and move it inside Rifle Object. Place it right in front of the barrel or slightly inside, like this:

  • Assign FirePoint Transform to a Fire point variable at SC_Weapon
  • Assign Rifle to a Secondary Weapon variable in SC_WeaponManager script

Setting Up a Submachinegun

  • Duplicate the Rifle Object and rename it to Submachinegun
  • Replace the gun model inside it with a different model (In my case I will use the custom-made model of TAVOR X95)

TAVOR X95

  • Move Fire Point transform till it fits the new model

Weapon Fire Point object setup in Unity.

  • Assign Submachinegun to a Primary Weapon variable in SC_WeaponManager script

Setting Up a Bullet Prefab

Bullet prefab will be spawned according to a Weapon's fire rate and will use Raycast to detect whether it hit something and inflict damage.

  • Create a new GameObject and name it "Bullet"
  • Add the Trail Renderer component to it and change its Time variable to 0.1.
  • Set the Width curve to a lower value (ex. Start 0.1 end 0), to add a trail that pointy look
  • Create new Material and name it bullet_trail_material and change its Shader to Particles/Additive
  • Assign a newly created material to a Trail Renderer
  • Change the Color of Trail Renderer to something different (ex. Start: Bright Orange End: Darker Orange)

  • Save the Bullet Object to Prefab and delete it from the Scene.
  • Assign a newly created Prefab (drag & drop from the Project view) to Rifle and Submachinegun Bullet Prefab variable

Submachinegun:

Rifle:

The weapons are now ready.

Step 3: Create the Enemy AI

The enemies will be simple Cubes that follow the Player and attack once they are close enough. They will attack in waves, with each wave having more enemies to eliminate.

Setting Up Enemy AI

Below I have created 2 variations of the Cube (The Left one is for the alive instance and the Right one will be spawned once the enemy is killed):

  • Add a Rigidbody component to both dead and alive instances
  • Save the Dead Instance to Prefab and delete it from Scene.

Now, the alive instance will need a couple more components to be able to navigate the game level and inflict damage to the Player.

  • Create a new script and name it "SC_NPCEnemy" then paste the code below inside it:

SC_NPCEnemy.cs

using UnityEngine;
using UnityEngine.AI;

[RequireComponent(typeof(NavMeshAgent))]

public class SC_NPCEnemy : MonoBehaviour, IEntity
{
    public float attackDistance = 3f;
    public float movementSpeed = 4f;
    public float npcHP = 100;
    //How much damage will npc deal to the player
    public float npcDamage = 5;
    public float attackRate = 0.5f;
    public Transform firePoint;
    public GameObject npcDeadPrefab;

    [HideInInspector]
    public Transform playerTransform;
    [HideInInspector]
    public SC_EnemySpawner es;
    NavMeshAgent agent;
    float nextAttackTime = 0;

    // Start is called before the first frame update
    void Start()
    {
        agent = GetComponent<NavMeshAgent>();
        agent.stoppingDistance = attackDistance;
        agent.speed = movementSpeed;

        //Set Rigidbody to Kinematic to prevent hit register bug
        if (GetComponent<Rigidbody>())
        {
            GetComponent<Rigidbody>().isKinematic = true;
        }
    }

    // Update is called once per frame
    void Update()
    {
        if (agent.remainingDistance - attackDistance < 0.01f)
        {
            if(Time.time > nextAttackTime)
            {
                nextAttackTime = Time.time + attackRate;

                //Attack
                RaycastHit hit;
                if(Physics.Raycast(firePoint.position, firePoint.forward, out hit, attackDistance))
                {
                    if (hit.transform.CompareTag("Player"))
                    {
                        Debug.DrawLine(firePoint.position, firePoint.position + firePoint.forward * attackDistance, Color.cyan);

                        IEntity player = hit.transform.GetComponent<IEntity>();
                        player.ApplyDamage(npcDamage);
                    }
                }
            }
        }
        //Move towardst he player
        agent.destination = playerTransform.position;
        //Always look at player
        transform.LookAt(new Vector3(playerTransform.transform.position.x, transform.position.y, playerTransform.position.z));
    }

    public void ApplyDamage(float points)
    {
        npcHP -= points;
        if(npcHP <= 0)
        {
            //Destroy the NPC
            GameObject npcDead = Instantiate(npcDeadPrefab, transform.position, transform.rotation);
            //Slightly bounce the npc dead prefab up
            npcDead.GetComponent<Rigidbody>().velocity = (-(playerTransform.position - transform.position).normalized * 8) + new Vector3(0, 5, 0);
            Destroy(npcDead, 10);
            es.EnemyEliminated(this);
            Destroy(gameObject);
        }
    }
}
  • Create a new script, name it "SC_EnemySpawner" then paste the code below inside it:

SC_EnemySpawner.cs

using UnityEngine;
using UnityEngine.SceneManagement;

public class SC_EnemySpawner : MonoBehaviour
{
    public GameObject enemyPrefab;
    public SC_DamageReceiver player;
    public Texture crosshairTexture;
    public float spawnInterval = 2; //Spawn new enemy each n seconds
    public int enemiesPerWave = 5; //How many enemies per wave
    public Transform[] spawnPoints;

    float nextSpawnTime = 0;
    int waveNumber = 1;
    bool waitingForWave = true;
    float newWaveTimer = 0;
    int enemiesToEliminate;
    //How many enemies we already eliminated in the current wave
    int enemiesEliminated = 0;
    int totalEnemiesSpawned = 0;

    // Start is called before the first frame update
    void Start()
    {
        //Lock cursor
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;

        //Wait 10 seconds for new wave to start
        newWaveTimer = 10;
        waitingForWave = true;
    }

    // Update is called once per frame
    void Update()
    {
        if (waitingForWave)
        {
            if(newWaveTimer >= 0)
            {
                newWaveTimer -= Time.deltaTime;
            }
            else
            {
                //Initialize new wave
                enemiesToEliminate = waveNumber * enemiesPerWave;
                enemiesEliminated = 0;
                totalEnemiesSpawned = 0;
                waitingForWave = false;
            }
        }
        else
        {
            if(Time.time > nextSpawnTime)
            {
                nextSpawnTime = Time.time + spawnInterval;

                //Spawn enemy 
                if(totalEnemiesSpawned < enemiesToEliminate)
                {
                    Transform randomPoint = spawnPoints[Random.Range(0, spawnPoints.Length - 1)];

                    GameObject enemy = Instantiate(enemyPrefab, randomPoint.position, Quaternion.identity);
                    SC_NPCEnemy npc = enemy.GetComponent<SC_NPCEnemy>();
                    npc.playerTransform = player.transform;
                    npc.es = this;
                    totalEnemiesSpawned++;
                }
            }
        }

        if (player.playerHP <= 0)
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                Scene scene = SceneManager.GetActiveScene();
                SceneManager.LoadScene(scene.name);
            }
        }
    }

    void OnGUI()
    {
        GUI.Box(new Rect(10, Screen.height - 35, 100, 25), ((int)player.playerHP).ToString() + " HP");
        GUI.Box(new Rect(Screen.width / 2 - 35, Screen.height - 35, 70, 25), player.weaponManager.selectedWeapon.bulletsPerMagazine.ToString());

        if(player.playerHP <= 0)
        {
            GUI.Box(new Rect(Screen.width / 2 - 85, Screen.height / 2 - 20, 170, 40), "Game Over\n(Press 'Space' to Restart)");
        }
        else
        {
            GUI.DrawTexture(new Rect(Screen.width / 2 - 3, Screen.height / 2 - 3, 6, 6), crosshairTexture);
        }

        GUI.Box(new Rect(Screen.width / 2 - 50, 10, 100, 25), (enemiesToEliminate - enemiesEliminated).ToString());

        if (waitingForWave)
        {
            GUI.Box(new Rect(Screen.width / 2 - 125, Screen.height / 4 - 12, 250, 25), "Waiting for Wave " + waveNumber.ToString() + " (" + ((int)newWaveTimer).ToString() + " seconds left...)");
        }
    }

    public void EnemyEliminated(SC_NPCEnemy enemy)
    {
        enemiesEliminated++;

        if(enemiesToEliminate - enemiesEliminated <= 0)
        {
            //Start next wave
            newWaveTimer = 10;
            waitingForWave = true;
            waveNumber++;
        }
    }
}
  • Create a new script, name it "SC_DamageReceiver" then paste the code below inside it:

SC_DamageReceiver.cs

using UnityEngine;

public class SC_DamageReceiver : MonoBehaviour, IEntity
{
    //This script will keep track of player HP
    public float playerHP = 100;
    public SC_CharacterController playerController;
    public SC_WeaponManager weaponManager;

    public void ApplyDamage(float points)
    {
        playerHP -= points;

        if(playerHP <= 0)
        {
            //Player is dead
            playerController.canMove = false;
            playerHP = 0;
        }
    }
}
  • Attach SC_NPCEnemy script to alive enemy instance (You'll notice it added another component called NavMesh Agent, which is needed to navigate the NavMesh)
  • Assign the recently created dead instance prefab to the Npc Dead Prefab variable
  • For the Fire Point, create a new GameObject, move it inside the alive enemy instance and place it slightly in front of the instance, then assign it to the Fire Point variable:

  • Finally, Save the alive instance to Prefab and delete it from Scene.

Setting Up Enemy Spawner

Now let's move to SC_EnemySpawner. This script will spawn enemies in waves and also will show some UI information on the screen, such as Player HP, current Ammo, how many Enemies are left in a current wave, etc.

  • Create a new GameObject and name it "_EnemySpawner"
  • Attach the SC_EnemySpawner script to it
  • Assign the newly created enemy AI to the Enemy Prefab variable
  • Assign the texture below to the Crosshair Texture variable

  • Create a couple of new GameObjects and place them around the Scene then assign them to the Spawn Points array

You'll notice that there is one last variable left to assign which is the Player variable.

  • Attach SC_DamageReceiver script to a Player instance
  • Change the Player instance tag to "Player"
  • Assign Player Controller and Weapon Manager variables in SC_DamageReceiver

  • Assign Player instance to a Player variable in SC_EnemySpawner

And lastly, we have to bake the NavMesh in our scene so the enemy AI will be able to navigate.

Also, don't forget to mark every static Object in the Scene as Navigation Static before baking NavMesh:

  • Go to the NavMesh window (Window -> AI -> Navigation), click on the Bake tab then click the Bake button. After the NavMesh is baked it should look something like this:

Now it's time to press Play and test it:

Sharp Coder Video Player

Everything works as expected!

Source
📁SimpleFPS.unitypackage4.61 MB
Suggested Articles
How to Make an AI of a Deer in Unity
Implementing AI of an Enemy in Unity
Unity Add Enemies to a 2D Platformer
Working with NavMeshAgent in Unity
Create an NPC that Follows the Player in Unity
Review of the Unity Asset Store Package - Zombie AI System
How to Make a Survival Game in Unity