How to Make a FPS With Enemy AI in Unity 3D

How to Make a FPS With Enemy AI in Unity 3D

by NSDG • Aug 7, 2019 • 0 Comments
511
In this tutorial you'll learn how to create a First Person Shooter with a Weapon system and a simple enemy AI in Unity 3D.

Part 1: Creating a Player Controller


  • Create a new Game Object (Game Object -> Create Empty) and name it "Player"
  • Create new Capsule (Game Object -> 3D Object -> Capsule) and move it inside "Player" Object
  • Remove Capsule Collider component from Capsule and change its position to (0, 1, 0)
  • Move Main Camera inside "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 = 4.5f;
    public float jumpSpeed = 8.0f;
    public float gravity = 20.0f;
    public Camera playerCamera;
    public float lookSpeed = 2.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 / lookSpeed;
    }

    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");
            rotation.x += -Input.GetAxis("Mouse Y");
            rotation.x = Mathf.Clamp(rotation.x, -20f, 20f);
            transform.eulerAngles = new Vector2(0, rotation.y) * lookSpeed;
            playerCamera.transform.localRotation = Quaternion.Euler(rotation.x * lookSpeed, 0, 0);
        }
    }
}

  • Attach SC_CharacterController script to "Player" Object (You will notice that it also added another component called Character Controller, change its center value to (0, 1, 0))

The Player controller is now ready:



Part 2: Creating Weapon System


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

  • Create 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 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 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 SC_Bullet script have some errors. That's because we have one last thing to do, which is to define IEntity interface.
Interfaces in C# are useful for when you need to make sure that the script which uses it, have certain methods implemented.
The IEntity interface will have one method which is ApplyDamage, that's later will be used to inflict the damage to enemies and our player.

  • Create 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


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)
  • Aattach SC_WeaponManager script to it
  • Assign the Main Camera to a Player Camera variable

Setting Up a Rifle


  • Drag and drop your rifle model into scene (or simply create a Cube and stretch it if you do not have a rifle 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 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:



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



Much better.

  • Attach SC_Weapon script to a Rifle Object (You will notice that it also added a 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.
Bullet Prefab variable will be explained later in this tutorial.
For now we will just assign the Fire point variable:

  • Create 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 rifle model inside it with a different model (In my case I will use custom-made model of TAVOR X95)



  • Move Fire Point transform till it fits the new model



  • 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 the damage.

  • Create new GameObject and name it "Bullet"
  • Add Trail Renderer component to it and change its Time variable to 0.1.
  • Set the Width curve to a lower values (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 to Prefab and remove if from the Scene
  • Assign a newly created Prefab to Rifle and Submachinegun Bullet Prefab variable

Submachinegun:


Rifle:


The weapons are now ready.

Part 3: Creating the Enemy AI


The enemies will be a 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

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

  • Create 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))]
[RequireComponent(typeof(Rigidbody))]

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;
    Rigidbody r;

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

    // 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));
        //Gradually reduce rigidbody velocity if the force was applied by the bullet
        r.velocity *= 0.99f;
    }

    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 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 - 75, Screen.height / 2 - 20, 150, 40), "Game Over\nPress '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 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 Npc Dead Prefab variable
  • For the Fire Point, create new GameObject, move it inside 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

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 much Enemies left in current wave etc.

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



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

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

  • Attach SC_DamageReceiver script to a Player instance
  • Change 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 Scene as Navigation Static before baking NavMesh:



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



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



Everything works as expected!

Interested in obtaining the source project of this tutorial? Click here