In questa lezione affronteremo tutti gli aspetti delle collisioni, sia quelle che riguardano le simulazioni fisiche, sia quelle relative ai cosiddetti trigger, ovvero elementi intangibili che possono essere usati come interruttori.
Il componente Collider
Alla base delle collisioni in Unity si trova il componente Collider. In realtà, Collider è la classe base da cui ereditano tutti i vari tipi di collider, ognuno dei quali ha una forma diversa. In una situazione reale troveremo sugli oggetti di gioco dei BoxCollider, SphereCollider, e così via, o una combinazione di questi a seconda della forma dell'oggetto.
I Collider non devono per forza corrispondere come forma alla geometria reale di un oggetto. Ad esempio, se la visuale di gioco è sufficentemente distante e non è necessario simulare le collisioni per davanzali, finestre, o per le antenne, un intero palazzo potrebbe avere un solo BoxCollider, ovvero a forma di semplice parallelepipedo.
Se serve una collisione più dettagliata, Unity mette a disposizione il componente MeshCollider, che vedremo più avanti.
Eventi e ottimizzazione
Una volta applicato un componente Collider ad un GameObject, esso inizia a rilevare le collisioni con altri Collider nella scena e invierà a tutti gli script tre eventi:
Evento | Descrizione |
---|---|
OnCollisionEnter | avviene nel frame in cui la collisione è iniziata |
OnCollisionExit | viene attivato nel frame in cui finisce la collisione |
OnCollisionStay | è attivo in tutti i frame in cui la collisione permane |
Sarà nostro compito gestire questi eventi e programmare eventuali azioni di risposta.
Tuttavia, indipendentemente da questo, Unity eseguirà comunque i check della fisica e delle collisioni. Perciò se un oggetto non dovrà collidere con altri, è consigliato rimuovere eventuali Collider perché di fatto aggiungono calcoli inutili durante il gioco.
OnCollisionEnter, rilevare le collisioni
Per agire conseguentemente ad una collisione, bisogna aggiungere uno script allo stesso oggetto che possiede il collider (creando uno script e trascinandolo sull'inspector dell'oggetto), poi occorre inserire nello script uno degli handler citati prima, ad esempio OnCollisionEnter
.
Tutti gli handler per questi eventi devono prendere come parametro un oggetto di tipo Collision, che ci permette di operare sulla collisione leggendone parametri come forza, direzione, e punti di contatto. Ad esempio:
private void OnCollisionEnter(Collision coll)
{
if(coll.relativeVelocity.magnitude >= 10f)
{
Destroy(gameObject);
Destroy(coll.gameObject);
}
}
In questo caso, nell'esempio utilizziamo la proprietà magnitude di relativeVelocity per rilevare urti più forti di una certa quantità decisa da noi. Se se ne verificano, distruggiamo sia l'oggetto che possiede questo script (gameObject) sia quello che ha urtato con lui (coll.gameObject
).
OnCollisionStay, gestire ciò che accade durante la collisione
Esaminiamo un altro esempio: supponiamo di avere un prefab con un effetto di scintille, e di volerlo creare in corrispondenza dei punti di contatto fra due oggetti, e vogliamo farlo per tutta la durata della collisione (magari l'oggetto struscia sul pavimento...). Usiamo OnCollisionStay, ed ogni 25 frame istanziamo un nuovo prefab in corrispondenza dei punti di contatto (ovvero coll.contacts
):
private void OnCollisionStay(Collision coll)
{
if(Time.frameCount % 25 == 0)
{
foreach(ContactPoint c in coll.contacts)
{
Instantiate(sparksPrefab, c.point, Quaternion.identity);
}
}
}
Rigidbody
È importante notare che oggetti che vengono mossi mediante il loro componente Transform (ovvero quindi modificandone la transform.position
, oppure tramite transform.Translate
, o con transform.Rotate
) non generano i messaggi riguardanti la fisica che abbiamo appena visto.
In altre parole un oggetto mosso così compenetrerà altri Collider come se non ci fosse stata alcuna collisione!
Per ricevere i messaggi, è necessario aggiungere un componente Rigidbody o muovere l'oggetto in altro modo con rigidbody.ApplyForce
o rigidbody.ApplyTorque
(come vedremo più avanti nella lezione sui Rigidbody).
MeshCollider
Invece di utilizzare una semplice forma geometrica come un cubo, una sfera o una capsula, il MeshCollider calcola le collisioni sulla base di un'effettiva geometria scelta.
Bisogna tener presente che, proprio per questa maggior precisione, il MeshCollider è il più pesante dei Collider da calcolare, percià è meglio evitarlo quando non strettamente necessario. Inoltre, un MeshCollider può collidere con un collider semplice ma non può collidere con un altro MeshCollider, a meno che uno dei due non sia impostato come "Convex".
Quando un collider è Convex, la mesh che lo compone viene ricalcolata e semplificata in modo da essere convessa. Questo semplifica anche i calcoli delle collisioni e permette al collider di toccare un altro MeshCollider, ma ha un limite di 255 triangoli.
Ecco un esempio di come un collider per una spada risulta nella sua versione normale, e convessa:
Mentre per Box e Sphere Collider l'Inspector presenta i valori delle dimensioni e/o del centro, nel caso di un MeshCollider questo non è presente e la dimensione dipende solo dalla mesh scelta (anche questo da Inspector).
Questo permette risvolti interessanti: un oggetto può avere graficamente un certo aspetto ma un collider di forma diversa, oppure avere molti poligoni per la mesh da renderizzare, ma avere un collider con un numero di triangoli molto ridotto (per semplificare i calcoli della fisica).
Matrici di collisione basate sui layer
È possibile ottimizzare la fisica di un gioco abilitando le collisioni solo per gli oggetti appartenenti ad alcuni layer. Possiamo anche usare questa funzione per creare degli effetti particolari.
Facendo un esempio, in uno shooter potremmo avere delle reti di fil di ferro, e volere che i proiettili non le colpiscano in modo da poter sparare attraverso di esse. Chiaramente i personaggi dovranno collidere, altrimenti le attraverserebbero come se non ci fossero. Per costriuire un sistema tale, andiamo su Edit > Project Settings > Tags and Layers e creiamo i layer che ci servono:
Se ora assegnamo ogni oggetto al giusto layer, e poi andiamo in Edit > Project Settings > Physics, sotto Collision Matrix troveremo una matrice che permette di decidere per ogni layer, con quali layer Unity deve effettivamente rilevare le collisioni, e con quali ignorarle.
Il funzionamento è semplice: se nella casella che si trova nell'incrocio fra una riga ed una colonna c'è una spunta, gli oggetti appartenenti al layer riga o al layer colonna collideranno fra loro durante il gioco.
Organizziamo la matrice così:
In questo modo, i personaggi collideranno con tutto (pavimento, reti, proiettili), i proiettili passeranno attraverso le reti, e inoltre non urteranno fra loro (sarebbe quantomeno irreale e difficile da gestire), mentre non verranno neanche tenute in considerazione collisioni fra pavimento e pavimento, e reti con reti (sono oggetti statici quindi non devono toccarsi).
Così facendo abbiamo ottenuto l'effetto voluto ed alleggerito i calcoli che Unity deve fare per la fisica ogni frame, che sono notoriamente dispendiosi, soprattutto su mobile.
I trigger
I trigger sono una versione speciale di Collider
, deputati alla creazione di oggetti non tangibili, di cui però si vogliono comunque rilevare le collisioni. È il caso ad esempio di un gioco d'avventura, in cui di solito si usano dei cubi invisibili per creare delle "aree interruttore" (trigger, appunto) in cui appena il personaggio vi entra, succede qualcosa (si chiude la porta dietro di lui, inizia una sfida, ecc.).
Per creare trigger, è necessario spuntare la casella Is Trigger nell'Inspector di un qualunque collider. Così facendo l'oggetto potrà compenetrare altri collider e non li spingerà via.
In più, i trigger non emettono i tre eventi OnCollisionEnter
, OnCollisionExit
, OnCollisionStay
, ma una versione speciale chiamata OnTriggerEnter, OnTriggerExit, OnTriggerStay. Questi messaggi funzionano in modo molto simile agli altri, ma posseggono un parametro che non è di tipo Collision
ma Collider
(ovvero il collider che ha toccato l'oggetto). Per questo motivo contengono meno informazioni, ma sono anche più leggere da calcolare delle controparti OnCollision.
Ad esempio:
private void OnTriggerEnter(Collider otherCollider)
{
if(otherCollider.gameObject.tag == "Player")
{
GameManager.Instance.CloseDoor();
}
}
In questo esempio, usiamo OnTriggerEnter per rilevare quando il giocatore entra in una stanza. Poiché il trigger potrebbe scattare anche nel caso in cui un nemico o un proiettile passi la porta, nella funzione leggiamo la proprietà otherCollider.gameObject.tag
dell'altro oggetto per chiamare CloseDoor solo se è il giocatore ad entrare.
Così come per i collider normali, i trigger chiamano queste funzioni solo se almeno uno dei due oggetti coinvolti nello scontro possiede un Rigidbody 'non kinematic' (che vedremo nella prossima lezione sui Rigidbody).