Archivio ScriptsImparare C#Imparare UnityPrincipiantiTips

Creare un menu di gioco

In questo articolo andremo a capire come creare un tipico menu di gioco.
La parole chiave è come sempre capire e non solo seguire le istruzioni dalla A alla Z per ottenere il risultato voluto, ma comprendere il funzionamento dello strumento che abbiamo sotto mano.

Tenterò di rendere questo articolo il più completo che possiate trovare sulla creazione di un menu di gioco.

Questo tutorial sarà diviso in due parti.
Nella prima parte andremo a vedere come inserire i pulsanti, passando per la creazione di un'animazione di comparsa/scomparsa, l'uso e la spiegazione dei componenti di Layout ecc... fin alla gestione di permanenza del menu per tutta la durata del gioco anche dopo un cambio di scena...

Nella seconda parte approfodiremo alcune funzioni dei pulsanti, come la comparsa di un menu d'opzioni compreso di sliders per il setting dell'audio.

 

 

 

Esempio di menu creato per Star Shift

Cosa faremo

Andremo a creare una UI dove inseriremo alcuni pulsanti, tipicamente i pulsanti presenti in un menù sono:
"Nuova Partita", "Carica Partita", "Opzioni" e "Chiudi il Gioco".
Fare questo però sarebbe davvero troppo riduttivo e quasi inutile nella sua funzione se non come menu iniziale.
Noi vogliamo anche rendere questo menu sempre disponibile nel gioco (in qualunque scena ci troveremo) e richiamabile tramite il tasto "ESC" anche durante lo svolgimento del gioco. A prescindere dalla situazione in cui ci si trova, il gioco andrà in pausa e si aprirà il menu.

Sappiamo che gli oggetti vengono distrutti durante un cambio di scena e di conseguenza, se non apportassimo qualche accorgimento specifico per evitarlo, dovremmo dotare ogni scena dello stesso menu con conseguenze del tutto imprevedibili e spesso ingestibili in modo corretto.

La procedura corretta è:
- Avere il menu nella prima scena del gioco.
- Renderlo invisibile (nascosto e inattivo) durante le sessioni di gioco.
- Fare in modo che il menu sia permanente e unico durante tutta la durata del gioco, senza duplicati.
- Renderlo visibile ed accessibile tramite la pressione del tasto ESC.
- Fare in modo che il gioco vada in pausa quando il menu è aperto.
- Siccome siamo perfezionisti; creare un'animazione di comparsa del menu.

 

Come funzionano le UI in Unity

Cosa sono le UI e come funzionano in Unity?
UI sta per User Interface. Tipicamente rappresentano elementi 2D come menu, pulsanti, immagini, testi, finestre con elementi al loro interno ecc..

Per saperne di più vi invito a leggere l'apposito articolo sulle UI di Unity.

Nell'articolo su citato vengono spiegati nel dettaglio elementi come i RectTrasform, le Mask (maschere) e l'elemento principale di una UI, le Canvas.

In questo tutorial partirò da una scena vuota

Dato per scontato che abbiate letto l'articolo su sulle UI di Unity, come primo passaggio andremo a creare un pannello.
Potremmo poi renderlo trasparente per mostrare la scena sottostante oppure scegliere un'immagine che funga da sfondo al menu.
Dal menu di Unity, andiamo su GameObject per creare un nuovo gameObject e su UI per creare il nostro primo pannello (Panel).
Come avrete intuito, tutti gli elementi di una UI sono gameObjects a tutti gli effetti.

 

Facendo questa operazione avremo saltato la creazione del Canvas (tela) che verrà generata automaticamente per contenere il Panel (pannello) e in seguito anche tutti gli altri elementi del menu che andremo ad aggiungere.
Il alternativa avremmo potuto, prima creare una Canvas e poi il pannello al suo interno.


Cavas è il contenitore di tutti gli elementi di una UI, che siano esse immagini, pannelli invisibili, pulsanti, finestre, testi, ecc...

Facciamo attenzione che il metodo di visualizzazione della Canvas sia Screen Space - Overlay.

Se volete approfondire le funzionalità di queste tre tipologie di visualizzazione, vi invito ancora una volta a leggere l'articolo su sulle UI di Unity dove vengono spiegate in modo molto dettagliato.
Nel nostro caso andremo a creare un normale menu 2D, dunque utilizzeremo Screen Space - Overlay, ovvero il metodo di rendering di default. In questo modo quando lo schermo viene ridimensionato o cambia la risoluzione, la Canvas cambierà automaticamente le dimensioni per riadattarsi. Come suggerisce il termine Overlay, l’UI verrà visualizzata sopra ad ogni oggetto 3D.
Usando questo sistema non sarà necessario spostare/ruotare l’oggetto in un punto specifico della scena perché esso sarà sempre posizionato al centro dello schermo e sarà dunque impossibile gestire manualmente il suo Rect Transform che verrà gestito automaticamente in base alle dimensioni dello schermo.

Un altro oggetto che verrà generato automaticamente all’interno della scena (se non era già presente) è l’Event System. Esso si occuperà della gestione i diversi input ricevuti dagli oggetti della nostra UI a seconda del dispositivo su cui sta girando il gioco.

Andiamo a modificare un po' il pannello in modo che sia visualizzato sulla scena e nella finestra del gioco (Game).
Possiamo modificarne il colore e l'immagine di sfondo, in questo caso io ho tolto l'immagine (impostandola su none) e ho applicato un colore blu, completamente opaco (alpha =1).

Inseriamo ora un nuovo pannello, quello che conterrà i pulsanti del nostro menu.
Potremmo anche creare i pulsanti dentro al pannello principale, ma per avere una corretta disposizione dei pulsanti e per poterli allineare e spostare a piacimento, è il caso di usare questo sotto-pannello destinato solo ai pulsanti.

Una volta creato il secondo pannello, trasciniamolo dentro pannello principale (nella Hierachy), così che esso diventi "figlio" del pannello di sfondo. A questo punto la sua posizione sarà in relazione al pannello principale.
Nell'esempio ho rinominato i due pannelli con PannelloPrincipale e PannelloPulsanti, così da non fare confusione e tenere la Hierarchy sempre ordinata e comprensibile a colpo d'occhio.

Possiamo allineare il menu a nostro piacimento, nel mio caso ho posizionato il pannello che conterrà i pulsanti sulla sinistra, così da poter avere uno spazio sul lato destro dello schermo per inserire altri elementi o, nel caso avessimo il pannello principale trasparente, visualizzare la scena sottostante.
Modificando il colore del pannello più piccolo (PannelloPulsanti) esso sarà visualizzato leggermente più scuro del pannello dello sfondo. Tali accorgimenti grafici saranno appicabili a vostra discrezione secondo le vostre esigenze artistiche.

Torniamo sul menu di Unity e sempre dal menu GameObject->UI, inseriamo il primo pulsante (Button).

Cloniamo il Button (o creaimo altri Button) per tutte le volte che vogliamo e posizionamoli come figli di PannelloPulsanti.
In questo caso avremo 5 Button, che però all'inizio saranno disposti uno sopra all'altro (con posizione 0,0,0 cioè al centro del PannelloPulsanti).

Delle loro coordinate a noi non ce ne importa nulla, perché ora andremo ad allineare i pulsanti con un apposito strumento chiamato Layout Group, nello specifico, il Layout Vertical Group.


Layout significa "disposizione" e tutti i componenti di Layout ci permetteranno di disporre gli elementi di una UI in modo sempre preciso ed equidistante, anche in caso di modifica della risoluzione.

 

Slezionamo il gameObject PannelloPulsanti e aggiungiamo il componenete Layout Vertical Group.

Come per magia, i nostri pulsanti andranno a posizionarsi in modo ordinato, mantenendo uno spazio uguale tra di loro.

Tramite i parametri del Layout Vertical Group possiamo ora gestire lo spazio tra di essi e altri parametri di di posizione, senza dover gestire il trasform di ogni singlo pulsante. Questo componenete farà in modo che i pulsanti siano sempre disposti in modo corretto all'interno del gameObject padre PannelloPulsanti.

Come vedete ho lasciato un po' di spazio nella parte alta del pannello, impostando il parametro "Top" su 100.
Questo ci rende possibile l'inserimento di un'immagine o di un testo o di qualsiasi altro elemento noi vorremmo inserire in alto al menu.
Nel nostro caso ci mostrerà l'utilizzo di un altro componente molto utile nella disposizione degli elementi di una UI, ovvero il Layout Elment.

 

Layout Element ci permette di "dire a Unity" che quell'elemento dovrà ignorare il Layout del gruppo di cui fa parte, ovvero non dovrà seguire le specifice del Layout Vertical Group di cui, essendo posto sotto il PannelloPulsanti, fa parte.
In questo modo potremmo posizionare il testo (o un'immagine o altro elemento UI) a mano, in una posizione arbitrariamente scelta da noi.

Miglioriamo l'aspetto grafico

A questo punto possiamo inziare a pensare ad inserire qualche accorgimento grafico al nostro menu che altrimenti risulterebbe troppo banale e spoglio.
Impariamo prima di tutto ad importare nuovi Fonts (set di caratteri) da poter utilizzare nelle nostre UI.

In rete sono presenti migliaia di siti da cui è possibile scaricare fonts gratuiti. Ma è possibile anche utilizzare quelli che sono già presenti sul vostro sistema operativo.
Per farlo è sufficiente andare nella cartella Fonts di Windows e copiare il tipo di carattere che più vi piace in una cartella del progetto Unity.

Con qualche accorgimento grafico potremmo ottenere l'effetto voluto. Tutto dipende dalla vostra vena artistica, dal genere di gioco in questione e dal risultato che vorrete ottenere.

A questo punto abbiamo il nostro menu pieno di pulsanti che però non fanno assolutamente nulla, se non starsene lì da solo mostrandosi in tutta la sua bellezza 😉 

Scriviamo un po' di codice per gestire il menu

E' buona norma creare uno script che gestisca il menu, che contenga tutti i metodi che dovranno essere eseguiti alla pressione di un pulsante e tutti i metodi che gestiscano la sua visualizzazione e permanenza in gioco.

Creiamo un nuovo script e chiamiamolo "GameMenu.cs". Tale script dovrà trovarsi sull'elemento principale della UI, ovvero sul gameObject della Canvas che come da foto ho rinominato in GameMenu.

 

Come prima cosa scriveremo il codice che gestisce la visualizzazione e la scomparsa del menu.
Esistono decine di modi per raggiungere tale scopo. Per nascondere/visualizzare il menu potremmo semplicemente disabilitare/abilitare il gameObject, oppure rendere la trasperenza della Canvas uguale a 0 e uguale a 1 quando lo vorremmo avere su schermo.
Questo dipende da diversi fattori legati alla struttura del menu e anche dal gameplay del gioco.

Modificando l'alpha della Canvas dovremmo anche disabilitare/abilitare il click sui pulsanti ed essere coscenti di dover tenere traccia di altre precauzioni.
Dunque meglio usare la tecnica di disabilitazione e riabilitazione del gameObject figlio, ovvero del gameObject PannelloPrincipale.
In questo modo il gameObject GameMenu sarà sempre attivo e di conseguenza anche lo script su di esso, mentre potremmo disabilitare il gameObject PannelloPrincipale a nostro piacimento senza interrompere un eventuale funzione in esecuzione nello script "GameMenu.cs" .

using UnityEngine;

public class GameMenu : MonoBehaviour {

    //Il nostro Menu, ovvero il gameObject PannelloPrincipale
    GameObject gameMenu;

	void Start () {
	    //Con questa riga andiamo a prendere il primo children sotto a questo transform
        gameMenu = transform.GetChild(0).gameObject; 
    }
	

	void Update () {
		
	}
}

 

Nel metodo Start (cioè al primo frame del gioco) andremo a cercare il primo transform sotto alla canvas con GetChild(0) così da averlo sempre a disposizione. Avremmo anche potuto rendere questa variabile pubblica ed inserirla da inspector. A voi la scelta.

Andiamo ora a nascondere o visualizzare il menu alla pressione del tasto ESC.

	void Update () {
        //Alla pressione del tasto ESC
        if(Input.GetKeyDown(KeyCode.Escape)){
            //Inverti la visualizzazione (se visibile diventa invisibile e viceversa)
            gameMenu.SetActive(!gameMenu.activeInHierarchy);
        }
	}

Questo metodo è semplice e funzionale.  Ci piace? Ecco, dimenticatelo. 😈 
Funziona finchè non si incappa (e prima o poi capita) in altri problemi a seguito di un gameplay complesso che per esempio potrebbe rendere necessaria la visualizzazione del menu non solo alla pressione di un tasto, ma per esempio anche dopo la morte del giocatore. E se per esempio si muore, appare il menu eil gioco va in pausa... e se a quel punto il giocatorei preme il tasto ESC?
Dunque, facciamo le cose per bene sin da subito. E' sempre meglio prevenire eventuali problemi creando codice il più possibile solido.

using UnityEngine;

public class GameMenu : MonoBehaviour {

    GameObject gameMenu; //Il nostro menu
    bool menuIsOpen; //Variabile che verifica lo stato aperto/chiuso del menu

     void Awake()
    {
        gameMenu = transform.GetChild(0).gameObject; //Con questa riga andiamo a prendere il primo children sotto a questo transform
    }


    void Start () {
        menuIsOpen = false; //Al primo frame nascondo il menu
        gameMenu.SetActive(menuIsOpen);// Per sicurezza, invece che gameMenu.SetActive(false) uso la variabile menuIsOpen
    }
	
    //Il metodo che apre il menu
   public void OpenMenu() {
        gameMenu.SetActive(true);
    }

    //Il metodo che chiude il menu
    public void CloseMenu()
    {
        gameMenu.SetActive(false);
    }

    void Update () {

        //Alla pressione del tasto ESC
        if(Input.GetKeyDown(KeyCode.Escape)){
            if (menuIsOpen)// Se il menu è aperto
                CloseMenu();//Chiudilo
            else //Se il menu è chiuso
                OpenMenu(); //Aprilo

            //Dopo aver aperto o chiuso il menu
            //invece che semplicemente menuIsOpen=false o true nei rispettivi metodi
            //facciamo un check sulla reale visibilità del menu
            menuIsOpen = gameMenu.activeInHierarchy; 
        }
	}
}

Abbiamo usato una metodologia più solida per prevenire eventuali esecuzioni esterne a questo script che potrebbero andare a nascondere il menu.
Noi ancora non abbiamo nessun'altro script che potrebbe creare problemi con il menu, ma in futuro potremmo averne diversi e nel momento in cui qualcosa non funzionerà potremmo dover passare tempo prima di capire cosa c'è che non va.
Per questo motivo, per determinare se il menu è aperto o meno, usiamo

  menuIsOpen = gameMenu.activeInHierarchy;

quando avremmo potuto usare una metodologia simile a questa:

    //Il metodo che apre il menu
   public void OpenMenu() 
   {
        menuIsOpen=true;
        gameMenu.SetActive(true);
    }

    //Il metodo che chiude il menu
    public void CloseMenu()
    {  
        menuIsOpen=false;
        gameMenu.SetActive(false);
    }

Il risultato sarebbe stato identico ma potrebbe capitare che in qualche altro script, in futuro, avremmo dovuto nascondere il menu per qualche altro motivo non passando per questi metodi, dunque il menu sarebbe potuto risultare non visibile e la variabile menuIsOpen risultare comunque true.
Per ovviare a questi inconvenienti è opportuno fare un chek con

menuIsOpen = gameMenu.activeInHierarchy;
Usiamo un Singleton e la funzione DontDestroyOnLoad per mantenere unico e persistente in tutte le scene il gameObject del menu

A questo punto andiamo a fare in modo che il gameObject menu sia unico per tutto il gioco, non distruttibile durante un cambio di scena e dunque sempre lo stesso per tutte le scene.

Per fare questo andremo ad usare 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 GameMenu per tutto il gioco e non ce ne potrà mai essere più di uno.

Se non avete idea di cosa sia un Singleton, vi invito a leggere l'articolo sull'organizzazione del codice dove spiego la funzionalità di questo sistema insieme alla funzione DontDestroyOnLoad che comunque descriverò sommariamente anche qui.

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.

       //L'istanza unica e statica della classe GameMenu
       static GameMenu instance;
       
        void Awake()
        {
            //Codice che definisce un Singleton
            
            //Nell'Awake, alla prima esecuzione del gioco, gli dico che:
            //se non esiste un'istanza di GameMenu allora l'istanza è questo stesso script. 
            //mentre se l'istanza già esiste, cancella questo gameObject così da utilizzare il GameMenu 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
                    
           }

 

Forse vi starete chiedendo perchè abbiamo certificato che esista una sola istanza di questa classe, quando sappiamo che il menu sarà presente solo nella prima scena?

Perché durante lo sviluppo del gioco potrebbe non essere necessariamente così.

if(instance== null) fa proprio questo, ovvero si sincera che non sia già presente una versione di questa classe e nel caso la si trovi, distrugge l'oggetto trovato per usare quello già precedentemente esistente.
Altrimenti, se si carica la scena senza passare per la prima scena, usa la versione del menu che trovi in questa scena. Cosa che non capiterà mai nel gioco vero e proprio ma che potrebbe capitare durante lo sviluppo e i test delle singole scene.

Questo è utile in fase di sviluppo perché se dobbiamo testare una qualsiasi scena con il menu annesso dovremmo inserire il menu anche nella scena su cui stiamo lavorando. A questo punto dovremmo ricordarci di cancellare il menu da tutte le scene una volta finito di lavorare sulle scene.
Per ovviare a questo che potrebbe diventare un problema, facciamo in modo che, se il menu è stato caricato dalla prima scena, allora, appena caricata una scena (nel metodo Awake, dunque prima del primo fotogramma di gioco) cerca e cancella tutte le altre istanze di quell'oggetto ed usa solo quella che "mi porto dietro" dalla prima scena.

A questo punto il nostro codice è diventato così:

using UnityEngine;

public class GameMenu : MonoBehaviour {

    GameObject gameMenu; //Il nostro menu
    bool menuIsOpen; //Variabile che verifica lo stato aperto/chiuso del menu
                    
    static GameMenu instance; //L'istanza unica e statica della classe GameMenu

    void Awake()
    {

        gameMenu = transform.GetChild(0).gameObject; //Con questa riga andiamo a prendere il primo children sotto a questo transform


        //Codice che definisce un Singleton

        //Nell'Awake, alla prima esecuzione del gioco, gli dico che:
        //se non esiste un'istanza di GameMenu allora l'istanza è questo stesso script. 
        //mentre se l'istanza già esiste, cancella questo gameObject così da utilizzare il GameMenu 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

    }
    


    void Start () {
        menuIsOpen = false; //Al primo frame nascondo il menu
        gameMenu.SetActive(menuIsOpen);// Per sicurezza, invece che gameMenu.SetActive(false) uso la variabile menuIsOpen
    }
	
    //Il metodo che apre il menu
   public void OpenMenu() {
        gameMenu.SetActive(true);
    }

    //Il metodo che chiude il menu
    public void CloseMenu()
    {
        gameMenu.SetActive(false);
    }

    void Update () {

        //Alla pressione del tasto ESC
        if(Input.GetKeyDown(KeyCode.Escape)){
            if (menuIsOpen)// Se il menu è aperto
                CloseMenu();//Chiudilo
            else //Se il menu è chiuso
                OpenMenu(); //Aprilo

            //Dopo aver aperto o chiuso il menu
            //invece che semplicemente menuIsOpen=false o true nei rispettivi metodi
            //facciamo un check sulla reale visibilità del menu
            menuIsOpen = gameMenu.activeInHierarchy; 
        }
	}
}

Premendo Play qualcosa cambia auotmaticamente nella nostra Hierarchy.

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.

In questo modo, anche cambiando scena, tutto ciò che si trova sotto DontDestroyOnLoad non verrà distrutto automaticamente come succede per i tutti gli altri gameObjects della scena.

Andiamo ora a dare una funzionalità al menu.

Nel nostro script GameMenu.cs andremo a scrivere dei metodi che saranno richiamati nel momento in cui si useranno i vari pulsanti del menu.

using UnityEngine;

public class GameMenu : MonoBehaviour {

    GameObject gameMenu; //Il nostro menu
    bool menuIsOpen; //Variabile che verifica lo stato aperto/chiuso del menu
                     //L'istanza unica e statica della classe GameMenu
    static GameMenu instance;

    void Awake()
    {

        gameMenu = transform.GetChild(0).gameObject; //Con questa riga andiamo a prendere il primo children sotto a questo transform


        //Codice che definisce un Singleton

        //Nell'Awake, alla prima esecuzione del gioco, gli dico che:
        //se non esiste un'istanza di GameMenu allora l'istanza è questo stesso script. 
        //mentre se l'istanza già esiste, cancella questo gameObject così da utilizzare il GameMenu 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

    }
    


    void Start () {
        menuIsOpen = false; //Al primo frame nascondo il menu
        gameMenu.SetActive(menuIsOpen);// Per sicurezza, invece che gameMenu.SetActive(false) uso la variabile menuIsOpen
    }
	
    //Il metodo che apre il menu
   public void OpenMenu() {
        gameMenu.SetActive(true);
    }

    //Il metodo che chiude il menu
    public void CloseMenu()
    {
        gameMenu.SetActive(false);
    }


    public void NewGame() {
        print("Hai premuto il pulsante NewGame!");
    }

    public void ContinueGame() {
        print("Hai premuto il pulsante ContinueGame!");
    }

    public void LoadGame() {
        print("Hai premuto il pulsante LoadGame!");
    }

    public void Options() {
        print("Hai premuto il pulsante Options!");
    }

    public void CloseGame() {
        print("Hai premuto il pulsante CloseGame!");
    }


    void Update () {

        //Alla pressione del tasto ESC
        if(Input.GetKeyDown(KeyCode.Escape)){
            if (menuIsOpen)// Se il menu è aperto
                CloseMenu();//Chiudilo
            else //Se il menu è chiuso
                OpenMenu(); //Aprilo

            //Dopo aver aperto o chiuso il menu
            //invece che semplicemente menuIsOpen=false o true nei rispettivi metodi
            //facciamo un check sulla reale visibilità del menu
            menuIsOpen = gameMenu.activeInHierarchy; 
        }
	}
}

Andiamo ad aggiungere l'esecuzione dei metodi ai rispettivi pulsanti.

Premendo sul + del pulsante, potremmo inserire il gameObject che contiene lo script con i metodi da eseguire.
Nel nostro caso il gameObject GameMenu.

A questo punto possiamo scegliere il metodo adeguato al pulsante scelto, dalla lista dei metodi pubblici dello script GameMenu.cs

Se avete seguito l'articolo con attenzione, tutti i pulsanti funzioneranno come dovrebbero e nella vostra console vedrete apparire i messaggi relativi alla pressione dei tasti.
Ovviamente non posso andare oltre con l'esecuzione delle funzioni dei pulsanti perché queste dipendono dal tipo e dalla struttura che intendete dare al vostro gioco. Nella seconda parte del tutorial vedremo come far apparire un ulteriore pannello dedicato alle opzioni del gioco.

Possiamo per aggiungere un'animazione per l'entrata del menu, ma anche questa è a vostra discrezione, a seconda dell'effetto che vorrete ottenere. In questo esempio ho fatto entrare il menu dall'esterno delo schermo e ho fatto un fade del pannello principale.

Per inserire un'animazione vi basterà inserire un Animator con la relativa animazione nel gameObject PannelloPrincipale senza fare altro.
L'animazione partirà automaticamente ad ogni attivazione del gameObject, dunque ogni volta che aprirete il menu.

Ed ecco il risultato del nostro lavoro sul nostro menu fino a questo momento.

Package completo del tutorial - Parte 1.

In questo package troverete tutto il materiale usato per la creazione del menu fino a questo punto del tutorial, compreso di scripts, immagini e della scena d'esempio funzionante. Creato in Unity 2018.2.2f1

Ci siamo dimenticati qualcosa?
Ma si, dobbiamo far andare in pausa il gioco nel momento in cui si apre il menu.

Ci basterà aggiungere le righe

Time.timeScale = 0;

e

Time.timeScale = 1;

negli appositi metodi di apertura e chiusira del menu.

   //Il metodo che apre il menu
   public void OpenMenu() {
        gameMenu.SetActive(true);
        Time.timeScale = 0;
    }

    //Il metodo che chiude il menu
    public void CloseMenu()
    {
        gameMenu.SetActive(false);
        Time.timeScale = 1;
    }

Facendo questo però dovremo modificare il metodo di aggiornamento dell'animazione di apertura del menu. Questo perché di default anche l'animazione del menu risente del timeScale e nel momento in cui è posto a 0, l'animazione sarà bloccata.

Ci basterà scegliere "Unscaled Time" su Update Mode, nelle opzioni dell'animator.


In questo modo l'animazione verrà eseguita a prescindere dalla pausa in game.

 

Fine Parte 1

 

 

 

TUTORIAL PARTE 2

Andiamo ora ad esaminare le singole funzioni dei pulsanti. Tenendo presente che è molto difficile poter generalizzare su queso aspetto che risente molto delle esigenze individuali.

- Faremo in modo di avere un pannello per le opzioni che si apre alla pressione dell'apposito pulsante.
- Doteremo del pannello di un'animazione d'entrata come per il menu principale.
- Inseriremo delle funzioni per la gestione del volume (globale - suoni - musiche) perfettamente funzionanti.

Come abbiamo visto, per ora i pulsanti non fanno altro che scrivere nella console un normalissimo testo. Questo può essere utile come banco di prova e controllare che tutto funzioni correttamente.

Inseriamo un nuovo pannello che si aprirà alla pressione sul pulsante "Opzioni".
Come prima cosa dobbiamo creare un nuovo pannello che andremo a visualizzare alla destra dello schermo.

Per creare un altro pannello si possono usare due strade differenti. La più veloce è quella di clonare il gameObject PannelloPulsanti per poi togliere i pulsanti e allineare il pannello sulla destra, modificandone le misure.
In alternativa possiamo creare un nuovo pannello da zero ed impostarne le misure.

Adesso dobbiamo tornare sullo script per poter gestire questo nuovo pannello.
Come prima cosa dobbiamo dire alla UI che il pannello delle opzioni, di default deve essere nascosto.

using UnityEngine;

public class GameMenu : MonoBehaviour {

    GameObject gameMenu; //Il nostro menu
    bool menuIsOpen; //Variabile che verifica lo stato aperto/chiuso del menu
                     //L'istanza unica e statica della classe GameMenu
    static GameMenu instance;

    public GameObject optionsPanel; //Ecco il gameObject del pannello Opzioni

    void Awake()
    {

        gameMenu = transform.GetChild(0).gameObject; //Con questa riga andiamo a prendere il primo children sotto a questo transform


        //Codice che definisce un Singleton

        //Nell'Awake, alla prima esecuzione del gioco, gli dico che:
        //se non esiste un'istanza di GameMenu allora l'istanza è questo stesso script. 
        //mentre se l'istanza già esiste, cancella questo gameObject così da utilizzare il GameMenu 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

    }
    


    void Start () {
        menuIsOpen = false; //Al primo frame nascondo il menu
        gameMenu.SetActive(menuIsOpen);// Per sicurezza, invece che gameMenu.SetActive(false) uso la variabile menuIsOpen
        optionsPanel.SetActive(false); //Allo start nascondiamo il pannello delle opzioni
    }

 

A questo punto avremo una variabile pubblica sull'inspector dello script dove dovremo inserire il nostro pannello delle Opzioni che in questo esempio ho rinominato PannelloOpzioni.

Per far apparire il nostro pannello delle opzioni sarà sufficiente modificare il metodo che si esegue alla pressione del pulsante Opzioni:

    public void Options() {
        optionsPanel.SetActive(true);
    }

Andiamo ora ad inserire un piccolo pulsante che abilita la chiusura del pannello Opzioni, tipicamente una X sul lato alto, sulla destra del pannello. Ovviamente tale pulsante dovrà trovarsi sotto il PannelloOpzioni.
Ci basterà clonare uno dei pulsanti del menu principale, spostarlo sotto il PannelloOpzioni e ridimensionarlo come meglio crediamo. All'interno di esso ci sarà un testo con la sola X.

Per far si che il "pulsantino" di chiusura sia ben allineato con il suo pannello "padre", usiamo questi parametri, con il punto di ancoraggio in alto a destra.
A seconda delle vostre esigenze potrete scegliere le dimensioni del pulsante modificando la Width e la Height, così come la posizione X Y del pulsante.

Per aggiungere la funzionalità di chiusura del pannello potremmo fare in diversi modi. Visto che fin'ora abbiamo usato dei metodi da richiamare alla pressione dei tasti, facciamo la stessa cosa anche con questo pulsantino.

Andiamo ad aggiungere il metodo per la chiusura del pannello opzioni.
Adesso avremo questo set di metodi richiamati dai vari pulsanti.

    //Il metodo che apre il menu
   public void OpenMenu() {
        gameMenu.SetActive(true);
        Time.timeScale = 0;
    }

    //Il metodo che chiude il menu
    public void CloseMenu()
    {
        gameMenu.SetActive(false);
        Time.timeScale = 1;
    }

    public void NewGame() {
        print("Hai premuto il pulsante NewGame!");
    }

    public void ContinueGame() {
        print("Hai premuto il pulsante ContinueGame!");
    }

    public void LoadGame() {
        print("Hai premuto il pulsante LoadGame!");
    }

    public void Options() {
        optionsPanel.SetActive(true);
    }

    public void CloseGame() {
        print("Hai premuto il pulsante CloseGame!");
    }

    public void ColseOptions()
    {
        optionsPanel.SetActive(false);
    }
    
    
    
    

Abbiamo aggiunto questo metodo:

public void ColseOptions()
{
optionsPanel.SetActive(false);
}

Non ci resta altro che assegnare questo metodo al pulsantino di chiusura, come abbiamo fatto per gli altri pulsanti.

Come abbiamo fatto per il menu principale, andiamo ad aggiungere una sorta d'animazione d'entrata anche al pannello delle opzioni.
Aggiungiamo un Animator al PannelloOpzioni e applichimao "Unscaled Time" su Update Mode.
Creiamo un'animazione e rendiamola "non Loop" dalle opzioni dell'animazione, trovando il file giusto dal pannello Project di Unity.
Vediamo un video di come fare:

Sembra funzionare tutto alla grande!

Inseriamo degli Sliders per la gestione dei volumi del gioco, divisi in Master (volume globale), Sounds (volume dei suoni) e Music (volume delle musiche).

Cosa sono gli Sliders?

Gli Sliders sono elementi della UI che restituiscono un valore in base alla posizione dell'Handle sulla sua "riga di appartenenza" (Handle Slide Area) restiuendo un valore che va da 0 a 1, dove 0 è quando si trova completamente a sinistra e 1 quando si trova completamente a destra. L'Handle (maniglia) non è altro che il punto che si sposta lungo la riga quando lo spostiamo con il mouse.

Tali volori sono impostati così di default ma volendo possono essere variati a piacimento. A noi questa impostazione va più che bene e nella maggir parte dei casi è meglio lasciare questa scala da 0 a 1. Sarà poi da codice che andremo a fare il lavoro di moltiplicazione o divisione del numero restituito dagli sliders.

Aggiungiamo un primo Slider al pannelloOpzioni.

Sempre dal menu GameObject di Unity, andiamo su UI e aggiungiamo uno Slider.


Ogni volta che creaimo un nuovo elemento della UI, quasi mai esso sarà creato dove vorremmo che fosse nella Hierarchy.
Dovremmo trascinare e posizionare l'oggetto di nostro pugno.

A questo punto sarebbe necessario modificare l'aspetto grafico dello slider, per renderlo più conforme allo stile che abbiamo dato all'intera UI. Di default lo slider è bianco, molto alto e generalmente poco elegamte.
Noi andremo a modificarlo secondo le nostre scelte artistiche.

Modificando il colore e le dimensioni dello slider si può ottenere un aspetto migliore. Troverete gli sliders modificati usati in questo tutorial all'interno del prossimo package, così da poter esaminare come ho modificato le dimensioni ed i colori dei vari elementi.

 

Andiamo avanti aggiungendo un testo che esprima a il valore attuale del volume. Avremo un testo numerico per ogni slider e faremo in modo che tale testo vada da 0 a 10, dove per esempio 5 rappresenta metà del volume massimo.
Ho rinominato il gameObject del testo in valueText, così da capire che quello è il testo del valore di quello slider.
In seguito vedremo che è sempre opportuno modificare il nome dei gameObjects in modo che siano riconoscibili a colpo d'occhio.

Ricordiamoci di mettere il testo sotto il gameObject dello Slider.
Non che sia una cosa strettamente necessaria ai fini della funzionalità, ma ci renderà la vita più facile nel momento in cui vorremmo spostare gli sliders per posizionarli dove vogliamo.

Per ora il testo al suo interno ci dice "New Text" e per il momento va bene. Il testo di default a noi non interessa perché esso sarà gestito e modificato tramite codice. Andremo a prendere il valore corretto (in numeri) da applicare a questo testo.

Come avrete notato in questo caso non abbiamo usato nessun componente di Layout per posizionare gli elementi della UI nel pannello delle opzioni, cosa che potrete fare a vostra discrezione.

Come passo successivo, prima di clonare lo slider per creare gli altri due, andiamo ad inserire un nuovo elemento di testo che identifichi il nome di quello slider. Ho chiamato il gameObject del testo sliderName.
In questo caso il testo all'interno del componente lo andiamo a scrivere di nostro pugno. Il primo lo chiamiamo "Globale" perchè si tratta del volume "Master" ovvero il volume che influenza l'intero sistema audio.

Apriamo una piccola parentesi per spiegare come si comportano gli elemeti della UI in base alla loro posizione nella Hierarchy.

La posizione degli elementi di una UI è molto importante. Come avrete notato l'immagine di background è sempre il primo gameObject nella gerarchia. Questo non è un caso ma una necessità. Gli oggetti posti più in basso sono quelli che risultano sopra agli altri. Per questo un'immagine che deve fungere da sfondo deve essere la prima nella Hierarchy. Dunque in queso caso, gli Sliders e tutti gli altri eventuali elementi devono essere posti dopo il gameObject BkImage.

 

Andiamo ora a duplicare gli sliders e modifichiamo i testi in base a quello che rappresentano. Ho modificato anche il testo dove andranno i valori del volume (noi avremo da 0 a 10) per verificare che poi, con dei valori numerici l'aspetto sia consono. Ho inserito "5" come testo di default, ma non è importante.

Torniamo al nostro script e aggiungiamo i tre elementi per lavorare con gli Sliders.

    public Slider GlobalSlider;
    public Slider SoundsSlider;
    public Slider MusicSlider;

La classe Slider però fa parte del Namespace UnityEngine.UI;

Dovremmo quindi aggiungere una direttiva:

using UnityEngine;

 


Se non sapete cosa sia un Namespace vi consiglio di andare a leggere l'articolo a riguardo: Sintassi e NameSpace

Per la precisione, using UnityEngine.UI; significa:
“usa la collezione di classi che si trovano all’interno del namespace UnityEngine.UI“.

Andiamo ora ad inserire gli sliders nell'inspector dello script.

Sullo script aggiungiamo anche i riferimenti dei testi per i valori del volume:

    public Text GlobalSliderValue;
    public Text SoundsSliderValue;
    public Text MusicSliderValue;

Poi andiamo ad aggiungere anche questi gameObjects sull'inspector dello script.


Facendo attenzione nel rispettare le giuste posizioni. Non avendo rinominato i gameObjects dei valueText potremmo sbagliarci ed entrare in confusione!
Per questo motivo, come dicevo precedentemente, è sempre meglio nominare ogni oggetto in modo univoco e ben distinguibile.
Cosa che possiamo fare in qualunque momento, anche dopo che li abbiamo aggiunti allo script.

Come noterete i nomi nell'inspector dello script vengono aggiornati automaticamente.

Ora nel nostro script abbiamo accesso agli sliders e potremmo sin da ora impostare i tre valori testuali in base al valore dello slider.

Nel metodo Update andremo ad aggiungere queste sitruzioni:

        //Gestione testi degli sliders
        GlobalSliderValue.text = Mathf.Round(GlobalSlider.value * 10).ToString();
        SoundsSliderValue.text = Mathf.Round(SoundsSlider.value * 10).ToString();
        MusicSliderValue.text = Mathf.Round(MusicSlider.value * 10).ToString();

Abbiamo fatto in modo che il valore delle tre aree di testo sia essere uguale al valore dello slider (che ricordiamo, va da 0 a 1) moltiplicato per 10.  Dunque avremo dei valori testuali da 0 a 10. Essendo questo codice all'interno del metodo Update, i testi verranno sempre aggiornati, frame dopo frame.
Per evitare di avere dei numeri con virgola che non ci interessano e farebbero solo confusione, usiamo la classe Mathf che possiede il metodo Round che ci aiuta ad arrotondare un numero float (con virgola) in un numero intero.


Attenzione:
Q
uesta parte del codice verrà eliminata tra non molto per essere sostituita da altro codice più performante.
Abbiamo la possibilità di far eseguire queste operazioni solamente "onValueChanged" cioè quando il valore dello slider cambia e non durante tutto il ciclo di Update come succede ora.
Per adesso ci basta aver capito il concetto e testare che funzioni.

A quanto pare funziona tutto come dovrebbe. I valori nei campi di testo vengono aggiornati correttamente. Ma come detto essi vengono aggiornati ad ogni ciclo di Update, cosa che potremo evitare.

 


 

Gestione del Mixer e dei Mixer Groups

Ancora gli slider non fanno assolutamente nulla.
Per testare il nostro sistema audio avremo prima di tutto bisogno di un Audio Source ovvero un gameObject che emetta qualche suono.
Inseriremo nella scena due Audio Source per testare sia i suoni che le musiche.

Se non avete una traccia audio su cui fare dei test potete liberamente scaricare questa per testare il mixer delle musiche:

TrackTest.mp3

 e questa per testare il mixer dei suoni:

SoundsTest.mp3

Se non avete una cartella Audio nel vostro progetto, createla e metteteci i due file audio.

Abbiamo parlato di Mixer.
Il Mixer è lo strumento con cui gestiremo l'audio.
Il nostro Mixeravrà tre sotto-gruppi (mixerGroup) gestibili da codice, uno globale (che influenza tutto l'audio), uno per le musiche (che influenza solo le tracce musicali) e uno per i suoni (che influenza solo gli effetti sonori).

A questo punto andiamo a creare il nostro Mixer e a dotarlo dei tre sottogruppi.
Cliccate con il tasto destro nel pannello Project, vicino ai due file audio e create un nuovo oggetto di tipo Mixer.

 


Vi troverete un nuovo oggetto che potete rinominare come meglio credete. Io l'ho chimato semplicemente Mixer.


Cliccando due volte sul Mixer si aprirà un pannello dove potrete aggiungere i sotto-gruppi.
In partenza esiste un solo sotto-gruppo chiamato Master. Esso sarà il mixerGroup che gestirà l'audio globale.
Dobbiamo crearne altri due.
Cliccando con il testo destro su "Master" potrete creare i due sotto-gruppi (mixerGroup) destinati ai suoni e alle musiche.

Come avrete intuito, anche i gruppi (mixerGroup) possiedono una gerarchia. Essendo i due sotto-gruppi Suoni e Musica posti sotto al gruppo principale "Master" essi risentiranno degli effetti e della quantità di volume di Master.
Il volume del Master gestirà dunque alche il volume dei suoi sotto-gruppi.

 

Prima di proseguire dobbiamo fare un altro paio di passaggi per rendere "leggibili" le variabili dei tre gruppi, ovvero per renderle "esposte" al nostro utilizzo da codice.

Tale operazione è decisamente rognosa, non perché sia complicata ma perché quando non eseguita non ci saranno errori che ci dicono cosa ci sia che non vada, semplicemente non avremo accesso al volume nel codice.
Non essendo espressamente evidenziata, l'esigenza di tale operazione è divenuta motivo di tante frustrazioni e richieste d'aiuto su Internet.


Dovremmo fare questa operazione per tutti i gruppi che abbiamo.
Avremo così tre parametri esposti.
Se infatti andiamo a guardare nel pannello dei gruppi, vedremo la scritta Exposed Parameters (3) a destra della barra.
Cliccando sopra a questa parte di barra potremmo rinominare i parametri. Facciamolo in modo coerente con ciò che rappresentano, ovvero i volumi dei tre gruppi.
Questi nomi saranno quelli che andremo a richiamare da codice.

Abbiamo creato il nostro Mixer, pronto per essere usato!

 

 

Tornaimo nella nostra Hierarchy.
Creiamo i due nuovi gameObjects che emettano rispettivamente suoni e musica.
Per farlo ci basterà creare dei gameObjects vuoti e dotare ognuno di essi del componente AudioSource.

Il componente AudioSource possiede diversi parametri, ma per ora a noi interessano solo due, AudioClip e Output.
In AudioClip andremo ad inserire gli mp3.
In Output andremo ad impostare su quale dei mixergroup dovranno suonare questi AudioSource.

1. Assegnamo l'mp3 della musica (TrackTest.Mp3) sull'audioClip del gameObject che qui ho nominato MusicSource.
Poi scegliamo "Musiche" come suo Output.

Facciamo la stessa cosa con l'altro gameObject (qui rinominato in SoundSource), assegnado l'mp3 dei suoni e l'output "Suoni".

A questo punto abbiamo associato ad ogni AudioSource il giusto gruppo Mixer.

 


Ogni qualvolta inseriremo un nuovo AudioSource nel gioco, dovremo specificare su quale Output dovrà essere eseguito, in base al fatto se emetterà dei Suoni o delle Musiche.
Quando non lo facciamo, un AudioSource utilizza di default il Mixer master.

 

Non ci resta altro che "collegare" il volume dei tre MixerGroups con gli slaiders del nostro menu opzioni.
Questo noi lo faremo da codice, nel nostro amato script GameMenu.cs

Eiste un metodo per farlo anche da inspector, ma a noi piace scrivere codice. 😀 

Come prima cosa creiamo le variabili del mixer e dei sotto-gruppi.

    public AudioMixer mainMixer; //Il mixer 
    AudioMixerGroup masterGroup; //Gruppo per il suono globale
    AudioMixerGroup soundsGroup; //Gruppo per i suoni
    AudioMixerGroup musicGroup; //Gruppo per le musiche

Avrete notato che nel codice il mainMixer è pubblico, mentre i sotto-gruppi no.
Questo perché il Mixer globale (mainMixer) lo andremo ad impostare tramite inspector, mentre ricaveremo i sotto-gruppi dallo stesso  Mixer globale.

NOTA:
Potremmo anche andare a cercare il mainMixer tra le risorse del progetto con una riga di codice simile a questa:
mainMixer= Resources.Load<AudioMixer>("MasterMixer"); 
Ma per evitare di dover creare una cartella Resources e dover aggiungere altri concetti non nerenti a questo articolo, usiamo l'inspector.

Nel metodo Awake, prima che accada altro, andiamo ad impostare i gruppi audio.

       //Estrapoliamo i gruppi dal Mixer, senza bisogno di impostarli da inspector
        masterGroup = mainMixer.FindMatchingGroups(string.Empty)[0];
        musicGroup = mainMixer.FindMatchingGroups(string.Empty)[2];
        soundsGroup = mainMixer.FindMatchingGroups(string.Empty)[1];

 

A questo punto andiamo ad occuparci di cambiamenti del codice necessari per migliorare le performance ovvero, andiamo a togliere l'azione che imposta i testi nell'Update ed usiamo dei Listener.

Il codice finale del nostro amato GameMenu.cs sarà dunque completo:

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Audio;
public class GameMenu : MonoBehaviour {

    GameObject gameMenu; //Il nostro menu
    bool menuIsOpen; //Variabile che verifica lo stato aperto/chiuso del menu   
    static GameMenu instance; //L'istanza unica e statica della classe GameMenu

    public Slider GlobalSlider;
    public Slider SoundsSlider;
    public Slider MusicSlider;

    public Text GlobalSliderValue;
    public Text SoundsSliderValue;
    public Text MusicSliderValue;

    public AudioMixer mainMixer; //Il mixer 
    AudioMixerGroup masterGroup; //Gruppo per il suono globale
    AudioMixerGroup soundsGroup; //Gruppo per i suoni
    AudioMixerGroup musicGroup; //Gruppo per le musiche

    public GameObject optionsPanel;

    void Awake()
    {
        //Estrapoliamo i gruppi dal Mixer, senza bisogno di impostarli da inspector
        masterGroup = mainMixer.FindMatchingGroups(string.Empty)[0];
        musicGroup = mainMixer.FindMatchingGroups(string.Empty)[2];
        soundsGroup = mainMixer.FindMatchingGroups(string.Empty)[1];

        gameMenu = transform.GetChild(0).gameObject; //Con questa riga andiamo a prendere il primo children sotto a questo transform


        //Codice che definisce un Singleton

        //Nell'Awake, alla prima esecuzione del gioco, gli dico che:
        //se non esiste un'istanza di GameMenu allora l'istanza è questo stesso script. 
        //mentre se l'istanza già esiste, cancella questo gameObject così da utilizzare il GameMenu 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

    }
    


    void Start () {
        menuIsOpen = false; //Al primo frame nascondo il menu
        gameMenu.SetActive(menuIsOpen);// Per sicurezza, invece che gameMenu.SetActive(false) uso la variabile menuIsOpen
        optionsPanel.SetActive(false);

        //Aggiungiamo tre Listener che "intercettino" un cambio di valore dei tre slaiders
        GlobalSlider.onValueChanged.AddListener(SetGlobalVolume); 
        SoundsSlider.onValueChanged.AddListener(SetSoundsVolume);
        MusicSlider.onValueChanged.AddListener(SetMusicVolume);
        
    }


    //Metodo che si esegue nel momento in cui lo slider GlobalSlider cambia valore
    public void SetGlobalVolume(float value)
    {
        GlobalSliderValue.text = Mathf.Round(GlobalSlider.value * 10).ToString();
        mainMixer.SetFloat("MasterVolume", Mathf.Log10(value + 0.0001f) * 20);
    }


    //Metodo che si esegue nel momento in cui lo slider SoundsSlider cambia valore
    public void SetSoundsVolume(float value)
    {
        SoundsSliderValue.text = Mathf.Round(value * 10).ToString();
        mainMixer.SetFloat("SoundsVolume", Mathf.Log10(value + 0.0001f) * 20);
    }


    //Metodo che si esegue nel momento in cui lo slider MusicSlider cambia valore
    public void SetMusicVolume(float value)
    {        
       MusicSliderValue.text = Mathf.Round(value * 10).ToString();
       mainMixer.SetFloat("MusicVolume", Mathf.Log10(value + 0.0001f) * 20);
    }







    //Il metodo che apre il menu
    public void OpenMenu() {
        gameMenu.SetActive(true);
        Time.timeScale = 0;
    }

    //Il metodo che chiude il menu
    public void CloseMenu()
    {
        gameMenu.SetActive(false);
        Time.timeScale = 1;
    }

    public void NewGame() {
        print("Hai premuto il pulsante NewGame!");
    }

    public void ContinueGame() {
        print("Hai premuto il pulsante ContinueGame!");
    }

    public void LoadGame() {
        print("Hai premuto il pulsante LoadGame!");
    }

    public void Options() {
        optionsPanel.SetActive(true);
    }

    public void CloseGame() {
        print("Hai premuto il pulsante CloseGame!");
        print("Il gioco si chiude solo nella build");
        Application.Quit();
    }

    public void ColseOptions()
    {
        optionsPanel.SetActive(false);
    }

    void Update () {

        //Alla pressione del tasto ESC
        if(Input.GetKeyDown(KeyCode.Escape)){
            if (menuIsOpen)// Se il menu è aperto
                CloseMenu();//Chiudilo
            else //Se il menu è chiuso
                OpenMenu(); //Aprilo

            //Dopo aver aperto o chiuso il menu
            //invece che semplicemente menuIsOpen=false o true nei rispettivi metodi
            //facciamo un check sulla reale visibilità del menu
            menuIsOpen = gameMenu.activeInHierarchy; 
        }




    }
}

Abbiamo aggiunto dei metodi specifici che verranno richiamati tramite Listener:

    //Metodo che si esegue nel momento in cui lo slider GlobalSlider cambia valore
    public void SetGlobalVolume(float value)
    {
        GlobalSliderValue.text = Mathf.Round(GlobalSlider.value * 10).ToString();
        mainMixer.SetFloat("MasterVolume", Mathf.Log10(value + 0.0001f) * 20);
    }


    //Metodo che si esegue nel momento in cui lo slider SoundsSlider cambia valore
    public void SetSoundsVolume(float value)
    {
        SoundsSliderValue.text = Mathf.Round(value * 10).ToString();
        mainMixer.SetFloat("SoundsVolume", Mathf.Log10(value + 0.0001f) * 20);
    }


    //Metodo che si esegue nel momento in cui lo slider MusicSlider cambia valore
    public void SetMusicVolume(float value)
    {        
       MusicSliderValue.text = Mathf.Round(value * 10).ToString();
       mainMixer.SetFloat("MusicVolume", Mathf.Log10(value + 0.0001f) * 20);
    }

Un metodo per ogni slider.
Ora il valore dello slider viene passato come parametro (value).


Se vi state chiedento la funzionalità delle righe

 Mathf.Log10(value + 0.0001f) * 20

sappiate che si tratta della tecnica migliore per interfacciarsi con il volume dei Mixer e che l'aggiunta di quello 0.0001f è necessario nel caso in cui il valore si imposti su 0, valore che il metodo logaritmico non accetta di buon grado.

Abbiamo abilitato anche la chiusura del gioco alla pressione sul pulsante "chiudi".
Questa funzionalità sarà però testabile solo nella Build.

 public void CloseGame() {
        print("Hai premuto il pulsante CloseGame!");
        print("Se il gioco fosse in escuzione in Build, si sarebbe chiuso");
        Application.Quit();
    }

 

Le funzionalità degli altri pulsanti sono strettamente legate al genere di gioco che state facendo.
Caricamenti di scene specifiche e caricamenti di salvataggi sono funzionalità che dipendono totalmente dal tipo di gioco e dal sistema che adotterete per i salvataggi.

Se doveste avere delle specifiche necessità non esitate, come sempre, a contattarmi tramite un commento qui sotto o tramite e-mail.

Qui sotto il video delle funzionalità fin qui inserite.
Lo slider Suoni gestirà il volume di tutti gli AudioSource che avranno come Output il mixerGroup per i suoni
Lo slider Muisca gestirà il volume di tutti gli AudioSource che avranno come Output il mixerGroup per le musiche.
Mmentre lo slider Globale agisce sul volume audio dell'intero sistema.

Mi rendo conto che questo articolo è stato più complesso e lungo del solito.
Siamo partiti con l'intenzione di creare un semplice menu di gioco e durante il viaggio abbiamo visto i Singleton, la gestione dei Mixer, le animazioni di una UI ecc... oltre che ovviamente l'uso di vari elementi di una UI come i Buttons, gli Sliders, i Panels e le Canvas e compnenti come i LaoytGroups.
Un bel po' di roba per una lezione sola!

Package completo del tutorial - Finale.

In questo package troverete tutto il materiale usato per la creazione del menu.
Scripts, immagini, suoni, musiche e la scena d'esempio funzionante. Creato in Unity 2018.2.2f1

3 pensieri su “Creare un menu di gioco

  1. Ma che splendido tutorial!
    Completo e ben spiegato. Davvero complimenti.
    Ti consiglio di usare un correttore automatico quando scrivi articoli così lunghi, giusto per evitare errori di battitura (ce ne sono un paio, cosa di per se già eccezzionale vista la lunghezza del testo che dimostra che sai scrivere molto bene) 😉

    1. Grazie Mirko!
      In effetti non uso nessun correttore automatico e qualche volta, (considerando anche che scrivo gli articoli sopratutto di notte, dunque con una palpebra chiusa e una mezza aperta) mi potrebbe sfuggire qualche errore di battitura. 🙂
      Grazie per i complimenti!

Lascia un commento

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