Articoli informativiImparare C#Imparare UnityPrincipianti

Struttura e organizzazione iniziale

In questo articolo cercherò di darvi dei consigli paratici su come organizzare il lavoro e la struttura degli scripts basilari.
Consigli che sono alla base dello sviluppo di un videogioco su Unity, così da non ritrovarsi con decine di problematiche in seguito.

Quali sono queste problematiche?

Sappiamo che al caricamento di una nuova scena tutti i gameObjects (e script annessi) verranno distrutti con la conseguente perdita dei dati.
E se volessimo mantenere delle variabili sempre aggiornate anche al cambio di scena...
O mantenere oggetti attivi per tutta la durata di gioco, anche con continui cambi di scena...

Alcuni banali esempi:
Mettete che volessimo contare il numero di nemici uccisi e mantenere questo contatore anche al caricamento di una nuova scena.

Oppure come potremmo mantenere il valore di energia del player al caricamento di una nuova scena?

Oppure, un problema comune è quello che deriva dal fatto che  su un prefab non è possibile impostare un oggetto sul suo inspector (tipo trascinare un oggetto della scena sul suo inspector).
Se per esempio volete istanziare i nemici in ogni scena o ogni tot di tempo... Ad ogni nemico istanziato dovrete "dirgli" chi è il nemico, per farglielo seguire, attaccare ecc... Ma se l'istanza proviene da un prefab, esso non potrà avere un link ad un oggetto nella scena.
Dovrete quindi andare a fare un "Find(Player)" per ogni nemico appena istanziato, cosa che a livello di performance potrebbe essere altamente deleterio, sopratutto in presenza di un certo numero di nemici. Non sarebbe più facile avere sempre presente il gameObject del Player, senza mai distruggerlo, per poter "dire" ad ogni script nemico: "segui il player" senza dovergli dire "prima trova il gameObject del player, poi seguilo"?
Oppure;
come detto più volte, ad ogni caricamento di scena ogni oggetto della scena precedente viene distrutto. E per oggetto si intende, sia gameObject che scripts annessi e conseguentemente anche i valori delle variabili. Player compreso.
Dunque ad ogni caricamento di scena dovremmo dire a tutti gli script del gioco "questo è il player". Poi dovremmo assegnargli l'energia che aveva nella scena precedente, lo stato generale, l'inventario, la sua posizione... cavolo, un sacco di roba da andare a riassegnare. 😯
Perché farlo, quando sappiamo che dovrebbe semplicemente rimanere come era un attimo prima, nello stato in cui era precedentemente al caricamento di una nuova scena?
Non faremmo prima semplicemente a non distruggere il Player e i suoi scripts al caricamento della nuova scena così che rimanga inalterato con tutti i sui parametri? E mantenere così un gameObject Player che venga assegnato solo all'avvio del gioco e non vanga mai più variato, senza il bisogno di andarlo a ricercare, riassegnare, ricollocare... con le sue variabili sempre assegnate, fino a che il gioco non verrà chiuso?

Avremmo bisogno di un GameObject/Script che rimanga in funzione per tutta la durata del gioco, dal suo avvio alla sua chiusura, tipicamente chiamato GameManager.

Un oggetto di questo tipo viene chiamato Singleton perché appunto singolo e unico, che verrà inizializzato una singola volta e a cui tutti gli altri scripts potranno fare sempre riferimento, in qualunque situazione del gioco/programma ci si trovi.

Il GameManager

La prima cosa da fare è creare un GameObject vuoto dove andremo ad attaccare un nuovo script chiamato GameManager. Chiamate anche lo stesso GameObject con lo stesso nome, così da averlo sempre sotto mano. In realtà potete chiamarlo come volete, ma lo standard da seguire sarebbe questo.


Questo script sarà il cuore del gioco, il "posto" in cui troverete tutte le variabili basilari del gioco, come il gameObject del Player, il suo punteggio, il suo rigidBody, la situazione globale del gioco e tutto ciò che vorrete che sia sempre accessibile in qualunque scena e situazione si trovi in quel momento il giocatore.
Il GameObject su cui sarà attaccato lo script GameManager sarà sempre presente in gerarchia, senza bisogno che esso venga inserito in tutte le scene, basta metterlo nella prima scena che viene caricata.

Questo script conterrà molte variabili statiche, ovvero tutte variabili uniche che non potranno avere istanze o valori diversi. Per esempio il gameObject Player sarà sempre uno e sempre quello, perchè ma di norma non potranno mai esserci due o più gameObject "Player" presenti nella scena.
Esempio:

 public class GameManager : MonoBehaviour {


        public static MyPlayerMovements myPlayerMovements; //Lo script che fa muovere il Player
        public static Rigidbody playerRigidBody; //Il RigidBody del Player
        public static GameObject playerGameObject; //Il GameObject del Player
        public static Camera activeCamera; //La telecamera del gioco
        public static int playerPoints; //Il punteggio del giocatore
        ......
        
        //Tutte variabili utili ed uniche, che potranno essere utilizzate 
        //anche in altri script durante lo svolgimento del gioco

NOTA:
In questo caso, essendo un esempio, ho ipotizzato che il programmatore abbia già il suo sistema per far muovere il Player e abbia chiamato lo script MyPlayerMovements.
In questo articolo si vuol evidenziare un sistema per “organizzare” gli scripts più importanti in un gioco e metterli “in un posto” dove siano sempre accessibili, per l’appunto, il GameManager.

Quello che ho fatto con la riga

public static MyPlayerMovements myPlayerMovements;

non è diverso dalle altre righe sottostanti dove ho dichiarato il RigidBody, la Camera ecc…
Solo che le classi RigidBody , Camera , GameObject ecc… esistono di natura tra le classi di Unity, mentre il MyPlayerMovements è una calsse ipotetica che fa muovere il player, che il programmatore dovrebbe aver creato da se.

Ora basterà cercare gli oggetti nell'Awake() dello script, così da fissarli nel GameManager e renderli disponibili ad ogni altro script del progetto.
Ricordiamoci che Awake() è il primissimo metodo che viene eseguito all'attivazione di uno script, prima del primo frame della scena.

 public class GameManager : MonoBehaviour {


        public static MyPlayerMovements myPlayerMovements; //Lo script che fa muovere il Player
        public static Rigidbody playerRigidBody; //Il RigidBody del Player
        public static GameObject playerGameObject; //Il GameObject del Player
        ......
        
        void Awake(){
            myPlayerMovements = FindObjectsOfType<MyPlayerMovements>(); //Trovo lo script del movimento
            playerGameObject = GameObject.Find("Player"); //Trovo il gameObject del Player
            playerRigidBody = playerGameObject.GetComponent<RigidBody>(); //Trovo il RigidBody del Player
            .......

In questo modo, se per esempio avrete uno script dei nemici che dovrà seguire un target (che tipicamente è il gameObject del Player), potrete richiamare tale gameObject andandolo a "prendere" dal GameManager dove è stato " trovato e fissato" all'inizio del gioco, senza andarlo a ricercare nuovamente, cosa che sappiamo utilizza molte risorse che potrebbero generare un "lag" nel framerate del gioco.

 GameManager.playerGameObject

Con questa riga avrete a disposizione il gameObject del Player.
A prescindere da ogni altra cosa, in qualunque scena vi troviate, senza dover andare a ricercare ogni volta il gameObject del Player  perché già salvato sulla variabile statica nel GameManager che sarà sempre presente e accessibile per tutto il gioco.

Se per esempio avete uno script per far inseguire un oggetto chiamato ipoteticamente "targetToFollow" potrete assegnargli il Player come tale.
Esempio:

    public class ClasseNemico : MonoBehaviour {
        
    void FollowPlayer()
    {
        
        targetToFollow=GameManager.playerGameObject;
        
        //Di seguito potrete inserire il vostro codice per far seguire il player
   ......
    }

A questo punto dovremmo fare in modo che il GamaManager sia sempre presente in ogni scena e sia sempre lo stesso, con le stesse variabili che non vengano distrutte al caricamento di una nuova scena.

Singleton e DontDestroyOnLoad, un'accoppiata vincente

Per fare questo si usa un Singleton, ovvero un sistema  che ha lo scopo di garantire che di una determinata classe venga creata una e una sola istanza, e di fornire un punto di accesso globale a tale istanza. Dunque ci sarà un solo oggetto di tipo GameManager per tutto il gioco e non ce ne potranno mai essere più di uno.

Un Singleton in combinazione con la funzione "DontDestroyOnLoad" di Unity permettono di mantenere un gameObject e tutti gli script ad esso attaccati, unici e presenti in tutte le scene, senza che essi vengano distrutti e ricreati al passaggio di scena, mantenendosi inalterati e senza che vengano mai più rieseguiti i metodi Awake o Start degli script, neanche al caricamento di una nuova scena.

 

Lo scopo dell'istruzione DontDestroyOnLoad è quello di rendere permanente e inalterato un gameObject e i suoi componenti anche al cambio di scena.

 

 

I metodi Awake e Start di questo script verranno eseguiti sempre e solo una sola volta, all'inizio del gioco, perché appunto, non ci sarà mai un "nuovo inizio" per questo script, ma esso sarà sempre presente e perseverante in tutto il gioco, a differenza di ogni altro script che viene distrutto e ricreato ad ogni inizio di scena.
Questo sarà molto utile per effettuare tutte quelle operazioni che vorremmo effettuare una sola volta e poi mai più, come la ricerca del Player, l'assegnazione di determinate variabili statiche e molte altre.

       public class GameManager : MonoBehaviour {

        public static GameManager instance; //Dichiaro un'istanza del GamaManager
        public static MyPlayerMovements myPlayerMovements;
        public static Rigidbody playerRigidBody;
        public static GameObject playerGameObject;
        public static Camera activeCamera;
        
        void Awake()
        {
            //Codice che definisce un Singleton
            
            //Nell'Awake, alla prima esecuzione del gioco, gli dico che:
            //se non esiste un'istanza di GameManager allora l'istanza è questo stesso script. 
            //mentre se l'istanza già esiste, cancella questo gameObject così da utilizzare il GameManager già presente      
            if (instance == null)
            {
            instance = this;
            DontDestroyOnLoad(transform.root.gameObject); //Con questa istruzione rendo "permanente" questo GameObject
            }
            else
            {
            Destroy(transform.root.gameObject);
            return; 
                    }
                    
            //Qui finisce il codice che definisce un Singleton
                    
           }

Riassumendo
Una cosa che dovrete sempre tenere presente è che ogni oggetto in una scena sarà distrutto nel momento in cui la scena stessa verrà distrutta (cioè ad ogni passaggio di scena) a meno che non si determini prima che un oggetto è da mantenere anche al passaggio di scena, con questa utile istruzione "DontDestroyOnLoad" .
l'istruzione "DontDestroyOnLoad" permette di mantenere un gameObject anche al passaggio tra le scene.

Noterete che al momento del "Play" gli oggetti che sono stati definiti "DontDestroyOnLoad", in Hierarchy saranno posti sotto un apposita sezione, così da rendere subito intuibile che essi sono in qualche modo "staccati" e indipendenti dalla scena in corso e fanno parte di una speciale di scena globale permanente.

5 pensieri su “Struttura e organizzazione iniziale

  1. Ho una domanda: già qualche sessione fa è successo che per dichiarare una variabile, anziché int, è stato scritto lo stesso nome della variabile. Ad esempio, qua scrivete “public static MyPlayerMovements myPlayerMovements”, alchè all’inizio pensai che fosse un errore, ma più sotto ho notato che invece, dopo static, scrivete int, esattamente quando scrivete “public static int playerPoints”. Avrei bisogno di un po’ di chiarezza perché questa cosa mi mette un sacco di confusione.

    1. Ciao, scusa per il ritardo con cui rispondo. Purtroppo il filtro anti-spam aveva bloccato molti messaggi, compreso il tuo.

      Per dichiarare una variabile si usa il tipo e il nome scelto per la variabile.
      int pippo;
      dove int è il tipo (numero intero) e pippo è il nome scelto da noi per quella variabile di tipo numero intero.
      Quando scrivo MyPlayerMovements myPlayerMovements le cose non sono diverse. MyPlayerMovements (con la M grande) è un tipo. Ma non un tipo standard di C# o di Unity, semplicemente è un tipo creato dal programmatore, in particolare è una classe.
      Scrivendo MyPlayerMovements myPlayerMovements; ho dichiarato un nuovo oggetto di tipo MyPlayerMovements e l’ho chiamato myPlayerMovements (con la m piccola) semplicemente perché è più facile da ricordare (è una prassi che si usa spesso quando si sa che si avrà solo quella variabile di quel tipo).
      Sapendo che poi non avrò altri oggetti di quel tipo, posso chiamarlo con un nome che mi riporti subito in mente di che tipo è quella variabile, ovvero il suo nome ma l’iniziale minuscola. Ma avrei potuto dichiarare quella variabile anche:
      MyPlayerMovements pippo;
      Non sarebbe cambiato nulla.

      1. perciò scusa (perchè mi sono fatto la stessa domanda di Riccardo anche io), scrivendo “public static MyPlayerMovements myPlayerMovements;” non hai semplicemente dichiarato una variabile, ma hai dichiarato la variabile myPlayerMovements all’interno dell’oggetto di classe “MyPlayerMovements” nella stessa riga?

        1. MyPlayerMovements è una classe che sta su un’altro file .cs.
          Dichiarandolo all’interno del GameManager ho creato un nuovo oggetto di quella classe.

          Quello che ho fatto con quella riga non è diverso dalle altre righe sottostanti dove ho dichiarato il RigidBody, la Camera ecc…
          Solo che le classi RigidBody , Camera , GameObject ecc… esistono di natura tra le classi di Unity, mentre il MyPlayerMovements è una clsse ipotetica che fa muovere il player, che il programmatore dovrebbe aver creato da se.

          In questo caso, essendo un esempio, ho ipotizzato che il programmatore abbia già il suo sistema per far muovere il Player e abbia chiamato lo script MyPlayerMovements.
          In questo articolo si vuol evidenziare un sistema per “organizzare” gli scripts più importanti in un gioco e metterli “in un posto” dove siano sempre accessibili, per l’appunto, il GameManager.

          Con quella riga ho dichiarato un nuovo oggetto di tipo MyPlayerMovements. E l’ho chiamto myPlayerMovements.
          Questo oggetto l’ho dichiarato all’interno della classe GameManager e resto statico così che sia accessibile con la riga GameManager.myPlayerMovements.

          Come se avessi dichiarato un nuovo oggetto di tipo GameObject con:
          GameObject gameObject; Che poi sia static, public o altro non ha importanza. Quelli sono modificatori di accesso.

          Spero di essere stato più chiaro. Se hai ancora dubbi fammelo sapere che cercherò di spiegarmi meglio. 😉
          Intanto ho modificato un po’ l’articolo inserendo anche la spiegazione che ti ho appena dato 🙂

    2. Allora:
      MyPlayerMovements è un tipo. Così come int.
      Solo che int è un tipo di dato proprietario di Unity (in realtà del nameSpace System) mentre MyPlayerMovements è una classe creata dal programmatore.

      MyPlayerMovements pippo;
      int paperino;

      In questo modoo ho dichiarato due variabili, una di tipo MyPlayerMovements e una di tipo Int. La prima si chiama pippo e la seconda paperino.

      Dunque, MyPlayerMovements è un tipo che identifica una classe creata dal programmatore.
      Ma non si differenzia dalla dichiarazione di una variabile di tipo int.

      Per comodità, visto che di solito la classe che identifica il movimento del giocatore è solo una e non ne esisteranno altre all’interno del gioco, per convenzione si usa lo stesso nome della classe, solo con la lettera piccola.

      MyPlayerMovements myPlayerMovements ;

      ma come detto poteva chiamarsii anche in altro modo, a discrezione del programmatore.
      per esempio:
      MyPlayerMovements movimentiGiocatore;
      ecc….
      fatto sta che MyPlayerMovements è il tipo e, in questo caso movimentiGiocatore è il nome della variabile che abbiamo appena dichiarato (per l’appunto, di tipo MyPlayerMovements )

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *