Una parte molto importante del gioco è l'interfaccia utente (UI), che permette ad esempio agli utenti di scegliere tra più opzioni o di visualizzare i propri risultati e raccogliere un po' i frutti delle loro "fatiche" videoludiche.
In questa sezione della guida mostreremo proprio come creare un'interfaccia utente e ci serviremo del gioco che abbiamo iniziato a sviluppare negli appuntamenti precedenti. Vedremo come aggiungere il punteggio, salvarlo e mostrare i migliori risultati.
Se non avete seguito le lezioni precedenti potete scaricare qui il progetto iniziale, altrimenti il consiglio è quello di ricrearlo a seguendo tutti i passaggi dall'inizio.
Per iniziare quindi possiamo aprire il progetto (o un progetto al quale aggiungere una GUI).
Creare un GameObject per la GUI
Prima di tutto ci servirà creare un GameObject che contenga la nostra GUI (Graphic User Interface). Per questo clicchiamo su GameObject > Create Empty
, poi clicchiamo sul nuovo GameObject nella finestra Hierachy e lo rinominiamo "GUI".
Creiamo un altro GameObject ma stavolta clicchiamo su Create Other > GUI Texture
Avremo un nuovo GUI Texture GameObject nella finestra Hierarchy, chiamato "UnityWatermark-small
", lo rinominiamo ScoreBackground
e lo trasciniamo dentro al GameObject GUI
.
Ora copiamo il file ScoreBG.png nella cartella Textures
del gioco.
Questa texture sarà utilizzata come sfondo per il punteggio. In pratica serve solo per definire una sorta di contenitore per il testo. La associamo come texture del Game Object ScoreBackground.
Per posizionare l'immagine modifichiamo appunto le proprietà del nostro nuovo Game Object di tipo "GUI Texture": clicchiamo su ScoreBackground e iniziamo impostando la posizione nel pannello inspector. I valori saranno 0, 1, 0
(X, Y, Z).
L'intenzione è quella di posizionare la texture nell'angolo in alto a sinistra dello schermo. Per la GUI possiamo ragionare con un sistema bidimensionale di coordinate che ha origine nell'angolo in basso a sinistra (x=0, y=0)
:
L'angolo in alto a destra del rettangolo corrisponde alle coordinate (x=1, y=1)
e le posizioni all'interno sono rappresentate come in percentuale (intervallo [0, 1]
). Possiamo pensare all'intervallo tra 0 e 1 come ad un valore percentuale (1=100%). Il centro sarà alle coordinate (0.5, 0.5)
.
Una volta impostata la posizione su (0,1,0)
, se premiamo play, la texture sarà nel angolo in alto a sinistra, ma totalmente o parzialmente fuori dallo schermo. Perché l'angolo in alto a sinistra della texture corrisponda all'angolo in alto a sinistra dello schermo possiamo impostare la proprietà Pixel Inset coi valori (x=0, y=-58)
, infatti la nostra immagine è proprio di 58px.
Ora che la nostra texture è posizionata come vogliamo, abbiamo bisogno di un font per le scritte.
Anzitutto aggiungiamo una cartella Font sotto la cartella Texture. Quindi aggiungiamo il font Segoe UI al progetto, lo troviamo anche nella cartella Windows (può essere un qualunque True Type Font), ci basta trascinarlo nella cartella che abbiamo creato nel progetto.
Se abbiamo trascinato il font direttamente dal sistema operativo, troviamo numerosi file con le varianti, possiamo eliminarle tutte e lasciare solo SEGOEUI.
Clicchiamo sul font SEGOEUI per tenerlo selezionato in evidenza. Creiamo ora un GameObject per il testo (GameObject > Create Other > GUI Text
) e lo trasciniamo sotto il GameObject GUI
Cliccando GUI Text vediamo che ha assunto direttamente il font selezionato al momento della sua creazione.
Se premiamo play, ora abbiamo la scritta "GUI Text" al centro dello schermo, dobbiamo posizionarla proprio all'interno della cornice che abbiamo creato, quindi modifichiamo la posizione del GUI Text impostandola a (0.01, 0.99, 1)
.
Come vediamo la Z è impostata a 1 perché la scritta sia posizionata al di sopra della texture. In generale possiamo utilizzare la Z per controllare l'ordine di visualizzazione degli elementi.
Impostiamo la dimensione del font a 24 e premiamo play per verificare il risultato.
Mostrare i punteggi e salvare la classifica
A questo punto possiamo contare il punteggio per ogni giocatore e, quando passiamo al Game Over, controllare il risultato e confrontarlo con la classifica. Avremo anche bisogno di salvare la classifica per non perderla quando usciamo dal gioco.
Per questo compito ci possiamo servire dell'oggetto PlayerPrefs di Unity, che ci consente di memorizzare dati non strutturati come numeri interi e testi.
Iniziamo aggiungendo alla classe GameController
una variabile di tipo GUIText che chiamiamo scoreText
:
public class GameController : MonoBehaviour
{
public GameObject enemy;
public GameObject laser;
public GUIText scoreText;
float spawnTimer;
float shootTimer;
// ...
Salviamo lo script e torniamo su Unity. Qui clicchiamo sull'oggetto Main Camera
e trasciniamo l'oggetto GUI Text
, che si trova sotto il GameObject GUI
, sulla proprietà scoreText
nel pannello Inspector.
Ora possiamo collegare il contatore con il punteggio dei giocatori. Torniamo sullo script GameController
e aggiungiamo un riga alla funzione Update
:
void Update()
{
spawnTimer -= Time.deltaTime;
shootTimer -= Time.deltaTime;
// Scriviamo il punteggio sul contatore
scoreText.text = PlayerPrefs.GetInt("CurrentScore", 0).ToString();
// ...
Quello che facciamo qui è impostare il testo dell'oggetto GUI Text
. Prendiamo il testo dall'intero CurrentScore
che troviamo tra le PlayerPrefs
. Poiché non esiste ancora, in questo momento il contatore mostrerà sempre 0. In altre parole il codice fa questo: Cerca l'intero CurrentScore tra le PlayerPrefs, se non esiste ritorna 0.
Dobbiamo quindi collegare l'aumento del punteggio al momento in cui il giocatore colpisce le navi spaziali nemiche.
Aggiungiamo una funzione allo script EnemyController.cs
che assegni il punteggio al giocatore. Chiameremo questa funzione (AddScore) ogni volta che un nemico sarà colpito.
public class EnemyController : MonoBehaviour {
public float speed;
public GameObject explosion; // il GameObject per le esplosioni
void Start () {
}
void AddScore()
{
// prende il punteggio attuale
int _tempScore = PlayerPrefs.GetInt("CurrentScore");
// aggiungiamo 10 punti e salviamo il nuovo risultato
_tempScore += 10;
PlayerPrefs.SetInt("CurrentScore", _tempScore);
}
void OnCollisionEnter(Collision other)
{
if (other.gameObject.tag.Equals("Laser"))
{
Destroy(other.gameObject);
Destroy(this.gameObject);
Instantiate(explosion, this.transform.position, this.transform.rotation);
// aumentiamo il punteggio ogni volta che si abbatte un nemico
AddScore();
}
}
// ...
Come si vede nel codice prendiamo il punteggio salvato in PlayerPrefs
e gli aggiungiamo 10 punti prima di salvarlo di nuovo. Ogni volta che si verifica una collisione mandiamo aggiorniamo il conto dei punti.
In realtà il punteggio sarà salvato solo per la sessione corrente e Unity salva le preferenze su disco quando lasciamo l'applicazione. Se però l'app viene chiusa prematuramente i dati potrebbero andare persi, perciò forzeremo la scrittura su disco con Save()
in alcuni punti del gioco, ad esempio sul cambio di scena al Game Over, ma lo vedremo più avanti. Meglio non forzare il salvataggio durante il gioco, potrebbe rallentarlo.
In questo momento dobbiamo occuparci di azzerare il punteggio ad ogni nuova partita. Possiamo farlo aggiungendo una semplice riga nella funzione Start
:
public class GameController : MonoBehaviour
{
public GameObject enemy;
public GameObject laser;
public GUIText scoreText;
float spawnTimer;
float shootTimer;
void Start()
{
spawnTimer = 1.0f;
PlayerPrefs.SetInt("CurrentScore", 0); // azzeriamo il contatore
}
In quessto modo impostiamo CurrentScore
solo quando inizia una nuova partita: la funzione Start
infatti viene richiamata solo quando GameController viene eseguito per la prima volta.
...e la classifica?
Nella scena di gameOver
vogliamo aggiornare e mostrare anche la classifica, quindi prima di tutto dobbamo controllare che il giocatore abbia realizzato un punteggio sufficiente e in caso affermativo salvarlo.
Apriamo lo script GameOverController e apportiamo queste modifiche:
public class GameOverController : MonoBehaviour
{
float gameOverTimer;
// controlliamo che il nostro giocatore abbia realizzato un record
void CheckHighscore()
{
int _score = PlayerPrefs.GetInt("CurrentScore", 0);
int _highscore = PlayerPrefs.GetInt("HighScore", 0);
if (_score > _highscore)
PlayerPrefs.SetInt("HighScore", _score);
}
void Start()
{
gameOverTimer = 5.0f;
CheckHighscore();
// approfittiamo del momento tranquillo per scrivere su disco la classifica
PlayerPrefs.Save();
}
// ...
Niente di particolamente rilevante qui, abbiamo solo verificato che il punteggio attuale sia maggiore del record. Abbiamo anche inserito il comando Save()
per assicurarci di aver salvato i record su disco.
Non ci resta che mostrare il record e il punteggio realizzato dal giocatore. Creiamo un nuovo GameObject nella scena Game Over e chiamiamolo GUI
, selezioniamo il font SEGOEUI e, come prima, creiamo un oggetto GUI Text
e lo chiamiamo ScoreText
. Creiamone ancora uno e chiamiamolo HighScoreText
.
Ora impostiamo le posizioni per GUI (0, 0, 0)
, ScoreText (0.5, 1, 0)
e HighScoreText (0.5, 0.95, 0)
.
Il risultato dovrebbe essere simile a questo:
Scriviamo il codice per mostrare i dati nella funzione Start dello script GameOverController
:
public class GameOverController : MonoBehaviour
{
float gameOverTimer;
public GUIText scoreText;
public GUIText highScoreText;
void CheckHighscore()
{
int _score = PlayerPrefs.GetInt("CurrentScore", 0);
int _highscore = PlayerPrefs.GetInt("HighScore", 0);
if (_score > _highscore)
PlayerPrefs.SetInt("HighScore", _score);
}
void Start()
{
gameOverTimer = 5.0f;
CheckHighscore();
scoreText.text = "Score: " + PlayerPrefs.GetInt("CurrentScore", 0);
highScoreText.text = "Highscore: " + PlayerPrefs.GetInt("HighScore", 0);
PlayerPrefs.Save();
}
//...
Abbiamo creato anche due contenitori public
per i nostri oggetti GUI Text, quindi al solito dobbiamo tornare in Unity e trascinare gli oggetti ScoreText
e HighScoreText
sulle relative variabili che troviamo tra le proprietà di Main Camera, nel pannello Inspector.
Abbiamo finito, premiamo play e giochiamo per un po', poi scegliamo di perdere e verifichiamo che tutto funzioni per bene!
UI e risoluzioni varibili
In questo tutorial abbiamo creato un componente della UI, che sarà visualizzato in modo relativo allo schermo.
Quando progettiamo le UI per dispositivi con risoluzioni variabili. è molto importante tenere a mente che ci servirà un'interfaccia flessibile. Possiamo ottenere questo adattamento alle divcerse risoluzioni creando un'interfaccia composta di diversi oggetti, che sia posizionata a una distanza pari al 10% della larghezza dello schermo, a partire dal lato sinistro.
Molti realizzano l'interfaccia con una singola immagine da scalare a seconda delle risoluzioni di gioco. Gli effetti di un simile approccio però possono non essere sempre belli o prevedibili. La UI di questo gioco sarà invece abbastanza simile su tutti i dispositivi: tablet, smartphone e PC.