Articles

GetComponent in C#

In questo tutorial

  • Accesso a un altro script dall’interno dell’oggetto
  • Come funziona GetComponent?
  • Ottenere molti componenti
  • Interazione tra oggetti.
  • Interazione tra oggetti all’interno della gerarchia
  • SendMessage e BroadcastMessage
  • Funzioni interattive

Introduzione

Un problema ricorrente quando si inizia con Unity è come accedere ai membri di uno script da un altro script. Molti penserebbero che dereferenziare dal nome dello script sarebbe sufficiente, si rendono subito conto che non lo è.

Quando si sviluppa un programma, le variabili vengono memorizzate nella memoria in posizioni diverse. Se un oggetto potesse vedere altri membri dell’oggetto, ci sarebbe il rischio di modificarli anche se non era previsto. Se per le istanze due oggetti contengono lo stesso script con gli stessi nomi di variabili, il compilatore non sarebbe in grado di capire a quale ci riferiamo.

Per evitare questo problema, ogni oggetto in memoria non può vedere altri oggetti. È quindi necessario dire a un oggetto dove la variabile di cui ha bisogno è in memoria. L’intero principio del puntatore è stato abbandonato molto tempo fa e ora stiamo usando il riferimento (che in realtà usa il puntatore internamente).

Ci sono vari modi per accedere alle variabili, alcuni sono migliori di altri e alcuni sono semplicemente da utilizzare in situazioni particolari.

  1. GetComponent, il più comune, anche quello che confonde di più all’inizio
  2. SendMessage, potrebbe sembrare più facile da afferrare ma è anche meno efficiente.
  3. Variabili statiche, la più semplice ma anche la più complicata da comprendere appieno in un primo momento.

Impostazione di una scena

Crea una nuova scena, aggiungi un oggetto di gioco vuoto e chiamalo “StormTrooper”. Crea due script denominati “Salute”e “Forza”. Aggiungi entrambi gli script a StormTrooper.

Apri entrambi gli script nell’editor e aggiungi queste righe:

Salute.cs

using UnityEngine;using System.Collections;public class Health : MonoBehaviour{ private int health = 5; private bool hasForce = false; private void Update() { if (Input.GetKeyDown(KeyCode.Space)) { this.hasForce = Force.ReportForce(); if(hasForce == true) { this.health = 10; } } }}

Forza.cs

using UnityEngine;using System.Collections;public class Force : MonoBehaviour{ private bool force = false; private void Start() { this.force = true; } private void Update() { if (Input.GetKeyDown(KeyCode.P)) { string str = (this.force == true) ? " the": " no"); Debug.Log("I have" + str + " force"); } } public bool ReportForce() { return force; }}

Provare questo codice restituirà semplicemente l’errore “È necessario un riferimento all’oggetto per accedere a membri non statici”.

Dobbiamo dire al componente Salute sullo StormTrooper dove si trova il componente Forza in modo che possa accedere ai suoi membri. Poiché i nostri script sono entrambi sullo stesso oggetto, dobbiamo semplicemente dichiarare una variabile di tipo dello script a cui vogliamo accedere e utilizzare GetComponent per trovare lo script.

Accesso a un altro script all’interno dell’oggetto

Modifica la salute con:

using UnityEngine;using System.Collections;public class Health : MonoBehaviour{ private Force forceScript= null; public int health = 5; private bool hasForce = false; private void Start() { this.forceScript= GetComponent<Force>(); } private void Update() { if (Input.GetKeyDown(KeyCode.Space)) { this.hasForce = this.forceScript.ReportForce(); if(this.hasForce == true) { this.health = 10; } } }}

Con le modifiche, esegui il gioco, premi Spazio e poi premi P, il tuo StormTrooper ora ha 10 di salute (Nota che uno StormTrooper non ha la forza, questo è solo per l’esempio). Si accede alle variabili nello stesso modo in cui si accede ai metodi. Basta assicurarsi che i membri che si sta tentando di raggiungere sono dichiarati pubblici o si otterrà un errore.

Se i membri fossero privati, non sarebbe possibile accedervi. Tutto ciò che non dovrebbe essere raggiunto dall’esterno della classe/script dovrebbe essere privato per far fronte al principio di incapsulamento.

Come funziona GetComponent?

Per prima cosa dichiariamo un oggetto del tipo di script che vogliamo raggiungere.

private Force forceScript = null;

Force è il nostro tipo e forceScript è solo un nome che forniamo per utilizzare il riferimento nello script. Potremmo dargli un nome qualsiasi.

La funzione GetComponent guarderà all’interno dell’oggetto (l’oggetto di gioco StormTrooper) per trovare un componente corrispondente al tipo che abbiamo passato. Se non viene trovato, viene restituito un riferimento null e la nostra variabile forceScript non farà nulla. Se viene trovato un componente di tipo Force, l’indirizzo di tale componente viene passato alla variabile forceScript, la variabile ora punta al componente Force memorizzato da qualche parte nella memoria. Per accedere ai membri pubblici di Force, abbiamo solo bisogno di dereferenziare la variabile script usando l’operatore dot. Si dice che la variabile forceScript sia un handle per l’oggetto. Dà accesso a tutti i membri pubblici (e quelli interni).

script.PublicMembers;

I membri privati e protetti rimarranno inaccessibili.

L’utilizzo dell’attributo SerializeField consente di vedere le variabili in inspector despute essere private. La variabile forceScript inizia con il valore di None (Force) e si trasforma in un riferimento di forza. Se si fa clic su di esso, evidenzierà l’oggetto di destinazione.

reference_component

Quindi posso memorizzare nella cache il componente con GetComponent?

Sì, e si consiglia di farlo per alcuni componenti regolarmente utilizzati.

Quando si accede a componenti incorporati come Rigidbody, Transform, Renderer, etc ecc., Unity utilizzato per consentire l’accesso “diretto” in questo modo:

private void Update(){ this.transform.position += this.transform.forward; this.rigidbody.AddForce(force);}

Li hanno rimossi tutti tranne la trasformazione poiché c’è sempre una trasformazione su un dato oggetto di gioco.

GetComponent dovrebbe essere evitato all’interno dell’aggiornamento, quindi è consigliabile il caching con poche semplici righe di codice come:

private Transform myTransform = null;private Rigidbody rigidbody = null;private void Start(){ this.myTransform = this.gameObject.GetComponent<Transform>(); this.rigidbody = this.gameObject.GetComponent<Rigidbody>();}private void Update(){ this.myTransform.position += this.myTransform.forward; this.rigidbody.AddForce(force);}

Questo salverà alcuni calcoli.

Ottenere molti componenti

Cosa succede se il nostro StormTrooper ha molte Forze in sé (non è probabile però). Duplicare la forza due volte in modo StormTrooper ha tre di loro.

La salute diventa:

using UnityEngine;using System.Collections;public class Health : MonoBehaviour{ private Force scripts = null; private int health = 5; private void Start() { this.scripts = this.gameObject.GetComponents<Force>(); } private void Update() { if (Input.GetKeyDown(KeyCode.Space)) { for (int i = 0; i < this.scripts.Length; i++) { if(this.scripts.ReportForce()) { this.health += 5; } } } }}

Esegui il gioco, premi lo spazio, il nostro StormTrooper sta diventando un Jedi. GetComponents cerca tutti i componenti del tipo e ne restituisce un array. Questo va bene se non si target un componente specifico del tipo, ma più probabilmente tutti.

Vedere quanto segue accenderebbe e spegnerebbe le luci:

public class LightFlicker:MonoBehaviour{ private Light lights = null; private float timer = 0.0f private float clockTimer = 0.0f; private void Start(){ this.lights = this.gameObject.GetComponents<Light>(); this.clockTimer = Random.Range(0.5f, 2.0f); } private void Update(){ this.timer += Time.deltaTime; if(this.timer > this.clockTimer){ this.timer = 0.0f; this.clocktimer = Random.Range(0.5f, 2.0f); for(int i = 0; i < this.lights.Length; i++){ lights.enabled = !lights.enabled; } } }}

Interazione tra oggetti.

Possiamo anche ottenere due oggetti diversi per interagire in modo simile. Crea un nuovo oggetto chiamato “Sith”. Rimuovi i componenti duplicati dallo StormTrooper in modo che ne rimanga solo uno.

using UnityEngine;using System.Collections;public class DarkSide : MonoBehaviour { private const bool force = true; private Force script = null; private void Start () { this.script = GameObject.Find ("StormTrooper").GetComponent<Force>(); } private void Update () { if(Input.GetKeyDown (KeyCode.Space)) { UseForceToControl (); } } private void UseForceToControl() { if(this.script.ReportForce() == false) { Debug.Log ("Do as I say."); } }}

Modifica lo script di forza dello StormTrooper in modo che non abbia la forza (il Sith lo troverebbe sospetto…), ti do un indizio che succede nello script di avvio.

Di seguito è riportata la linea che trova lo StormTrooper e ottiene il suo componente di Forza.

private void Start () { this.script = GameObject.Find ("StormTrooper").GetComponent<Force>();}

GameObject.Find () trova prima l’oggetto passato come parametro e quindi cerca il componente richiesto al suo interno. Si consiglia di eseguire tali azioni all’inizio o in un momento del gioco in cui i ritardi non influenzeranno il gameplay. GameObject.Find e GetComponent sono funzioni costose poiché devono scorrere l’intera gerarchia. Se la tua scena include solo 10 oggetti, andrà bene, ma se ne contiene migliaia (un esercito di cloni) vedrai sicuramente un effetto negativo. Considera di chiamarli nella funzione Start. Nota che se la tua scena contiene molti StormTroopers, Uity non può sapere quale intendevi usare e restituirà il primo che trova. Molto probabilmente, si tiene un riferimento all’oggetto con cui si desidera interagire, ad esempio un callback di collisione.

Interazione tra oggetti all’interno della gerarchia

Si consideri un veicolo,, quando si guida, il carattere è collegato ad esso come un oggetto figlio. Il veicolo avrebbe caratteristica speciale se il conducente ha la forza (migliore maneggevolezza, più veloce e così via). Come l’autista è attaccato, è sotto nella gerarchia.
Il veicolo può trovare la componente di forza del conducente con lo stesso principio che abbiamo usato in precedenza (GameObject.Trovare) ma possiamo farlo accadere più velocemente indicando al veicolo che il conducente è nella propria gerarchia.

private Force script = null;private void Start () { this.script = this.transform.Find ("Sith").GetComponent<DarkSideForce>();}

GameObject viene scambiato per transform. In effetti, un oggetto figlio appartiene alla trasformazione del genitore, quindi dobbiamo esaminare la trasformazione per trovare il nostro oggetto. Nell’esempio, l’oggetto si trova proprio sotto l’oggetto chiamante, se il tuo Sith si trova all’interno dell’oggetto cockpit sotto l’oggetto car, devi fornire il percorso:

private void Start () { this.script = this.transform.Find ("Cockpit/Sith").GetComponent<DarkSideForce>();}

L’oggetto chiamante è l’Auto e viene riferito come questo nella chiamata. Poi Pozzetto viene cercato e Sith viene cercato. Se il percorso è sbagliato, Find restituisce null e si ottiene un’eccezione di riferimento Null. Puoi evitarlo in questo modo:

private void Start () { Transform tr = this.transform.Find ("Cockpit/Sith"); if(tr != null){ this.script = tr.GetComponent<DarkSideForce>(); }else{Debug.LogWarning("No component found");}}

transform.Trova restituisce una trasformazione mentre GameObject.Trova restituisce un GameObject.

Ancora meglio, possiamo usare:

private void Start () { this.script = GetComponentInChildren<DarkSideForce>();}

Ora il nostro veicolo può sapere che il nostro Sith ha la forza e applicare una caratteristica speciale.
Un problema da sapere, se l’oggetto genitore contiene uno script del tipo che stai cercando, lo restituirà. Quindi, pensando di avere a che fare con il componente figlio, in realtà hai a che fare con quello genitore.

In questo esempio, dobbiamo controllare per ogni tipo di driver se si tratta di uno StormTrooper o di un Sith. Un approccio migliore sarebbe stato quello di utilizzare l’ereditarietà in modo che la classe base superiore mantenesse la variabile force e una singola funzione sarebbe stata utilizzata per tutti i tipi di driver. Ma l’eredità e l’efficienza non erano il nostro problema principale qui.

SendMessage e BroadcastMessage

SendMessage e BroadcastMeassage sono altre due possibilità che possono essere più semplici ma anche più costose. Il principio rimane lo stesso, abbiamo un riferimento a un oggetto e vogliamo modificare uno script che contiene.

Con SendMessage, è possibile chiamare una funzione su un oggetto senza dover trovare un riferimento allo script. Ora mi dici: “Che cosa è tutta questa confusione giro se si può rendere semplice?”

SendMessage utilizza reflection ed è estremamente lento rispetto a GetComponent e dovrebbe essere considerato solo nei casi in cui non influenzerà il gioco o se non hai altra soluzione.

E questo dovrebbe sembrare logico, con GetComponent indichiamo esattamente dove si trova lo script in memoria. Con SendMessage, il compilatore conosce l’oggetto, ma passerà attraverso tutti gli script e le funzioni fino a trovare il fortunato vincitore. Questo può influenzare il tuo gioco se lo usi spesso su più oggetti e se l’oggetto ha molti componenti e script (che sono componenti a proposito).

SendMessage cerca solo l’oggetto a cui viene effettuata la chiamata, BroadcastMessage avvia una ricerca sull’oggetto di destinazione ma anche su tutti i suoi figli. La ricerca sarà ancora più lunga se l’oggetto contiene una grande gerarchia. Un nuovo arrivato è apparso con Unity4 SendMessageUpwards che avresti dovuto indovinare fa lo stesso di BroadcastMessage ma verso l’alto così tutti i genitori dell’oggetto fino a root.

Ok, prendiamo in considerazione il nostro StormTrooper ha una funzione per la ricezione di ordine

//Order.csusing UnityEngine;using System.Collections;public class Order : MonoBehaviour{ private void ReceiveOrder(string order) { switch(order) { case "Fetch": //Action Debug.Log("Can you repeat Sir?"); break; // other cases } }}

Ora consideriamo la nostra StormTrooper incontra un comandante dell’Impero:

// Commander.csprivate void Update(){ if(Input.GetKeyDown(KeyCode.Space)) { GameObject.Find("StormTrooper").SendMessage("ReceiveOrder","Fetch"); }}

Come si può vedere, il principio rimane lo stesso, ho bisogno di un riferimento all’oggetto di destinazione e quindi inviare un messaggio. Proprio come GetComponent è meglio se hai memorizzato nella cache il riferimento all’oggetto.

È anche possibile aggiungere un parametro per assicurarsi che il messaggio trovi un ricevitore o meno.

Si noti che non è possibile utilizzare una funzione che restituisce un valore. Dovresti quindi tornare indietro e usare GetComponent.

Funzioni interattive

Unity utilizza molte funzioni che fanno interagire due o più oggetti. Il vantaggio di queste funzioni è che memorizzano molte informazioni sull’altro oggetto.

  1. Funzioni di collisione OnTriggerXXXX/OnCollisionXXXX
  2. Funzioni fisiche XXXXcast

Eccoci con la dichiarazione di collisione:

private void OnCollisionEnter(Collision colReference){}private void OnTriggerEnter(Collider colReference){}

La chiamata della funzione crea una variabile di tipo Collision o Collider. La funzione riempirà questa istanza con informazioni sulla collisione e manterrà un riferimento all’altro oggetto con cui stiamo interagendo.

prviate void OnCollisionEnter(Collision colReference){ GameObject goRef = colReference.gameObject; if(goRef.CompareTag("StormTrooper")) { Health script = goRef.GetComponent<Health>(); script.health -= 5; }}

Si noti che non è necessario cercare l’altro oggetto poiché colReference è di tipo Collision e contiene un riferimento a quell’oggetto come membro. Ora posso eseguire il mio GetComponent come al solito e accedere alla mia variabile di salute pubblica.

Le funzioni della classe di Fisica hanno un principio simile. I dati non vengono memorizzati in un’istanza Collision/Collider ma in un’istanza RaycastHit:

using UnityEngine;using System.Collections;public class CheckFlat : MonoBehaviour { private void Update() { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit)) { float result = Vector3.Dot(hit.normal,Vector3.up); if(Mathf.Abs(result) > 0.9) { Debug.Log("Ground is flat enough"); } else { Debug.Log("Not Flat Enough"); } } }}

Dichiaro una variabile di tipo RaycastHit, la passo alla funzione. RaycastHit è una struttura e quindi un tipo di valore, ho bisogno della parola chiave out per creare un riferimento a quella struttura in modo che la funzione possa modificare la struttura all’interno. Se ti stai chiedendo di quel processo, controlla l’articolo di Gestione della memoria e la sezione sul valore/tipo di riferimento.
Quello che sto facendo qui è controllare quanto sia piatta la superficie su cui sto posizionando il cursore. Questo è utile per i giochi strategici in cui si desidera posizionare un edificio, ma non su una scogliera.
Io uso il prodotto dot del terreno normale con il Vector3.Up e se il risultato è abbastanza vicino a 1 significa che sono allineati, il terreno è abbastanza piatto per la costruzione.
Ovviamente, sono necessari altri calcoli in quanto questo è solo per un punto e dovresti controllare il normale, ma ottieni l’idea. Ho creato l’interazione tra la mia posizione del mouse e il terreno utilizzando un raycast.

Un raycast contiene un punto di partenza e una direzione. Nel nostro caso, il punto di partenza è la conversione della posizione del mouse in posizione mondiale e quindi una direzione basata sulla rotazione della telecamera e sulla matrice di proiezione. Questa linea va infinitamente nella direzione definita (puoi anche impostare una distanza massima) e se colpisce un collisore, le informazioni di quel collisore e collisione sono memorizzate nella struttura RaycastHit. La struttura contiene quindi un riferimento all’oggetto colpito.