Hur man gör en FPS med AI-stödet i Unity

First-person shooter (FPS) är en undergenre av skjutspel där spelaren styrs från ett förstapersonsperspektiv.

För att göra ett FPS-spel i Unity behöver vi en spelarkontroll, en mängd föremål (vapen i det här fallet) och fienderna.

Steg 1: Skapa Player Controller

Här kommer vi att skapa en kontroller som kommer att användas av vår spelare.

  • Skapa ett nytt spelobjekt (Spelobjekt -> Skapa tomt) och namnge det "Player"
  • Skapa en ny kapsel (spelobjekt -> 3D-objekt -> kapsel) och flytta den inuti "Player"-objektet
  • Ta bort Capsule Collider-komponenten från Capsule och ändra dess position till (0, 1, 0)
  • Flytta huvudkameran inuti "Player"-objektet och ändra dess position till (0, 1.64, 0)
  • Skapa ett nytt skript, namnge det "SC_CharacterController" och klistra in koden nedan i det:

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);
        }
    }
}
  • Bifoga SC_CharacterController-skript till "Player"-objektet (Du kommer att märka att det också lade till en annan komponent som heter Character Controller, och ändrar dess mittvärde till (0, 1, 0))
  • Tilldela huvudkameran till variabeln Player Camera i SC_CharacterController

Spelarkontrollen är nu klar:

Steg 2: Skapa vapensystemet

Spelarens vapensystem kommer att bestå av 3 komponenter: en vapenhanterare, ett vapenskript och ett kulmanus.

  • Skapa ett nytt skript, namnge det "SC_WeaponManager" och klistra in koden nedan i det:

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;
        }
    }
}
  • Skapa ett nytt skript, döp det till "SC_Weapon" och klistra in koden nedan i det:

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);
    }
}
  • Skapa ett nytt skript, döp det till "SC_Bullet" och klistra in koden nedan i det:

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 kommer du att märka att SC_Bullet-skriptet har några fel. Det beror på att vi har en sista sak att göra, som är att definiera IEntity-gränssnittet.

Gränssnitt i C# är användbara när du behöver se till att skriptet som använder det har vissa metoder implementerade.

IEntity-gränssnittet kommer att ha en metod som är ApplyDamage, som senare kommer att användas för att skada fiender och vår spelare.

  • Skapa ett nytt skript, döp det till "SC_InterfaceManager" och klistra in koden nedan i det:

SC_InterfaceManager.cs

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

Konfigurera en vapenhanterare

En vapenhanterare är ett objekt som kommer att finnas under huvudkameraobjektet och som kommer att innehålla alla vapen.

  • Skapa ett nytt GameObject och namnge det "WeaponManager"
  • Flytta WeaponManager inuti spelarens huvudkamera och ändra dess position till (0, 0, 0)
  • Bifoga SC_WeaponManager-skript till "WeaponManager"
  • Tilldela huvudkameran till variabeln Player Camera i SC_WeaponManager

Sätta upp ett gevär

  • Dra och släpp din vapenmodell i scenen (eller skapa helt enkelt en kub och sträck ut den om du inte har en modell ännu).
  • Skala modellen så att dess storlek är relativt en spelarkapsel

I mitt fall kommer jag att använda en skräddarsydd Rifle-modell (BERGARA BA13):

BERGARA BA13

  • Skapa ett nytt GameObject och namnge det "Rifle" och flytta sedan gevärsmodellen inuti det
  • Flytta "Rifle"-objektet inuti "WeaponManager"-objektet och placera det framför kameran så här:

Åtgärda problemet med kameraklippning i Unity.

För att fixa objektklippningen ändrar du helt enkelt kamerans nära klippningsplan till något mindre (i mitt fall ställer jag in det till 0,15):

BERGARA BA13

Mycket bättre.

  • Bifoga SC_Weapon-skript till ett gevärsobjekt (du kommer att märka att det också lade till en ljudkälla-komponent, detta behövs för att spela elden och ladda om ljudet).

Som du kan se har SC_Weapon 4 variabler att tilldela. Du kan tilldela Fire-ljud- och Reload-ljudvariabler direkt om du har lämpliga ljudklipp i ditt projekt.

Bullet Prefab-variabeln kommer att förklaras senare i denna handledning.

För nu kommer vi bara att tilldela Fire point-variabeln:

  • Skapa ett nytt GameObject, byt namn på det till "FirePoint" och flytta det inuti Rifle Object. Placera den precis framför pipan eller lite innanför, så här:

  • Tilldela FirePoint Transform till en Fire Point-variabel vid SC_Weapon
  • Tilldela gevär till en sekundär vapenvariabel i SC_WeaponManager-skriptet

Ställa in en maskinpistol

  • Duplicera gevärsobjektet och döp om det till Submachinegun
  • Byt ut pistolmodellen inuti den med en annan modell (i mitt fall kommer jag att använda den skräddarsydda modellen av TAVOR X95)

TAVOR X95

  • Flytta Fire Point-transformeringen tills den passar den nya modellen

Vapen Fire Point-objektuppsättning i Unity.

  • Tilldela Submachinegun till en primär vapenvariabel i SC_WeaponManager-skriptet

Konfigurera en Prefab för punkt

Prefab för kulor kommer att skapas enligt ett vapens eldhastighet och kommer att använda Raycast för att upptäcka om den träffar något och orsakar skada.

  • Skapa ett nytt GameObject och namnge det "Bullet"
  • Lägg till Trail Renderer-komponenten till den och ändra dess tidsvariabel till 0.1.
  • Ställ in breddkurvan på ett lägre värde (t.ex. Start 0.1 slut 0) för att lägga till ett spår som är spetsigt
  • Skapa nytt material och döp det till bullet_trail_material och ändra dess Shader till Partiklar/Additiv
  • Tilldela ett nyskapat material till en Trail Renderer
  • Ändra färgen på Trail Renderer till något annat (ex. Start: Bright Orange End: Darker Orange)

  • Spara kulobjektet i Prefab och ta bort det från scenen.
  • Tilldela en nyskapad Prefab (dra och släpp från projektvyn) till Rifle and Submachinegun Bullet Prefab variabel

Kulsprutepistol:

Gevär:

Vapnen är nu klara.

Steg 3: Skapa fiendens AI

Fienderna kommer att vara enkla kuber som följer spelaren och attackerar när de är tillräckligt nära. De kommer att attackera i vågor, där varje våg har fler fiender att eliminera.

Konfigurera Enemy AI

Nedan har jag skapat 2 varianter av kuben (den vänstra är för den levande instansen och den högra kommer att skapas när fienden har dödats):

  • Lägg till en Rigidbody-komponent till både döda och levande instanser
  • Spara den döda instansen till Prefab och ta bort den från scenen.

Nu kommer den levande instansen att behöva ytterligare ett par komponenter för att kunna navigera på spelnivån och skada spelaren.

  • Skapa ett nytt skript och namnge det "SC_NPCEnemy" och klistra sedan in koden nedan i det:

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);
        }
    }
}
  • Skapa ett nytt skript, namnge det "SC_EnemySpawner" och klistra sedan in koden nedan i det:

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++;
        }
    }
}
  • Skapa ett nytt skript, namnge det "SC_DamageReceiver" och klistra sedan in koden nedan i det:

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;
        }
    }
}
  • Bifoga SC_NPCEnemy-skript till levande fiendeinstans (du kommer att märka att det lades till en annan komponent som heter NavMesh Agent, som behövs för att navigera i NavMesh)
  • Tilldela den nyligen skapade prefab för döda instanser till variabeln Npc Dead Prefab
  • För Fire Point, skapa ett nytt GameObject, flytta det inuti den levande fiendens instans och placera det något framför instansen, tilldela det sedan till Fire Point-variabeln:

  • Slutligen, spara den levande instansen till Prefab och ta bort den från scenen.

Konfigurera enemy spawner

Låt oss nu gå till SC_EnemySpawner. Det här skriptet kommer att skapa fiender i vågor och kommer även att visa lite UI-information på skärmen, såsom Player HP, aktuell ammunition, hur många fiender som finns kvar i en aktuell våg, etc.

  • Skapa ett nytt GameObject och namnge det "_EnemySpawner"
  • Bifoga SC_EnemySpawner-skriptet till det
  • Tilldela den nyskapade fiendens AI till variabeln Enemy Prefab
  • Tilldela texturen nedan till Crosshair Texture-variabeln

  • Skapa ett par nya GameObjects och placera dem runt scenen och tilldela dem sedan till Spawn Points-arrayen

Du kommer att märka att det finns en sista variabel kvar att tilldela som är Player-variabeln.

  • Bifoga SC_DamageReceiver-skript till en Player-instans
  • Ändra Player-instanstaggen till "Player"
  • Tilldela Player Controller och Weapon Manager variabler i SC_DamageReceiver

  • Tilldela Player-instans till en Player-variabel i SC_EnemySpawner

Och slutligen måste vi baka NavMesh i vår scen så att fiendens AI kommer att kunna navigera.

Glöm inte heller att markera varje statiskt objekt i scenen som navigeringsstatiskt innan du bakar NavMesh:

  • Gå till NavMesh-fönstret (Fönster -> AI -> Navigation), klicka på Bake-fliken och klicka sedan på Bake-knappen. Efter att NavMesh har bakats bör det se ut ungefär så här:

Nu är det dags att trycka på Play och testa det:

Sharp Coder Videospelare

Allt fungerar som förväntat!

Källa
📁SimpleFPS.unitypackage4.61 MB
Föreslagna artiklar
Hur man gör en AI av en hjort i enhet
Implementering av AI av en fiende i Unity
Unity lägger till fiender till en 2D-plattformsspelare
Skapa en NPC som följer spelaren i Unity
Arbeta med NavMeshAgent i Unity
Granskning av Unity Asset Store-paketet - Zombie AI System
Hur man gör ett överlevnadsspel i Unity