Hoe je een FPS maakt met de AI-ondersteuning in Unity

First-person shooter (FPS) is een subgenre van schietspellen waarbij de speler wordt bestuurd vanuit een first-person-perspectief.

Om een ​​FPS-game te maken in Unity hebben we een spelercontroller, een reeks items (in dit geval wapens) en de vijanden nodig.

Stap 1: Maak de spelercontroller

Hier zullen we een controller maken die door onze speler zal worden gebruikt.

  • Maak een nieuw spelobject (Spelobject -> Leeg maken) en geef het een naam "Player"
  • Maak een nieuwe capsule (Spelobject -> 3D-object -> Capsule) en verplaats deze binnen het "Player"-object
  • Verwijder de Capsule Collider-component uit de Capsule en verander de positie in (0, 1, 0)
  • Verplaats de hoofdcamera binnen het "Player"-object en verander de positie ervan in (0, 1.64, 0)
  • Maak een nieuw script, noem het "SC_CharacterController" en plak de onderstaande code erin:

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);
        }
    }
}
  • Voeg het SC_CharacterController-script toe aan "Player" Object (u zult merken dat het ook een andere component heeft toegevoegd, genaamd Character Controller, waarbij de middelste waarde is gewijzigd in (0, 1, 0))
  • Wijs de hoofdcamera toe aan de spelercameravariabele in SC_CharacterController

De spelercontroller is nu klaar:

Stap 2: Creëer het wapensysteem

Het wapensysteem van de speler zal uit 3 componenten bestaan: een wapenmanager, een wapenscript en een kogelscript.

  • Maak een nieuw script, noem het "SC_WeaponManager" en plak de onderstaande code erin:

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;
        }
    }
}
  • Maak een nieuw script, noem het "SC_Weapon" en plak de onderstaande code erin:

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);
    }
}
  • Maak een nieuw script, noem het "SC_Bullet" en plak de onderstaande code erin:

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;
    }
}

Nu zult u merken dat het SC_Bullet-script enkele fouten bevat. Dat komt omdat we nog één laatste ding moeten doen, namelijk het definiëren van de IEntity-interface.

Interfaces in C# zijn handig als u ervoor wilt zorgen dat het script dat er gebruik van maakt, bepaalde methoden heeft geïmplementeerd.

De IEntity-interface zal één methode hebben: ApplyDamage, die later zal worden gebruikt om schade toe te brengen aan vijanden en onze speler.

  • Maak een nieuw script, noem het "SC_InterfaceManager" en plak de onderstaande code erin:

SC_InterfaceManager.cs

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

Een wapenmanager instellen

Een wapenmanager is een object dat zich onder het hoofdcamera-object bevindt en dat alle wapens bevat.

  • Maak een nieuw GameObject en geef het een naam "WeaponManager"
  • Verplaats de WeaponManager binnen de hoofdcamera van de speler en verander zijn positie in (0, 0, 0)
  • Voeg het SC_WeaponManager-script toe aan "WeaponManager"
  • Wijs de hoofdcamera toe aan de spelercameravariabele in SC_WeaponManager

Een geweer opzetten

  • Sleep je wapenmodel naar de scène (of maak eenvoudig een kubus en rek deze uit als je nog geen model hebt).
  • Schaal het model zodat de grootte relatief is ten opzichte van een spelercapsule

In mijn geval gebruik ik een op maat gemaakt geweermodel (BERGARA BA13):

BERGARA BA13

  • Maak een nieuw GameObject en noem het "Rifle" en verplaats vervolgens het geweermodel erin
  • Verplaats het "Rifle" object binnen het "WeaponManager" object en plaats het als volgt voor de camera:

Los het probleem met het knippen van de camera op in Unity.

Om het uitknippen van objecten te corrigeren, wijzigt u eenvoudigweg het nabije uitknipvlak van de camera naar iets kleiners (in mijn geval stel ik dit in op 0,15):

BERGARA BA13

Veel beter.

  • Voeg het SC_Weapon-script toe aan een geweerobject (je zult merken dat het ook een audiobroncomponent heeft toegevoegd, dit is nodig om het vuur af te spelen en de audio opnieuw te laden).

Zoals u kunt zien, heeft SC_Weapon 4 variabelen om toe te wijzen. U kunt de audiovariabelen Fire audio en Reload meteen toewijzen als u over geschikte audioclips in uw project beschikt.

De Bullet Prefab-variabele wordt later in deze tutorial uitgelegd.

Voorlopig wijzen we alleen de Fire point-variabele toe:

  • Maak een nieuw GameObject, hernoem het naar "FirePoint" en verplaats het binnen Rifle Object. Plaats hem recht voor de ton of iets naar binnen, zoals hier:

  • Wijs FirePoint Transform toe aan een Fire point-variabele bij SC_Weapon
  • Wijs Geweer toe aan een secundaire wapenvariabele in het SC_WeaponManager-script

Een machinepistool opzetten

  • Dupliceer het geweerobject en hernoem het naar Submachinegun
  • Vervang het pistoolmodel erin door een ander model (in mijn geval zal ik het op maat gemaakte model van TAVOR X95 gebruiken)

TAVOR X95

  • Verplaats Fire Point-transformatie totdat deze in het nieuwe model past

Wapenvuurpuntobject instellen in Unity.

  • Wijs Submachinegun toe aan een primaire wapenvariabele in het SC_WeaponManager-script

Een Bullet Prefab opzetten

Prefab kogels worden voortgebracht op basis van de vuursnelheid van een wapen en zullen Raycast gebruiken om te detecteren of het iets raakt en schade toebrengt.

  • Maak een nieuw GameObject en geef het een naam "Bullet"
  • Voeg de component Trail Renderer eraan toe en wijzig de variabele Time in 0,1.
  • Stel de breedtecurve in op een lagere waarde (bijv. Begin 0,1 en eind 0), om een ​​spoor toe te voegen dat er puntig uitziet
  • Maak een nieuw materiaal en noem het bullet_trail_material en verander de Shader in Particles/Additive
  • Wijs een nieuw gemaakt materiaal toe aan een Trail Renderer
  • Verander de kleur van Trail Renderer in iets anders (bijvoorbeeld Start: Helder Oranje Einde: Donkerder Oranje)

  • Sla het Bullet-object op in Prefab en verwijder het uit de scène.
  • Wijs een nieuw aangemaakt Prefab toe (slepen en neerzetten vanuit de projectweergave) aan de Prefab-variabele Rifle en Submachinegun Bullet

Machinepistool:

Geweer:

De wapens zijn nu klaar.

Stap 3: Creëer de vijandelijke AI

De vijanden zullen eenvoudige kubussen zijn die de speler volgen en aanvallen zodra ze dichtbij genoeg zijn. Ze zullen aanvallen in golven, waarbij elke golf meer vijanden heeft om te elimineren.

Vijandelijke AI instellen

Hieronder heb ik 2 varianten van de kubus gemaakt (de linker is voor de levende instantie en de rechter zal verschijnen zodra de vijand is gedood):

  • Voeg een Rigidbody-component toe aan zowel dode als levende exemplaren
  • Sla de dode instantie op in Prefab en verwijder deze uit Scene.

Nu heeft de levende instantie nog een paar componenten nodig om door het spelniveau te kunnen navigeren en schade aan de speler toe te brengen.

  • Maak een nieuw script, noem het "SC_NPCEnemy" en plak de onderstaande code erin:

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);
        }
    }
}
  • Maak een nieuw script, noem het "SC_EnemySpawner" en plak de onderstaande code erin:

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++;
        }
    }
}
  • Maak een nieuw script, noem het "SC_DamageReceiver" en plak de onderstaande code erin:

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;
        }
    }
}
  • Voeg het SC_NPCEnemy-script toe aan een levende vijandelijke instantie (je zult merken dat er een andere component is toegevoegd genaamd NavMesh Agent, die nodig is om door de NavMesh te navigeren)
  • Wijs de recent gemaakte dode instance-prefab toe aan de Npc Dead Prefab-variabele
  • Maak voor het Fire Point een nieuw GameObject, verplaats het binnen de levende vijandelijke instantie en plaats het iets voor de instantie, en wijs het vervolgens toe aan de Fire Point-variabele:

  • Sla ten slotte de levende instantie op in Prefab en verwijder deze uit Scene.

Vijandelijke spawner instellen

Laten we nu naar SC_EnemySpawner gaan. Dit script zal vijanden in golven spawnen en zal ook wat UI-informatie op het scherm tonen, zoals Speler HP, huidige munitie, hoeveel vijanden er nog over zijn in een huidige golf, enz.

  • Maak een nieuw GameObject en geef het een naam "_EnemySpawner"
  • Voeg het SC_EnemySpawner-script eraan toe
  • Wijs de nieuw gemaakte vijandelijke AI toe aan de Enemy Prefab-variabele
  • Wijs de onderstaande textuur toe aan de variabele Crosshair Texture

  • Maak een paar nieuwe GameObjects en plaats ze rond de scène en wijs ze vervolgens toe aan de Spawn Points-array

U zult merken dat er nog één laatste variabele overblijft om toe te wijzen, namelijk de Player-variabele.

  • Koppel het SC_DamageReceiver-script aan een Player-instantie
  • Wijzig de Player-instantietag in "Player"
  • Wijs Player Controller- en Weapon Manager-variabelen toe in SC_DamageReceiver

  • Wijs een Player-instantie toe aan een Player-variabele in SC_EnemySpawner

En ten slotte moeten we de NavMesh in onze scène bakken, zodat de vijandelijke AI kan navigeren.

Vergeet ook niet om elk statisch object in de scène als navigatie statisch te markeren voordat u NavMesh gaat bakken:

  • Ga naar het NavMesh-venster (Venster -> AI -> Navigatie), klik op het tabblad Bakken en klik vervolgens op de knop Bakken. Nadat de NavMesh is gebakken, zou het er ongeveer zo uit moeten zien:

Nu is het tijd om op Play te drukken en het te testen:

Sharp Coder Video speler

Alles werkt zoals verwacht!

Bron
📁SimpleFPS.unitypackage4.61 MB
Voorgestelde artikelen
Hoe je een AI van een hert in eenheid maakt
Unity Voeg vijanden toe aan een 2D-platformgame
Implementatie van AI van een vijand in eenheid
Werken met NavMeshAgent in Unity
Creëer een NPC die de speler in eenheid volgt
Herziening van het Unity Asset Store-pakket - Zombie AI-systeem
Hoe je een overlevingsspel in eenheid maakt