In questa ultima parte del tutorial completiamo il gioco che abbiamo realizzato (Il progetto è scaricabile da qui), aggiungendo alcuni elementi di interfaccia utente, come il punteggio, la barra dell'energia, le schermate di inizio e di game over. Inoltre vediamo come introdurre altre funzionalità, nuovi nemici e bonus.
Aggiungere una interfaccia utente
Per aggiungere i punteggi utilizziamo la stessa tecnica vista nella lezione 5: iniziamo con il creare un nuovo GameObject vuoto (GameObject > Create Empty
), lo chiamiamo GUI e ne impostiamo la posizione a (0, 0, 0)
.
Quindi aggiugniamo un GameObject di tipo GUITexture (GameObject > Create Other > GUI Texture
), lo chiamiamo PlayerInfoTexture
e lo inseriamo sotto il GameObejct GUI
.
Nella cartella Textures troviamo poi l'immagine UI e ne impostiamo il Texture Type a GUI
nella finestra Inspector:
Quindi clicchiamo su Apply e trasciniamo questa texture nelle proprietà dell'oggetto PlayerInfoTexture. Poi impostiamo la posizione (0, 1, 0)
per questo oggetto e nel campo Pixel Inset impostiamo la X a 0
e Y a -58
.
Quindi aggiungiamo il testo che mostrerà il livello e l'esperienza del giocatore. Aggiungiamo un nuovo elemento GUI Text
(GameObject > Create Other > Gui Text
) e lo chiamiamo "LevelLabel". Lo innestiamo sotto all'oggetto GUI e ne impostiamo la posizione a (0.01, 0.99, 1)
, dimensione del font a 16 e scriviamo "Level" nel campo testo.
Duplichiamo LevelLabel
(Tasto destro e Duplicate) e chiamiamo il nuovo oggetto "XpLabel", scriviamo nel testo "35/100" e lo spostiamo in posizione (0.01, 0.95, 1)
.
Non ci resta che far funzionare la nostra semplice interfaccia. Modificheremo il testo nello script GameHandler.
Anzitutto dobbiamo rendere public
l'elenco levelList
del PlayerHandler:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class PlayerHandler : MonoBehaviour
{
float speed = 4.0f;
public GameObject laserPrefab;
float shootTimer = 0.0f;
float setShootTimerTo = 0.5f;
public int Xp = 0;
public int Level = 0;
public List levelList = new List();
//PlayerHandler playerHandler;
public int HP;
float colorModifier = 1.0f;
// ...
Salviamo e modifichiamo lo script GameHandler:
using UnityEngine;
using System.Collections;
public class GameHandler : MonoBehaviour
{
public int level;
public GameObject enemy;
public GameObject player;
public PlayerHandler playerHandler;
public GUIText levelLabel;
public GUIText xpLabel;
float spawnNewEnemyTimer = 10;
void Start() {
playerHandler = player.GetComponent<PlayerHandler>();
}
void Update()
{
if (playerHandler != null)
{
level = playerHandler.Level;
levelLabel.text = "Level " + (level+1);
int xpInLevel = playerHandler.Xp;
if (level > 0)
{
xpInLevel = playerHandler.Xp - playerHandler.levelList[level - 1];
}
int xpForNextLevel = (playerHandler.levelList[level]);
if(level > 0)
xpForNextLevel = playerHandler.levelList[level] - playerHandler.levelList[level - 1];
xpLabel.text = xpInLevel + "/" + xpForNextLevel;
}
// ...
}
// ...
}
Questo script calcola semplicemente quanti punti Xp servono per il livello attuale, quanti ne ha il giocatore e li mostra nella label dei punti esperienza. Anche il livello sarà aggiornato nella relativa label.
Importante come sempre assegnare alle variabili public gli oggetti cui devono fare riferimento. Clicchiamo sulla Main camera e trasciniamo gli elementi della GUI sulle rispettive variabili.
Un nuovo nemico
Per rendere ancora più interessante il gioco possiamo aggiungere secondo nemico: un robottino dorato. Visto che dovrà avere comportamenti simili al ragno, possiamo iniziare prendendo il prefab Enemy e portandolo sulla scena. Questo crea una nuovo oggetto che clona il prefab del ragno, lo modificheremo e ne faremo un nuovo prefab.
Una volta trascinato ilprefab Enemy sulla scena, diamogli posizione (0,0,0)
e modifichiamone il nome in enemy2
. Quindi nella finestra hierarchy cancelliamo le tre animazioni del ragno e creiamo quelle del robottino.
Nella cartella Textures/Enemy
troviamo lo sprite GoldenBot. Al solito impostiamo Sprite Mode su Multiple e grazie allo Sprite Editor lo suddivideremo in fotogrammi da 96x128px.
Fatto questo possiamo al solito creare le tre animazioni e i relativi GameObject (BotWalk
, BotIdle
e BotDead
), che collegheremo alle relative azioni sull'AnimationHandler del nuovo nemico.
Non ci resta che trascinare Enemy2 nella cartella Prefabs e cancellarlo dalla scena.
Aggiungeremo anche ai nemici i punti vita, inseriamo quindi una variabile public Hp nell'EnemyHandler. Poi modifichiamo il codice per far sì che i nemici non muoiano subito ma perdano un po' di energia alla volta. Solo quando l'avranno persa tutta saranno distrutti:
public class EnemyHandler : MonoBehaviour
{
float speed = 4.0f;
GameObject player;
public GameObject explodePrefab;
public GameObject smokePrefab;
bool isDead = false;
float isDeadTimer = 0.5f;
public int enemyDifficulty;
public int HP; // introdurre i punti vita
AnimationHandler animationHandler;
PlayerHandler playerHandler;
void OnCollisionEnter(Collision collision)
{
if (collision.rigidbody != null)
{
if (collision.rigidbody.name == "Player")
{
Instantiate(explodePrefab, this.transform.position, Quaternion.identity);
Instantiate(smokePrefab, this.transform.position, Quaternion.identity);
Destroy(this.gameObject);
}
if (collision.rigidbody.CompareTag("Laser"))
{
HP -= 1;
Instantiate(smokePrefab, this.transform.position, Quaternion.identity);
if (HP <= 0)
{
isDead = true;
Destroy(collision.gameObject);
if (animationHandler != null)
{
animationHandler.ChangeAnimationState(2);
}
}
}
}
}
// ...
Ora possiamo assegnare HP=2
al prefab Enemy2, nella finestra Inspector di Unity (non abbiamo bisogno di impostare HP se il nemico ha solo una vita: il test infatti viene effettuato solo in caso di collisione).
Nello script GameHandler, dichiariamo una nuova variabile public che conterrà il prefab Enemy2, poi non resta che impostare la generazione anche per i nemici robottini, dopo il superamento del primo livello.
using UnityEngine;
using System.Collections;
public class GameHandler : MonoBehaviour
{
public int level;
public GameObject player;
public GameObject enemy;
public GameObject enemy2;
public GUIText levelLabel;
public GUIText xpLabel;
public PlayerHandler playerHandler;
float spawnNewEnemyTimer = 10;
void Start() {
playerHandler = player.GetComponent<PlayerHandler>();
}
void Update()
{
if (playerHandler != null)
{
level = playerHandler.Level;
levelLabel.text = "Level " + (level+1);
int xpInLevel = playerHandler.Xp;
if (level > 0)
{
xpInLevel = playerHandler.Xp - playerHandler.levelList[level - 1];
}
int xpForNextLevel = (playerHandler.levelList[level]);
if(level > 0)
xpForNextLevel = playerHandler.levelList[level] - playerHandler.levelList[level - 1];
xpLabel.text = xpInLevel + "/" + xpForNextLevel;
}
spawnNewEnemyTimer -= Time.deltaTime;
if (spawnNewEnemyTimer <= 0)
{
spawnNewEnemyTimer = 5 - (level * 0.25f);
int spawnNumberOfEnemies = 1 + (level / 3);
for (int i = 0; i < spawnNumberOfEnemies; i++)
{
GameObject enemyToSpawn;
enemyToSpawn = enemy;
if (level > 2)
{
float rndEnemy = Random.Range(0.0f, 1.0f);
if (rndEnemy >= 0.5)
enemyToSpawn = enemy;
else
enemyToSpawn = enemy2;
}
float modifier = Random.Range(0.0f, 1.0f);
Instantiate(enemyToSpawn, new Vector3(player.transform.position.x + 20.0f + i * 3,
player.transform.position.y + modifier, 0.0f), Quaternion.identity);
}
}
}
}
Ricordiamo come sempre di collegare il prefab Enemy2 alla variabile enemy2
del GameHandler sulla Main Camera.
Disegnare la barra dell'energia
La prossima cosa che vogliamo fare è un elemento di UI che mostri l'energia residua dei personaggi.
Duplichiamo Levellabel
e chiamiamo il nuovo oggetto HpLabel
, poi impostiamone le proprietà: testo = "||||||||||"
; posizione = (0.01, 0.95, 1)
; Font size: 60
.
Ora modifichiamo lo script GameHandler per impostare la label con il valore di HP:
upublic class GameHandler : MonoBehaviour
{
public int level;
public GameObject player;
public GameObject enemy;
public GameObject enemy2;
public GUIText levelLabel;
public GUIText xpLabel;
public GUIText hpLabel;
public PlayerHandler playerHandler;
float spawnNewEnemyTimer = 2;
void Start() {
playerHandler = player.GetComponent<PlayerHandler>();
}
void Update()
{
if (playerHandler != null)
{
level = playerHandler.Level;
levelLabel.text = "Level " + (level+1);
int xpInLevel = playerHandler.Xp;
if (level > 0)
{
xpInLevel = playerHandler.Xp - playerHandler.levelList[level - 1];
}
int xpForNextLevel = (playerHandler.levelList[level]);
if(level > 0)
xpForNextLevel = playerHandler.levelList[level] - playerHandler.levelList[level - 1];
xpLabel.text = xpInLevel + "/" + xpForNextLevel;
hpLabel.text = "[ ";
for (int i = 0; i < playerHandler.HP; i++)
{
hpLabel.text += "|";
}
hpLabel.text += "] ";
if (playerHandler.HP <= 3)
hpLabel.color = Color.red;
else hpLabel.color = new Color(88.0f / 255.0f, 82.0f / 255.0f, 75.0f / 255.0f);
}
spawnNewEnemyTimer -= Time.deltaTime;
if (spawnNewEnemyTimer <= 0)
{
spawnNewEnemyTimer = 5 - (level * 0.25f);
int spawnNumberOfEnemies = 1 + (level / 3);
for (int i = 0; i < spawnNumberOfEnemies; i++)
{
GameObject enemyToSpawn;
enemyToSpawn = enemy;
if (level > 2)
{
float rndEnemy = Random.Range(0.0f, 1.0f);
if (rndEnemy >= 0.5)
{
enemyToSpawn = enemy;
}
else
{
enemyToSpawn = enemy2;
}
}
float modifier = Random.Range(0.0f, 1.0f);
Instantiate(enemyToSpawn, new Vector3(player.transform.position.x + 20.0f + i * 3,
player.transform.position.y + modifier, 0.0f), Quaternion.identity);
}
}
}
}
Avremo tante lineette per quanti punti energia ci rimangono e, quando saremo in riserva, la barra si colorerà per ricordarcelo. Ricordiamo ancora di impostare la label tra le proprietà della Main Camera.
I bonus energia
Il prossimo passo è quello di aggiungere un bonus energia (un power-up) che faccia acquisire al giocatore parte dell'energia persa. Al solito aggiungiamo un nuovo GameObject vuoto alla scena e chiamiamolo HPPowerUp
, quindi creiamo un nuovo materiale chiamato HPPowerUp
e aggiungiamo la texture omonima che troviamo nella cartella Textures. Alla texture assegnamo il Texture Type "GUI". Al materiale invece assegnamo lo shader Transparent/Cutout/Soft Edge Unit
.
Creiamo un nuovo Quad (che chiamiamo "Texture") al GameObject HPPowerUp e vi applichiamo il materiale appena creato. Impostiamo le dimensioni (Scale
) di Texture come (0.46, 0.84, 1)
.
Poi aggiungiamo un nuovo tag "PowerUp" e lo associamo all'oggetto HPPowerUp. Aggiungiamo anche Box Collider e un Rigidbody. Per quest'ultimo impostiamo tutti i vincoli di movimento (freeze position e rotation) e togliamo la gravità come sempre. Per il Box Collider basterà ridimensionare X e Y (0.46 e 0.84, come la texture).
Ora non resta che trascinare il GameObject HPPowerUp nella cartella Prefabs per ottenere un prefab della boccetta-bonus. Poi possiamo cancellare l'oggetto dalla scena. Ricordiamoci anche di cancellare il mesh collider dall'oggetto Texture.
Ora nel GameHandler aggiungiamo una variabile public per l'oggetto PowerUp e implementiamo la semplice logica che ne istanzierà diversi in modo casuale durante il gioco:
public class GameHandler : MonoBehaviour
{
// ...
public GameObject hpPowerUp;
// ...
void Start() {
// ...
}
void Update()
{
// ...
spawnNewEnemyTimer -= Time.deltaTime;
if (spawnNewEnemyTimer <= 0)
{
// ...
float rndPowerupHp = Random.Range(0.0f, 1.0f);
if (rndPowerupHp < 0.1)
{
Instantiate(hpPowerUp, new Vector3(player.transform.position.x + 30.0f,
player.transform.position.y, 0.0f), Quaternion.identity);
}
}
}
}
Nello script PlayerHandler, controlliamo la collisione contro un bonus e nel caso lo cancelliamo dalla scena e aggiungiamo punti vita (non oltrepassando i 10).
if (collision.rigidbody.CompareTag("PowerUp"))
{
Destroy(collision.gameObject);
HP += 5;
if (HP > 10) HP = 10;
}
Qual è lo scopo del gioco? Ottenere quanti più punti esperienza e raggiungere i livelli più alti. Quando arrivia il fatidico Game Over vogliamo salvare il miglior risultato sia in termini di punteggio che di livello. Tutto nel PlayerPrefs.
Il menu principale e la schermata di Game Over
Iniziamo cercando le texture SteamLandsLogo e GameOver e impostando per entrambe il tipo (Texture Type) GUI. Ora creiamo una nuova scena e la salviamo come MainMenu.
Aggiungiamo un elemento Gui Texture (GameObject > Create Other > Gui Texture
) e lo chiamiamo "MainTexture" e utilizziamo SteamLandsLogo come texture per questo oggetto. Utilizziamo il seguente setup:
position: 0.5, 0.5, 0
scale : -0.26, -0.37, 1
Pixel Inset:
X = -512
y = -384
Ora creiamo uno script chiamato MainMenu, lo assegnamo alla Main Camera e lo modifichiamo in questo modo:
Create a new script called MainMenu, add it to the Main Camera and modify it:
using UnityEngine;
using System.Collections;
using UnityEngine;
using System.Collections;
public class MainMenu : MonoBehaviour {
float startTimer = 2.0f;
void Start () { }
void Update () {
startTimer -= Time.deltaTime;
if (startTimer <= 0)
{
if (Input.GetMouseButtonDown(0))
{
Application.LoadLevel("Level");
}
}
}
}
Abbiamo inserito un breve timer perché il gioco non parta cliccando accidentalmente all'inizio.
Clicchiamo ancora sulla Main Camera e impostiamo il colore di sfondo (RGBA: 203, 214, 192, 5). Clicchiamo su File->Build Settings
e aggiungiamo la scena corrente. Poi andiamo sulla cartella Scenes e trasciniamo nella finestra Build Setting anche la scena Level. Otterremo gli ID per le due scene:
Aggiungiamo una nuova scena e la salviamo come GameOver e rifacciamo quanto già fatto per MainMenu, ma utilizzando la texture GameOver. Inseriamo un oggetto GUI Text alla scena e lo chiamiamo "ScoreLabel".
Creiamo uno script GameOver e lo aggiungiamo alla Main Camera (al posto di MainMenu).
using UnityEngine;
using System.Collections;
public class GameOver : MonoBehaviour {
float gameOverTimer = 5.0f;
public GUIText scoreLabel;
int score = 0;
int level = 0;
int highscore = 0;
// Use this for initialization
void Start () {
score = PlayerPrefs.GetInt("Score", 0);
level = PlayerPrefs.GetInt("Level", 1);
highscore = PlayerPrefs.GetInt("HighScore", 0);
if (score > highscore)
{
highscore = score;
}
scoreLabel.text = "Level " + level + " / Score: " + score + "\nHigh score: " + highscore;
}
// Update is called once per frame
void Update () {
gameOverTimer -= Time.deltaTime;
if (gameOverTimer <= 0)
{
scoreLabel.text = "Tap to restart";
if(Input.GetMouseButtonDown(0))
{
Application.LoadLevel("MainMenu");
}
}
}
}
L'ultima cosa, molto importante, è quella di rilevare lo stato di Game Over nel gioco. Se abbiamo finito l'energia impostiamo l'animazione della morte, registriamo il punteggio e lanciamo la scena di GameOver. Modifichiamo così lo script PlayerHandler:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class PlayerHandler : MonoBehaviour
{
float speed = 4.0f;
public GameObject laserPrefab;
float shootTimer = 0.0f;
float setShootTimerTo = 0.5f;
public int Xp = 0;
public int Level = 0;
public List<int> levelList = new List<int>();
public int HP;
float colorModifier = 1.0f;
bool isGameOver = false;
float gameOverTimer = 3.0f;
AnimationHandler animationHandler;
void OnCollisionEnter(Collision collision)
{
if (collision.rigidbody != null)
{
if (collision.rigidbody.CompareTag("Enemy"))
{
HP -= 1;
colorModifier = 0.0f;
}
if (collision.rigidbody.CompareTag("PowerUp"))
{
Destroy(collision.gameObject);
HP += 5;
if (HP > 10) HP = 10;
}
}
}
void Start()
{
for (int i = 0; i < 20; i++)
{
levelList.Add((int)(50 + (i * 50) + (i * 50 * 0.25)));
}
animationHandler = this.GetComponent<AnimationHandler>();
}
void Update()
{
if (HP <= 0)
{
isGameOver = true;
animationHandler.ChangeAnimationState(2);
gameOverTimer -= Time.deltaTime;
if (gameOverTimer <= 0.0f)
{
PlayerPrefs.SetInt("Score", Xp);
PlayerPrefs.SetInt("Level", Level);
Application.LoadLevel("GameOver");
}
}
if (!isGameOver)
{
Vector3 movePlayerVector = Vector3.right;
shootTimer -= Time.deltaTime;
((SpriteRenderer)this.GetComponentInChildren<SpriteRenderer>()).color =
new Color(1.0f, colorModifier, colorModifier);
if (colorModifier < 1.0f)
colorModifier += Time.deltaTime;
//if (playerHandler.Xp >= levelList[Level])
if (Xp >= levelList[Level])
{
Level++;
}
if (Input.GetMouseButton(0))
{
Vector3 touchWorldPoint = Camera.main.ScreenToWorldPoint(
new Vector3(Input.mousePosition.x,
Input.mousePosition.y,
10.0f));
if (touchWorldPoint.x < this.transform.position.x + 5.0f)
{
if (touchWorldPoint.y > this.transform.position.y)
{
movePlayerVector.y = 1.0f;
}
else movePlayerVector.y = -1.0f;
}
else
{
if (shootTimer <= 0)
{
Vector3 shootPos = this.transform.position;
shootPos.x += 2;
Instantiate(laserPrefab, shootPos, Quaternion.identity);
shootTimer = setShootTimerTo;
}
}
}
this.transform.position += movePlayerVector * Time.deltaTime * speed;
// limite per il marciapiede
if (transform.position.y > -2.0)
{
transform.position = new Vector3(transform.position.x,
-2.0f,
transform.position.z);
}
if (transform.position.y < -5.5)
{
transform.position = new Vector3(transform.position.x,
-5.5f,
transform.position.z);
}
}
}
}
Questo script disabilita l'input utente quando l'energia è finita e inizia a contare qualche secondo per lasciare al giocatore il tempo di capire che il personaggio è morto. Poi salva i risultati e lancia la scena di game over (ricordiamoci di aggiungere la scena di GameOver nei Build Settings...).
Il gioco è praticamente finito ma ...c'è sempre qualcosa da poter aggiungere.