Imparare C#Imparare UnityTips

Sparare con un’arma

Abbiamo già visto come instanziare un oggetto (prefab) alla pressione di un tasto sull’articolo riguardante i prefabs

using UnityEngine;

public class Player : MonoBehaviour {

    public GameObject PrefabOriginale;




	void Spara () {
        //Istanzia l'oggetto proiettile, creando una copia del PrefabOriginale impostando la posizione e la rotazione
        GameObject proiettile = GameObject.Instantiate(PrefabOriginale,transform.position,Quaternion.identity);
	}


    void Update()
    {
        // Alla pressione di Fire1, esegui il metodo Spara()
        if (Input.GetButtonDown("Fire1"))
        {
            Spara();
        }




    }



}

Questa funzione ci può tornare utile per creare un sistema di “fuoco” di un’arma. Il codice visto in precedenza (sull’articolo riguardante i prefabs) è però troppo generico e rudimentale perché possa essere considerato realmente utilizzabile se non per un tipo di sparo singolo.

Ora creeremo un sistema che istanzierà un proiettile alla pressione del tasto “Fire1″ e faremo in modo che tenendo premuto il tasto la nostra arma sparerà di continuo, colpo dopo colpo, con una cedenza da noi stabilita.
Ma non solo… faremo in modo anche che il nostro caricatore abbia un numero limitato di colpi e ogni raffica effettuata tenendo premuto “Fire1” sia così contenuta entro un certo numero di proiettili dopo i quali bisognerà rilasciare il pulsante per rendere possibile la raffica successiva. Simuleremo così il riscaldamento dell’arma o se preferite un effetto “UZI” a raffica controllata.
Potremmo poi cambiare i valori a nostro piacimento per simulare qualsiasi tipo di arma, ad un colpo singolo, a raffica controllata o a raffica continua.

Teniamo sempre presente la differenza tra GetButton e GetButtonDown.


Iniziamo creando lo script che gestisce le munizioni dell’arma che chiamerò ArmaControl.

Sarebbe buona usanza usare sempre nomi di variabili in inglese, ma noi, visto che siamo qui per imparare, solo per scopo didattico e per una comprensione più immediata, useremo anche qualche parola in italiano.

Questo script è da posizionarsi su una singola arma, così da poter creare anche diverse tipologie di armi, ognuna con un fire-rate differente.

using UnityEngine;


public class ArmaControl : MonoBehaviour
{
    public float fireRate = 0.5f; //Tempo tra un colpo e l'altro   
    public bool recargeOnMouseUp=false; //Se true, quando si rilascia il tasto di fuoco ricarica la raffica senza attesa
    public int maxAmmoRaffica = 5; //Colpi di una singola raffica
    public float pauseTime; //Tempo di attesa tra una raffica e l'altra
    public int ammoQuantity = 20; //Quantità di munizioni totali


    int ammoRaffica; //Munizioni attuali
    float nextFire; //Prossimo colpo pronto
    float pauseTimer; //Il timer della pausa
    float cooldown; //Cantatore di colpi
    bool pause =false; //In pausa




     void Start()
    {
        ammoRaffica = maxAmmoRaffica; //All'inizio il caricatore è pieno
    }




    void Update()
    {
        if (!pause && ammoQuantity>0) //Se è tra una raffica e l'altra non sparare
        {
            if (fireRate == 0 && Input.GetButtonDown("Fire1"))
            {   //Il primo colpo lo effettua senza dover attendere nulla
                Shoot();
            }
            else
            {
                if (Input.GetButton("Fire1") && Time.time > nextFire && fireRate > 0) //Pronto per sparare
                {
                    //Colpi successivi, tenendo premuto il pulsante
                    if (ammoRaffica > 0)
                    {   //Se ci sono munizioni
                        nextFire = Time.time + fireRate;
                        Shoot(); //Esegue la funzione di "sparo"

                    }
                    if (ammoRaffica == 0)
                    {   //Se non ci sono più munizioni
                        if (cooldown > Time.time)
                        {   //Se non è passato il tempo giusto
                            cooldown = Time.time + fireRate;
                        }
                    }
                }
            }

            if (Time.time > cooldown && ammoRaffica == 0)
            {  //Se il tempo di recupero (cooldown) è finito e le munizioni sono terminate
                pause = true;
                
            }
        }

   
        //Se si è scelto di far ricaricare al rilascio del tasto
       if(recargeOnMouseUp && Input.GetButtonUp("Fire1")){
            pauseTimer = 0;
            pause = false;


            if (ammoQuantity >= maxAmmoRaffica)//Se ci sono abbastanza i proiettili
                ammoRaffica = maxAmmoRaffica; //Ricarico per la prossima raffica
            else
                ammoRaffica = ammoQuantity; //Se non sono i proiettili metti quelli disponibili
        }

           //Se in pausa, fai il conteggio del tempo tra una raffica e l'altra
            if (pause)
            ShootPause();

    } //Fine di Update()


    //Metodo che fa il conteggio del tempo tra una scarica e l'altra
    void ShootPause()
    {
        pauseTimer += Time.deltaTime;
        if (pauseTimer >= pauseTime)
        {
            pauseTimer = 0;
            pause = false;


            if(ammoQuantity>= maxAmmoRaffica)//Se ci sono abbastanza i proiettili
            ammoRaffica = maxAmmoRaffica; //Ricarico per la prossima raffica
            else
            ammoRaffica = ammoQuantity; //Se non sono i proiettili metti quelli disponibili
        }
    }



    void Shoot() {

        ammoRaffica--; //Rimuove un colpo della raffica ad ogni sparo
        ammoQuantity--; //Rimuove un colpo dalla quantità in possesso

        print("Shooooot");//Spara!
    }


 

} //Chiusura classe




A questo punto abbiamo uno script che ci permette di settare diversi parametri.
Se per esempio non vogliamo il “sistema a scariche”, basterà impostare su 0 il pauseTime.
Abbiamo inserito anche un parametro (ammoQuantity) per gestire il numero di munizioni a disposizione del giocatore, terminate le quali non sarà più possibile sparare.


Fino a qui abbiamo creato la gestione dei colpi con cui poter creare diversi tipi di “reazione” alla pressione del nostro “grilletto”.
Il metodo Spara() fin’ora non fa altro che scrivere in console la stringa Shooooot che potremmo contare per verificare che tutto funzioni correttamente.

Ora lavoriamo sul metodo Spara() per creare il nostro proiettile ed anche un effetto “fiammata” sul punto d’uscita della canna della nostra arma.
Per fare le cose per bene andremo anche ad inserire un suono che si esegue al momento dello sparo.

Dovremmo prima di tutto inserire la variabile del prefab che identifica il proiettile, poi dovremo identificare il punto di creazione del proiettile che sarà lo stesso del punto di creazione dell’ effetto fiamma e del suono.

Per fare tutto questo si potrebbero percorrere tante strade differenti, noi useremo quella che secondo me è quella più utile a scopo didattico.

1. Creiamo un arma (Arma) , nel nostro esempio un semplice parallelepipedo.
2. Creaimo un oggetto che funga da proiettile (Proiettile) , nel nostro caso un semplice cubo. Il gameObject Proiettile non deve essere presente nella scena, ma solo in una cartella del progetto, servirà come prefab da istanziare nel momento dello sparo.
3. Creiamo un gameObject vuoto (ShotPoint) e lo mettiamo come children al gameObject dell’arma, posizionandolo là dove vorremmo far “uscire” il proiettile, in cima alla “canna” dell’arma. ShotPoint sarà anche il punto in cui verrà visualizzato l’effetto “lampo/fiammata” dell’arma.

Se vogliamo aggiungere anche un effetto “lampo/fiammata” allo sparo, creiamo un Particle System e usiamolo come prefab da generare insieme al proiettile.
Ricordiamoci di impostare su la sua Stop Action su “destroy” in modo che l’effetto sia automaticamente distrutto una volta completato (le impoostazioni del Particle System le potete vedere scaricando il package del tutorial).

NOTA SULLE PRESTAZIONI:
Come desctitto nei commenti, avremmo anche potuto usare una tecnica più performante per generare l’effetto “fiammata”. Nel sistema usato ora c’è bisogno di un’ulteriore istanziamento, oltre che del proiettile, anche del Particle System. Notoriamente, gli istanziamenti sono la morte delle prestazioni, dunque dove possibile sarebbe meglio evitarli.
Si potrebbe per esempio creare un unico Particle System posto sulla punta della canna e farlo “emettere” nel momento dello sparo. Oppure dotare il proiettile di un Particle System che verrà rilasciato nel momento dello sparo… Le tecniche sono tante.

Non solo l’istanzaimento del Particle System potrebbe essere evitato… ma persino quello dei proiettili!
Vedremo in seguito un sistema chiamato genericamente “pool system”, ovvero una tecnica che riutilizza sempre gli stessi proiettili, nascondendoli e riattivandoli (riposizionandoli nel punto dello sparo) nel momento più opportuno, senza doverli istanziare e distruggere tutte le volte, cosa molto dispendiosa in termini di performance.

using UnityEngine;


public class ArmaControl : MonoBehaviour
{

    //Variabili pubbliche
    public float fireRate = 0.5f; //Tempo tra un colpo e l'altro   
    public bool recargeOnMouseUp=false; //Se true, quando si rilascia il tasto di fuoco ricarica la raffica senza attesa
    public int maxAmmoRaffica = 5; //Colpi di una singola raffica
    public float pauseTime; //Tempo di attesa tra una raffica e l'altra
    public int ammoQuantity = 20; //Quantità di munizioni totali
    public GameObject proiettilePrefab;//Il prefab del proiettile
    public GameObject flashEffectPrefab;//Il prefab dell'effetto flash/fiammata
    public Transform shotPoint; //Il trasform che identifica il punto di creazione dei proiettili
    public AudioClip shotSound; //Il suono dello sparo



    //Variabili private
    int ammoRaffica; //Munizioni attuali
    float nextFire; //Prossimo colpo pronto
    float pauseTimer; //Il timer della pausa
    float cooldown; //Cantatore di colpi
    bool pause =false; //In pausa
    AudioSource audioSource; //L'audioSource che eseguirà il suono



    void Start()
    {
           AudioSourceCreation();//Crea l'AudioSource (se non presente)
           ammoRaffica = maxAmmoRaffica; //All'inizio il caricatore è pieno
    }

    //Creiamo l'AudioSource a runtime, senza doverlo inserire manualmente
    void AudioSourceCreation() {
        //Se non è stato creato manualmente
        if (!GetComponent())
        {
            audioSource = gameObject.AddComponent(); //Crea l'AdudioSource
        }
        else
        {
            audioSource = GetComponent(); //Se già esisteva usa quello esistente
        }

        //Notare che si può creare l'audioSource anche manualmente, in modo da impostare i parametri 
        //i parametri (come il volume, il pitch ed altri) direttamente da Inspector
        //In alternativa potete anche impostarli qui, nel momento della creazione, scegliendo i parametri 
        //a runtime (audioSource.volume=1,  audioSource.pitch=1, ecc.)

    }



    void Update()
    {
        if (!pause && ammoQuantity>0) //Se è tra una raffica e l'altra non sparare
        {
            if (fireRate == 0 && Input.GetButtonDown("Fire1"))
            {   //Il primo colpo lo effettua senza dover attendere nulla
                Shoot();
            }
            else
            {
                if (Input.GetButton("Fire1") && Time.time > nextFire && fireRate > 0) //Pronto per sparare
                {
                    //Colpi successivi, tenendo premuto il pulsante
                    if (ammoRaffica > 0)
                    {   //Se ci sono munizioni
                        nextFire = Time.time + fireRate;
                        Shoot(); //Esegue la funzione di "sparo"

                    }
                    if (ammoRaffica == 0)
                    {   //Se non ci sono più munizioni
                        if (cooldown > Time.time)
                        {   //Se non è passato il tempo giusto
                            cooldown = Time.time + fireRate;
                        }
                    }
                }
            }

            if (Time.time > cooldown && ammoRaffica == 0)
            {  //Se il tempo di recupero (cooldown) è finito e le munizioni sono terminate
                pause = true;
                
            }
        }

   
        //Se si è scelto di far ricaricare al rilascio del tasto
       if(recargeOnMouseUp && Input.GetButtonUp("Fire1")){
            pauseTimer = 0;
            pause = false;


            if (ammoQuantity >= maxAmmoRaffica)//Se ci sono abbastanza i proiettili
                ammoRaffica = maxAmmoRaffica; //Ricarico per la prossima raffica
            else
                ammoRaffica = ammoQuantity; //Se non sono i proiettili metti quelli disponibili
        }

           //Se in pausa, fai il conteggio del tempo tra una raffica e l'altra
            if (pause)
            ShootPause();

    } //Fine di Update()


    //Metodo che fa il conteggio del tempo tra una raffica e l'altra
    void ShootPause()
    {
        pauseTimer += Time.deltaTime;
        if (pauseTimer >= pauseTime)
        {
            pauseTimer = 0;
            pause = false;


            if(ammoQuantity>= maxAmmoRaffica)//Se ci sono abbastanza i proiettili
            ammoRaffica = maxAmmoRaffica; //Ricarico per la prossima raffica
            else
            ammoRaffica = ammoQuantity; //Se non sono i proiettili metti quelli disponibili
        }
    }



    void Shoot() {

        ammoRaffica--; //Rimuove un colpo della raffica ad ogni sparo
        ammoQuantity--; //Rimuove un colpo dalla quantità in possesso

        //Istanzia l'oggetto proiettile, creando una copia del proiettilePrefab impostando la posizione e la rotazione
        GameObject proiettile = Instantiate(proiettilePrefab, shotPoint.position, shotPoint.rotation);
        //Notare che impostiamo la posizione di origine e la rotazione inziale uguali a quelle del trasform shotPoint 

        //Secondo istanziamento di un gameObject, necessario per l'effetto "fiammata"
        GameObject flashEffect = Instantiate(flashEffectPrefab, shotPoint.position, shotPoint.rotation); 
        //Notare che si potrebbe usare anche un'altra tecnica, meno dispendiosa in termini di prestazioni
        //che vedremo in seguito


        if (shotSound)//Assicuriamoci che c'è un suono scelto per il rumore dello sparo
        audioSource.PlayOneShot(shotSound); //Esegui il suono


        print("Shooooot");//Spara!
    }


 

} //Chiusura classe




A questo punto sta a voi dare dare velocità al proiettile, altrimenti tutti i proiettili saranno generati nel punto “shotPoint” senza muoversi, accumulandosi in cima alla canno dell’arma.
Per fare questo dovrete dotare il prefab proiettile di uno script apposito, che lo faccia muovere appena generato e che lo faccia autodistruggere dopo un tot di tempo, altre che ovviamente ad un sistema di collisione che rilevi quando il proiettile va a sengno con un nemico o altro.
Potremmo dare forza al proiettile anche direttamente dallo script dell’arma, ma è buona norma dividere le cose in modo da poter gestire i proiettili individualmente.
Questo argomento è influenzato da molti aspetti soggettivi che dipendono da come si vuol far interagire i proiettili con in mondo circostante e con i nemici. Esistono diverse tecniche per muovere gli oggetti, dotandoli di rigidBody o muovendoli da Transform. 

using UnityEngine;

public class Proiettile : MonoBehaviour {

    public Rigidbody rig;
    public float speed=100f;

    void Start () {
		
	}
	

	void Update () {
        rig.AddForce(transform.forward* speed);
	}
}

 Nel package del tutorial troverete un sistema molto semplice per muovere i proiettili, a voi modificarlo ed espanderlo a vostro piacimento.

Tutorial Package: Arma Tutorial.


7 pensieri su “Sparare con un’arma

  1. Cavolo, sei il migliore!
    Se non era per un puro caso non avrei mai trovato questo portale. Dovresti pubblicizzarlo perchè in italiano è di sicuro il migliore che si puó trovare. E forse anche di quelli in inglese.
    Grazie! 😉

  2. Ciao 🙂 Volevo sapere se oltre al metodo ‘istantiate’ per proiettili e particles, ce ne fosse un altro meno dispendioso in termini di prestazioni.
    Grazie in anticipo.

    1. Ciao, in presenza di molte “istantiate” (tipo una raffica di colpi), è buona norma usare un Pool System , ovvero un sistema che istanzia un certo numero di oggetti tutti al primo frame di apertura della scena, in modo di non avere altri Instantiate e Destroy durante il gamplay. In questo modo si istanzierebbero un certo numero di proiettili solo allo Start, così che che poi si potranno usare all’occorrenza. E’ anche possibile inserirli direttamente nella scena senza mai usare Instantiate, come se fosserò realmente presenti nel caricatore dell’arma.
      Quando l’arma dovrà sparare, al posto di istanziare un nuovo proiettile, si potrà attivare e riposizionare uno dei proiettili già presenti in scena, riportandolo nel punto di partenza come se fosse un nuovo proiettile. Allo stesso modo, invece di distruggerlo, si potrà disattivare, così da essere pronto per essere riutilizzato di nuovo.
      In pratica è una specie di reciclo degli stessi oggetti, senza mai distruggerli per poi doverli istanzarne di nuovo.

  3. Ciao nello script c’è questa riga:
    if (!pause && ammoQuantity>0)
    Forse mi sono perso qualcosa ma !pause non capisco cosa voglia dire, so che ! Indica un confronto contrario ma in questo caso non capisco proprio. Ti sarei grato se potessi spiegarmelo grazie mille

    1. Ciao.
      nello script pause è un bool, cioè può essere vero o falso. Se pause è vero vuol dire che ì in corso una pausa, se è falso (!pause) la pausa non è attiva.
      Nella riga da te citata dico “Se pause è falso e la quantità di munizioni è superiore a zero…”

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.