Cum să faci un FPS cu suportul AI în Unity

First-person shooter (FPS) este un subgen de jocuri cu împușcături în care jucătorul este controlat dintr-o perspectivă la persoana întâi.

Pentru a crea un joc FPS în Unity, vom avea nevoie de un controler de jucător, o serie de obiecte (arme în acest caz) și inamicii.

Pasul 1: Creați controlerul jucătorului

Aici vom crea un controler care va fi folosit de jucătorul nostru.

  • Creați un nou obiect de joc (Obiect de joc -> Creați gol) și denumiți-l "Player"
  • Creați o nouă capsulă (Obiect de joc -> Obiect 3D -> Capsulă) și mutați-o în interiorul obiectului "Player"
  • Scoateți componenta Capsule Collider din Capsule și schimbați-i poziția în (0, 1, 0)
  • Mutați camera principală în interiorul obiectului "Player" și schimbați-i poziția la (0, 1.64, 0)
  • Creați un nou script, numiți-l "SC_CharacterController" și inserați codul de mai jos în interiorul acestuia:

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);
        }
    }
}
  • Atașați SC_CharacterController script la obiectul "Player" (Veți observa că a adăugat și o altă componentă numită Character Controller, schimbându-și valoarea centrală la (0, 1, 0))
  • Atribuiți Camera principală variabilei Camera Player în SC_CharacterController

Controlerul Player este acum gata:

Pasul 2: Creați sistemul de arme

Sistemul de arme al jucătorului va consta din 3 componente: un manager de arme, un script de armă și un script Bullet.

  • Creați un nou script, numiți-l "SC_WeaponManager" și inserați codul de mai jos în interiorul acestuia:

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;
        }
    }
}
  • Creați un nou script, denumiți-l "SC_Weapon" și inserați codul de mai jos în el:

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);
    }
}
  • Creați un nou script, denumiți-l "SC_Bullet" și inserați codul de mai jos în el:

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

Acum, veți observa că scriptul SC_Bullet are câteva erori. Asta pentru că avem un ultim lucru de făcut, și anume să definim interfața IEntity.

Interfețele în C# sunt utile atunci când trebuie să vă asigurați că scriptul care îl folosește are anumite metode implementate.

Interfața IEntity va avea o metodă care este ApplyDamage, care va fi folosită mai târziu pentru a provoca daune inamicilor și jucătorului nostru.

  • Creați un nou script, denumiți-l "SC_InterfaceManager" și inserați codul de mai jos în el:

SC_InterfaceManager.cs

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

Configurarea unui manager de arme

Un manager de arme este un obiect care va locui sub obiectul camerei principale și va conține toate armele.

  • Creați un nou GameObject și denumiți-l "WeaponManager"
  • Mutați WeaponManager în camera principală a jucătorului și schimbați-i poziția la (0, 0, 0)
  • Atașați scriptul SC_WeaponManager la "WeaponManager"
  • Atribuiți Camera principală variabilei Camera Player în SC_WeaponManager

Instalarea unei puști

  • Trageți și plasați modelul dvs. de armă în scenă (sau pur și simplu creați un cub și întindeți-l dacă nu aveți încă un model).
  • Scalați modelul astfel încât dimensiunea acestuia să fie relativă la o capsulă de jucător

În cazul meu, voi folosi un model de pușcă la comandă (BERGARA BA13):

BERGARA BA13

  • Creați un nou GameObject și denumiți-l "Rifle", apoi mutați modelul puștii în interiorul acestuia
  • Mutați obiectul "Rifle" în interiorul obiectului "WeaponManager" și plasați-l în fața camerei astfel:

Remediați problema decupării camerei în Unity.

Pentru a remedia decuparea obiectului, schimbați pur și simplu planul de tăiere apropiat al camerei la ceva mai mic (în cazul meu, l-am setat la 0,15):

BERGARA BA13

Mult mai bine.

  • Atașați scriptul SC_Weapon la un obiect Rifle (veți observa că a adăugat și o componentă Sursă audio, aceasta este necesară pentru a reda focul și a reîncărca sunetul).

După cum puteți vedea, SC_Weapon are 4 variabile de atribuit. Puteți atribui variabile audio Fire și Reîncărcare audio imediat dacă aveți clipuri audio adecvate în proiect.

Variabila Bullet Prefab va fi explicată mai târziu în acest tutorial.

Deocamdată, vom aloca variabila punct de foc:

  • Creați un nou GameObject, redenumiți-l în "FirePoint" și mutați-l în Rifle Object. Așezați-l chiar în fața butoiului sau ușor înăuntru, astfel:

  • Atribuiți FirePoint Transform unei variabile Fire point la SC_Weapon
  • Atribuiți pușca unei variabile Armă secundară în scriptul SC_WeaponManager

Instalarea unui pistol-mitralieră

  • Duplicați obiectul pușcă și redenumiți-l în Mitralieră
  • Înlocuiți modelul de pistol din interiorul acestuia cu un alt model (În cazul meu voi folosi modelul personalizat de TAVOR X95)

TAVOR X95

  • Mutați transformarea punctului de foc până se potrivește noului model

Configurarea obiectului Punctul de foc al armei în Unity.

  • Atribuiți Submachinegun unei variabile Armă principală în scriptul SC_WeaponManager

Configurarea unui prefabricat Bullet

Prefabricatul de gloanțe va fi generat în funcție de rata de tragere a armei și va folosi Raycast pentru a detecta dacă a lovit ceva și a provoca daune.

  • Creați un nou GameObject și denumiți-l "Bullet"
  • Adăugați componenta Trail Renderer și modificați variabila Time la 0.1.
  • Setați curba Lățimea la o valoare mai mică (de ex. Start 0.1 end 0), pentru a adăuga un traseu cu aspect ascuțit
  • Creați un material nou și denumiți-l bullet_trail_material și schimbați-i Shaderul în Particule/Aditive
  • Atribuiți un material nou creat unui Trailer Renderer
  • Schimbați culoarea traseului de redare cu ceva diferit (de ex. Început: Portocaliu strălucitor Sfârșit: Portocaliu închis)

  • Salvați obiectul Bullet în Prefab și ștergeți-l din scenă.
  • Atribuiți un prefabricat nou creat (glisare și plasare din vizualizarea Proiect) variabilei Prefabricate pentru pușcă și mitralieră Bullet

Carabina:

Puşcă:

Armele sunt acum gata.

Pasul 3: Creați IA inamicului

Inamicii vor fi simple Cuburi care urmează jucătorul și atacă odată ce sunt suficient de aproape. Ei vor ataca în valuri, fiecare val având mai mulți inamici de eliminat.

Configurarea AI inamicului

Mai jos am creat 2 variante ale Cubului (cea din stânga este pentru instanța vie, iar cea din dreapta va fi generată odată ce inamicul este ucis):

  • Adăugați o componentă Rigidbody atât la instanțele moarte, cât și la cele active
  • Salvați instanța moartă în Prefab și ștergeți-o din Scenă.

Acum, instanța în viață va avea nevoie de încă câteva componente pentru a putea naviga la nivelul jocului și a provoca daune jucătorului.

  • Creați un nou script și denumiți-l "SC_NPCEnemy" apoi inserați codul de mai jos în interiorul lui:

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);
        }
    }
}
  • Creați un nou script, denumiți-l "SC_EnemySpawner" apoi inserați codul de mai jos în el:

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++;
        }
    }
}
  • Creați un nou script, numiți-l "SC_DamageReceiver" apoi inserați codul de mai jos în interiorul lui:

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;
        }
    }
}
  • Atașați scriptul SC_NPCEnemy la instanța inamică în viață (veți observa că a adăugat o altă componentă numită NavMesh Agent, care este necesară pentru a naviga în NavMesh)
  • Atribuiți prefabricatul de instanță mort creat recent variabilei Npc Dead Prefab
  • Pentru Fire Point, creați un nou GameObject, mutați-l în interiorul instanței inamice în viață și plasați-l ușor în fața instanței, apoi atribuiți-l variabilei Fire Point:

  • În cele din urmă, salvați instanța activă în Prefab și ștergeți-o din Scene.

Configurarea generatorului inamicului

Acum să trecem la SC_EnemySpawner. Acest script va genera inamici în valuri și, de asemenea, va afișa câteva informații despre UI pe ecran, cum ar fi Player HP, muniție curentă, câți inamici rămân într-un val curent etc.

  • Creați un nou GameObject și denumiți-l "_EnemySpawner"
  • Atașați-i scriptul SC_EnemySpawner
  • Atribuiți IA inamicului nou creat variabilei Enemy Prefab
  • Atribuiți textura de mai jos variabilei Crosshair Texture

  • Creați câteva GameObjects noi și plasați-le în jurul scenei, apoi atribuiți-le matricei Spawn Points

Veți observa că mai există o ultimă variabilă de atribuit, care este variabila Player.

  • Atașați scriptul SC_DamageReceiver la o instanță Player
  • Schimbați eticheta de instanță Player la "Player"
  • Atribuiți variabilele Player Controller și Weapon Manager în SC_DamageReceiver

  • Atribuiți instanța Player unei variabile Player în SC_EnemySpawner

Și, în sfârșit, trebuie să coacem NavMesh în scena noastră, astfel încât AI inamic să poată naviga.

De asemenea, nu uitați să marcați fiecare obiect static din scenă ca static de navigare înainte de a coace NavMesh:

  • Accesați fereastra NavMesh (Fereastră -> AI -> Navigare), faceți clic pe fila Bake apoi faceți clic pe butonul Bake. După ce NavMesh este copt, ar trebui să arate cam așa:

Acum este timpul să apăsați pe Play și să îl testați:

Sharp Coder Video player

Totul funcționează conform așteptărilor!

Sursă
📁SimpleFPS.unitypackage4.61 MB
Articole sugerate
Cum să faci un AI al unui cerb în Unity
Implementarea IA a unui inamic în Unitate
Unity Adaugă inamici la un platformer 2D
Creați un NPC care urmează jucătorul în Unity
Lucrul cu NavMeshAgent în Unity
Revizuirea pachetului Unity Asset Store - Zombie AI System
Cum să faci un joc de supraviețuire în Unity