#4 Space Invaders - Vola navicella....Vola - Unity Tutorial

Nel precedente articolo abbiamo creato un esercito di invasori. Ora è il momento di concentrarci sul nostro eroe: il player! Ma non parliamo di un semplice "giocatore". No no no, parliamo di una navicella spaziale in grado di volare tra le stelle e sparare laser senza sosta! Se siete pronti, allacciate le cinture e preparatevi a far volare questa navicella!


Proiettili laser

Prima di far volare la nostra navicella, creiamo la sua arma: i proiettili laser. Dalla cartella Sprites, trasciniamo lo sprite chiamato Laser nella nostra Hierarchy . Coloriamolo con lo stesso colore del player, in modo che l'utente sappia che si tratta del laser del player, Aggiungiamo un Box Collider 2D e spuntiamo la casella is Trigger. Successivamente, rendiamo il proiettile un componente fisico, aggiungendo un RigidBody 2D per simulare la fisica dell'oggetto, inclusi le collisioni. Impostiamo il Body Type del RigidBody 2D su Kinematic. Come abbiamo fatto in precedenza con gli invader, trasciniamo il nostro sprite dalla Hierarchy alla cartella prefab. Una volta fatto, possiamo eliminare il laser dalla Hierarchy. 

Selezioniamo il nuovo prefab e aggiungiamo un nuovo layer.


I layer servono a dividere gli oggetti in gruppi distinti, aiutando a controllare quali oggetti interagiscono tra di loro e in quale modo. Aggiungiamo i seguenti layer:

  • Player
  • Invader
  • Laser
  • Missile

Ora che abbiamo creato i nostri layer, torniamo al prefab Laser e aggiungiamo il corretto layer. Cambiamo il layer anche al nostro Player, infine andiamo sul nostro prefab Invader_Base e aggiungiamo il layer Invader. Ora andiamo in Edit -> Project Settings, clicchiamo sulla voce Physic 2D e noteremo una matrice di collisione che indica quali layer possono entrare in collisione con altri. Disattiviamo la collisione tra Player e Laser e tra Invader e Laser.



Ora possiamo passare alla scrittura del codice. Iniziamo a creare uno script in C# e chiamiamolo Projectile

using UnityEngine;

[RequireComponent(typeof(BoxCollider2D))]
public class Projectile : MonoBehaviour
{
    public float speed = 20f;
    public Vector3 direction = Vector3.up;
    public System.Action<Projectile> destroyed;
    public new BoxCollider2D collider { get; private set; }

    private void Awake()
    {
        collider = GetComponent<BoxCollider2D>();
    }

    private void OnDestroy()
    {
        if (destroyed != null) {
            destroyed.Invoke(this);
        }
    }

    private void Update()
    {
        transform.position += direction * speed * Time.deltaTime;
    }

    private void CheckCollision(Collider2D other)
    {
        Destroy(gameObject);
    }

    private void OnTriggerEnter2D(Collider2D other)
    {
        CheckCollision(other);
    }

    private void OnTriggerStay2D(Collider2D other)
    {
        CheckCollision(other);
    }

}


Vi spiego il codice appena scritto:
  • La riga @RequireComponent(typeof(BoxCollider2D)) è un attributo che specifica che il componente richiede la presenza di un BoxCollider2D per funzionare correttamente. Questo assicura che il componente sarà sempre presente sul GameObject al momento dell'esecuzione.
  • Sono state create diverse variabili pubbliche, come speed che definisce la velocità del proiettile, direction che definisce la direzione del movimento del proiettile, e destroyed che è un delegato che viene chiamato quando il proiettile viene distrutto.
  • Il metodo Awake inizializza il riferimento al componente BoxCollider2D.
  • Il metodo OnDestroy viene chiamato quando il componente viene distrutto. Se il delegato destroyed è stato assegnato, viene chiamato con il parametro del proiettile.
  • Il metodo Update sposta il GameObject nella direzione del vettore direction alla velocità speed.
  • I metodi CheckCollision, OnTriggerEnter2D e OnTriggerStay2D gestiscono le collisioni del proiettile con altri oggetti. In questo caso, quando il proiettile collide con un oggetto, viene distrutto.
Aggiungiamo lo script al prefab Laser.

Player

Ora passiamo a creare il player, gestire le sue movenze e sparare i laser.

using UnityEngine;

public class Player : MonoBehaviour
{
    public float speed = 5f;
    public Projectile laserPrefab;
    public System.Action killed;
    public bool laserActive { get; private set; }

    private void Update()
    {
        Vector3 position = transform.position;

        if (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow)) {
            position.x -= speed * Time.deltaTime;
        }
        else if (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow)) {
            position.x += speed * Time.deltaTime;
        }

        Vector3 leftEdge = Camera.main.ViewportToWorldPoint(Vector3.zero);
        Vector3 rightEdge = Camera.main.ViewportToWorldPoint(Vector3.right);

        // Clamp the position of the character so they do not go out of bounds
        position.x = Mathf.Clamp(position.x, leftEdge.x, rightEdge.x);
        transform.position = position;

        if (Input.GetKeyDown(KeyCode.Space) || Input.GetMouseButtonDown(0)) {
            Shoot();
        }
    }

    private void Shoot()
    {
        // Only one laser can be active at a given time so first check that
        // there is not already an active laser
        if (!laserActive)
        {
            laserActive = true;

            Projectile laser = Instantiate(laserPrefab, transform.position, Quaternion.identity);
            laser.destroyed += OnLaserDestroyed;
        }
    }

    private void OnLaserDestroyed(Projectile laser)
    {
        laserActive = false;
    }

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.gameObject.layer == LayerMask.NameToLayer("Missile") ||
            other.gameObject.layer == LayerMask.NameToLayer("Invader"))
        {
            if (killed != null) {
                killed.Invoke();
            }
        }
    }

}


  • La variabile speed rappresenta la velocità di movimento orizzontale del personaggio. 
  • La variabile laserPrefab è il prefab del laser che verrà istanziato quando il personaggio spara. 
  • La variabile laserActive è un flag booleano per controllare se un laser è attivo o meno, quindi gestire un solo laser alla volta sullo schermo.
  • Nel metodo Update(), il player viene mosso a sinistra o a destra in base all'input dell'utente, e viene verificato se il personaggio sta andando oltre i limiti della schermata. In caso affermativo, la posizione del personaggio viene bloccata ai limiti. Quando l'utente preme il tasto "spazio" o fa clic con il mouse, viene chiamato il metodo Shoot().
  • Il metodo Shoot() istanzia un nuovo laser solo se non c'è già un laser attivo. 
  • Il metodo OnLaserDestroyed() viene richiamato quando il laser viene distrutto. 
  • Nel metodo OnTriggerEnter2D viene chiamato quando il personaggio entra in collisione con un oggetto con il layer "Missile" o "Invader", viene eseguita l'azione "killed".

Ora è possibile testare il gioco e verificare il funzionamento del personaggio e dei suoi laser. Aggiungiamo al nostro Player lo script e aggiungiamo il prefab del laser alla voce Laser Prefab. 

Uccidiamo gli invaders

Il prossimo passo è far sì che l'invader colpito dal laser venga effettivamente eliminato. Per fare ciò, apriamo lo script Invader e aggiungiamo la collisione con il laser. Come fatto con lo script Projectile, dobbiamo creare un delegato che viene chiamato quando l'invader viene distrutto.
public System.Action<Invader> killed;

Successivamente aggiungiamo la nostra collisione.

private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.gameObject.layer == LayerMask.NameToLayer("Laser")) {
            killed?.Invoke(this);
        }
    }

Ora andiamo a modificare lo script Invaders 

    public AnimationCurve speed = new AnimationCurve();
    public System.Action<Invader> killed;

    public int AmountKilled { get; private set; }
    public int AmountAlive => TotalAmount - AmountKilled;
    public int TotalAmount => rows * columns;
    public float PercentKilled => (float)AmountKilled / (float)TotalAmount;

    private void Awake()
    {
        initialPosition = transform.position;

        // Form the grid of invaders
        for (int i = 0; i < rows; i++)
        {
            ......
            for (int j = 0; j < columns; j++)
            {
                // Create an invader and parent it to this transform
                Invader invader = Instantiate(prefabs[i], transform);
                invader.killed += OnInvaderKilled;

                ......
            }
        }
    }

    private void OnInvaderKilled(Invader invader)
    {
        invader.gameObject.SetActive(false);
        AmountKilled++;
        killed(invader);
    }

}


Vi spiego il codice appena scritto:
  • La variabile speed con l'aggiunta di AnimationCurve, definisce una curva di animazione che può essere utilizzata per controllare l'animazione dei nemici. 
  • La variabile AmountKilled restituisce il numero di invaders uccisi.
  • La variabile AmountAlive restituisce il numero di invaders ancora in vita.
  • La variabile TotalAmount restituisce il numero totale di invaders.
  • La variabile PercentKilled restituisce la percentuale di invasori uccisi rispetto al totale.
  • Nel metodo OnInvaderKilled(), viene chiamato quando viene ucciso un Invader e incrementiamo il contatore

Una volta salvato questo codice, clicchiamo sul GameObject Invaders dalla Hierarchy. Noteremo che è stata aggiunta l'opzione speed. Clicchiando su di essa, apparirà un grafico. Impostiamolo nel seguente modo.


Prima di concludere questo capitolo, affrontiamo la gestione degli attacchi missilistici da parte dei nostri invaders. L'obiettivo è far si che ad ogni X secondi gli invaders sparino dei missili.  Dalla cartella Sprites, trasciniamo lo sprite chiamato Missile nella nostra Hierarchy . Aggiungiamo un Box Collider 2D e spuntiamo la casella is Trigger. Successivamente, rendiamo il proiettile un componente fisico, aggiungendo un RigidBody 2D per simulare la fisica dell'oggetto, inclusi le collisioni. Impostiamo il Body Type del RigidBody 2D su Kinematic. Aggiungiamo lo script Projectile e configuriamo i valori Speed: 20, Direction Y: -1 e aggiungiamo il Layer creato in precedenza chiamato Missile
Come fatto in precedenza con gli invader, trasciniamo il nostro sprite dalla Hierarchy alla cartella dei prefab. Una volta completato ciò, possiamo rimuovere il laser dalla Hierarchy. 

L'ultimo step da fare consiste nell'andare a modificare la classe Invaders, aggiungendo il seguente codice. 

    [Header("Missiles")]
    public Projectile missilePrefab;
    public float missileSpawnRate = 1f;


private void Start()
    {
        InvokeRepeating(nameof(MissileAttack), missileSpawnRate, missileSpawnRate);
    }

    private void MissileAttack()
    {
        int amountAlive = GetAliveCount();

        // No missiles should spawn when no invaders are alive
        if (amountAlive == 0)
        {
            return;
        }

        foreach (Transform invader in transform)
        {
            // Any invaders that are killed cannot shoot missiles
            if (!invader.gameObject.activeInHierarchy)
            {
                continue;
            }

            // Random chance to spawn a missile based upon how many invaders are
            // alive (the more invaders alive the lower the chance)
            if (Random.value < (1f / (float)amountAlive))
            {
                Instantiate(missilePrefab, invader.position, Quaternion.identity);
                break;
            }
        }
    }

    public int GetAliveCount()
    {
        int count = 0;

        foreach (Transform invader in transform)
        {
            if (invader.gameObject.activeSelf)
            {
                count++;
            }
        }

        return count;
    }


Vi spiego il codice appena scritto:
  • La variabile missilePrefab corrisponde al prefab del missile. 
  • La variabile missileSpawnRate indica la cadenza di fuoco.
  • Nel metodo OnStart(), invoca in modo ripetitivo il metono MissileAttack
  • Nel metodo MissileAttack(), calcola quanti invasori sono ancora vivi richiamando il metono getAliveCount e in modo casuale lancia un missile.
Tornando alla Hierarchy, aggiungiamo il prefab del missile e testiamo il codice appena scritto. Per oggi è tutto, il prossimo capitolo sarà l'ultimo di questa serie. Affronteremo le ultime collisioni dei missili e ad aggiungeremo piccoli dettagli qua e là. Parte 5

Commenti

Post popolari in questo blog

#1 Design Patterns - Creational Design Patterns: Il Potere di Creare Oggetti con Stile

Inbox & Outbox pattern - La consegna dei messaggi ai tempi moderni