Comment créer un FPS avec le support de l'IA dans Unity

Le jeu de tir à la première personne (FPS) est un sous-genre de jeux de tir où le joueur est contrôlé à la première personne.

Pour créer un jeu FPS dans Unity, nous aurons besoin d'un contrôleur de joueur, d'un ensemble d'objets (des armes dans ce cas) et des ennemis.

Étape 1: créer le contrôleur de lecteur

Ici, nous allons créer un contrôleur qui sera utilisé par notre joueur.

  • Créez un nouvel objet de jeu (Game Object -> Create Empty) et nommez-le "Player"
  • Créez une nouvelle capsule (Objet de jeu -> Objet 3D -> Capsule) et déplacez-la à l'intérieur de l'objet "Player"
  • Retirez le composant Capsule Collider de Capsule et changez sa position en (0, 1, 0)
  • Déplacez la caméra principale à l'intérieur de l'objet "Player" et changez sa position en (0, 1.64, 0)
  • Créez un nouveau script, nommez-le "SC_CharacterController" et collez-y le code ci-dessous:

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);
        }
    }
}
  • Attachez le script SC_CharacterController à l'objet "Player" (vous remarquerez qu'il a également ajouté un autre composant appelé Character Controller, en changeant sa valeur centrale en (0, 1, 0))
  • Attribuez la caméra principale à la variable Player Camera dans SC_CharacterController

Le contrôleur du lecteur est maintenant prêt:

Étape 2: Créer le système d'arme

Le système d'arme du joueur comprendra 3 composants: un gestionnaire d'armes, un script d'arme et un script de balle.

  • Créez un nouveau script, nommez-le "SC_WeaponManager" et collez-y le code ci-dessous:

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;
        }
    }
}
  • Créez un nouveau script, nommez-le "SC_Weapon" et collez-y le code ci-dessous:

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);
    }
}
  • Créez un nouveau script, nommez-le "SC_Bullet" et collez-y le code ci-dessous:

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

Maintenant, vous remarquerez que le script SC_Bullet contient des erreurs. C'est parce que nous avons une dernière chose à faire, qui est de définir l'interface IEntity.

Les interfaces en C# sont utiles lorsque vous devez vous assurer que le script qui l'utilise a implémenté certaines méthodes.

L'interface IEntity aura une méthode qui est ApplyDamage, qui sera ensuite utilisée pour infliger des dégâts aux ennemis et à notre joueur.

  • Créez un nouveau script, nommez-le "SC_InterfaceManager" et collez-y le code ci-dessous:

SC_InterfaceManager.cs

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

Configurer un gestionnaire d'armes

Un gestionnaire d'armes est un objet qui résidera sous l'objet caméra principal et contiendra toutes les armes.

  • Créez un nouveau GameObject et nommez-le "WeaponManager"
  • Déplacez le WeaponManager à l'intérieur de la caméra principale du joueur et changez sa position en (0, 0, 0)
  • Attachez le script SC_WeaponManager à "WeaponManager"
  • Attribuez la caméra principale à la variable Player Camera dans SC_WeaponManager

Configuration d'un fusil

  • Faites glisser et déposez votre modèle de pistolet dans la scène (ou créez simplement un cube et étirez-le si vous n'avez pas encore de modèle).
  • Mettez le modèle à l'échelle afin que sa taille soit relative à une capsule de joueur

Dans mon cas, j'utiliserai un modèle de fusil sur mesure (BERGARA BA13):

BERGARA BA13

  • Créez un nouveau GameObject et nommez-le "Rifle" puis déplacez le modèle de fusil à l'intérieur
  • Déplacez l'Objet "Rifle" à l'intérieur de l'Objet "WeaponManager" et placez-le devant la Caméra comme ceci:

Résoudre le problème d'écrêtage de la caméra dans Unity.

Pour corriger l'écrêtage de l'objet, changez simplement le plan d'écrêtage proche de la caméra en quelque chose de plus petit (dans mon cas, je l'ai réglé sur 0,15):

BERGARA BA13

Beaucoup mieux.

  • Attachez le script SC_Weapon à un objet fusil (vous remarquerez qu'il a également ajouté un composant source audio, cela est nécessaire pour jouer le feu et recharger les audios).

Comme vous pouvez le voir, SC_Weapon a 4 variables à assigner. Vous pouvez assigner immédiatement des variables audio Fire et Reload audio si vous avez des clips audio appropriés dans votre projet.

La variable Bullet Prefab sera expliquée plus loin dans ce tutoriel.

Pour l'instant, nous allons juste assigner la variable Fire point:

  • Créez un nouveau GameObject, renommez-le "FirePoint" et déplacez-le dans Rifle Object. Placez-le juste devant le canon ou légèrement à l'intérieur, comme ceci:

  • Affecter la transformation FirePoint à une variable de point d'incendie à SC_Weapon
  • Attribuer un fusil à une variable d'arme secondaire dans le script SC_WeaponManager

Configuration d'une mitraillette

  • Dupliquez l'objet fusil et renommez-le en mitraillette
  • Remplacez le modèle de pistolet à l'intérieur par un modèle différent (dans mon cas, j'utiliserai le modèle sur mesure de TAVOR X95)

TAVEUR X95

  • Déplacez la transformation Fire Point jusqu'à ce qu'elle corresponde au nouveau modèle

Configuration de l'objet Weapon Fire Point dans Unity.

  • Attribuer Submachinegun à une variable d'arme principale dans le script SC_WeaponManager

Configuration d'un préfabriqué Bullet

Le préfabriqué Bullet sera généré en fonction de la cadence de tir d'une arme et utilisera Raycast pour détecter s'il a touché quelque chose et infligé des dégâts.

  • Créez un nouveau GameObject et nommez-le "Bullet"
  • Ajoutez-y le composant Trail Renderer et changez sa variable Time en 0.1.
  • Réglez la courbe de largeur sur une valeur inférieure (par exemple, début 0,1 et fin 0), pour ajouter une traînée au look pointu
  • Créez un nouveau matériau et nommez-le bullet_trail_material et changez son shader en particules/additif
  • Attribuer un matériau nouvellement créé à un Trail Renderer
  • Changez la couleur de Trail Renderer en quelque chose de différent (ex. Start: Bright Orange End: Darker Orange)

  • Enregistrez l'objet Bullet dans Prefab et supprimez-le de la scène.
  • Attribuez un préfabriqué nouvellement créé (glisser-déposer depuis la vue du projet) à la variable de préfabriqué de fusil et de mitraillette

Mitraillette:

Fusil:

Les armes sont maintenant prêtes.

Étape 3: créer l'IA ennemie

Les ennemis seront de simples cubes qui suivront le joueur et attaqueront une fois qu'ils seront suffisamment proches. Ils attaqueront par vagues, chaque vague ayant plus d'ennemis à éliminer.

Configuration de l'IA ennemie

Ci-dessous, j'ai créé 2 variantes du cube (celle de gauche est pour l'instance vivante et celle de droite apparaîtra une fois l'ennemi tué):

  • Ajouter un composant Rigidbody aux instances mortes et vivantes
  • Enregistrez l'instance morte dans Prefab et supprimez-la de Scene.

Maintenant, l'instance vivante aura besoin de quelques composants supplémentaires pour pouvoir naviguer dans le niveau du jeu et infliger des dégâts au joueur.

  • Créez un nouveau script et nommez-le "SC_NPCEnemy" puis collez-y le code ci-dessous:

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);
        }
    }
}
  • Créez un nouveau script, nommez-le "SC_EnemySpawner" puis collez-y le code ci-dessous:

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++;
        }
    }
}
  • Créez un nouveau script, nommez-le "SC_DamageReceiver" puis collez-y le code ci-dessous:

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;
        }
    }
}
  • Attachez le script SC_NPCEnemy à l'instance ennemie vivante (vous remarquerez qu'il a ajouté un autre composant appelé NavMesh Agent, qui est nécessaire pour naviguer dans le NavMesh)
  • Attribuez le préfabriqué d'instance morte récemment créé à la variable Npc Dead Prefab
  • Pour le Fire Point, créez un nouveau GameObject, déplacez-le à l'intérieur de l'instance ennemie vivante et placez-le légèrement devant l'instance, puis affectez-le à la variable Fire Point:

  • Enfin, enregistrez l'instance vivante dans Prefab et supprimez-la de Scene.

Configuration du générateur d'ennemis

Passons maintenant à SC_EnemySpawner. Ce script fera apparaître des ennemis par vagues et affichera également des informations sur l'interface utilisateur à l'écran, telles que les PV du joueur, les munitions actuelles, le nombre d'ennemis restants dans une vague actuelle, etc.

  • Créez un nouveau GameObject et nommez-le "_EnemySpawner"
  • Attachez-lui le script SC_EnemySpawner
  • Attribuez l'IA ennemie nouvellement créée à la variable Enemy Prefab
  • Attribuez la texture ci-dessous à la variable Crosshair Texture

  • Créez quelques nouveaux GameObjects et placez-les autour de la scène, puis affectez-les au tableau Spawn Points

Vous remarquerez qu'il reste une dernière variable à affecter qui est la variable Player.

  • Attacher le script SC_DamageReceiver à une instance Player
  • Remplacez la balise d'instance de joueur par "Player"
  • Affectez les variables Player Controller et Weapon Manager dans SC_DamageReceiver

  • Attribuez une instance Player à une variable Player dans SC_EnemySpawner

Et enfin, nous devons intégrer le NavMesh dans notre scène afin que l'IA ennemie puisse naviguer.

N'oubliez pas non plus de marquer chaque objet statique de la scène comme navigation statique avant de cuire NavMesh:

  • Allez dans la fenêtre NavMesh (Window -> AI -> Navigation), cliquez sur l'onglet Bake puis cliquez sur le bouton Bake. Une fois le NavMesh cuit, il devrait ressembler à ceci:

Il est maintenant temps d'appuyer sur Play et de le tester:

Sharp Coder Lecteur vidéo

Tout fonctionne comme prévu !

Source
📁SimpleFPS.unitypackage4.61 MB
Articles suggérés
Comment faire une IA d'un cerf dans Unity
Implémentation de l'IA d'un ennemi dans Unity
Unity ajoute des ennemis à une plate-forme 2D
Créer un PNJ qui suit le joueur dans Unity
Travailler avec NavMeshAgent dans Unity
Examen du package Unity Asset Store - Zombie AI System
Comment créer un jeu de survie dans Unity