Articles

GetComponent en C #

Dans ce tutoriel

  • Accéder à un autre script depuis l’intérieur de l’objet
  • Comment fonctionne GetComponent ?
  • Obtenir de nombreux composants
  • Interaction entre les objets.
  • Interaction entre les objets de la hiérarchie
  • SendMessage et BroadcastMessage
  • Fonctions interactives

Introduction

Un problème récurrent lors du démarrage avec Unity est de savoir comment accéder aux membres d’un script à partir d’un autre script. Beaucoup penseraient que le déréférencement à partir du nom du script serait suffisant, ils réalisent rapidement que ce n’est pas le cas.

Lors du développement d’un programme, les variables sont stockées dans la mémoire à différents endroits. Si un objet pouvait voir d’autres membres de l’objet, il y aurait un risque de les modifier même si cela n’était pas prévu. Si, par exemple, deux objets contiennent le même script avec les mêmes noms de variables, le compilateur ne serait pas en mesure de déterminer celui auquel nous faisons référence.

Pour éviter ce problème, chaque objet en mémoire ne peut pas voir d’autres objets. Il est alors nécessaire de dire à un objet où se trouve la variable dont il a besoin en mémoire. Tout le principe du pointeur a été abandonné il y a longtemps et maintenant nous utilisons la référence à la place (qui utilise en fait le pointeur en interne).

Il existe différentes façons d’accéder aux variables, certaines sont meilleures que d’autres et certaines doivent simplement être utilisées dans des situations particulières.

  1. GetComponent, le plus courant, aussi celui qui déroute le plus au début
  2. SendMessage, il peut sembler plus facile à saisir mais il est également moins efficace.
  3. Variables statiques, les plus faciles mais aussi les plus compliquées à comprendre au début.

Configuration d’une scène

Créez une nouvelle scène, ajoutez un objet de jeu vide et nommez-le « StormTrooper ». Créez deux scripts nommés « Santé » et « Force ». Ajoutez les deux scripts à StormTrooper.

Ouvrez les deux scripts dans l’éditeur et ajoutez ces lignes :

Santé.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; } } }}

Force.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; }}

Essayer ce code renverra simplement l’erreur « Une référence d’objet est requise pour accéder aux membres non statiques ».

Nous devons indiquer au composant de santé sur le StormTrooper où se trouve le composant de force afin qu’il puisse accéder à ses membres. Puisque nos scripts sont tous les deux sur le même objet, nous devons simplement déclarer une variable de type du script auquel nous souhaitons accéder et utiliser GetComponent pour trouver le script.

Accéder à un autre script à l’intérieur de l’objet

Modifier la santé avec:

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; } } }}

Avec les modifications, lancez le jeu, appuyez sur Espace puis appuyez sur P, votre StormTrooper a maintenant 10 de santé (Notez qu’un StormTrooper n’a pas la force, c’est seulement pour l’exemple). Vous accédez aux variables de la même manière que vous avez accédé aux méthodes. Assurez-vous simplement que les membres que vous essayez d’atteindre sont déclarés publics ou vous obtiendrez une erreur.

Si les membres étaient privés, il ne serait pas possible d’y accéder. Tout ce qui ne doit pas être atteint depuis l’extérieur de la classe / du script doit être privé pour respecter le principe d’encapsulation.

Comment fonctionne GetComponent ?

Nous déclarons d’abord un objet du type du script que nous voulons atteindre.

private Force forceScript = null;

Force est notre type et forceScript est juste un nom que nous fournissons pour utiliser la référence dans le script. On pourrait lui donner n’importe quel nom.

La fonction GetComponent regardera à l’intérieur de l’objet (l’objet de jeu StormTrooper) pour trouver un composant correspondant au type que nous avons passé. Si aucun n’est trouvé, une référence nulle est renvoyée et notre variable forceScript ne fera rien. Si un composant de type Force est trouvé, l’adresse de ce composant est transmise à la variable forceScript, la variable pointe maintenant vers le composant Force stocké quelque part en mémoire. Pour accéder aux membres publics de Force, il suffit de déréférencer la variable de script à l’aide de l’opérateur dot. On dit que la variable forceScript est un handle de l’objet. Il donne accès à tous les membres publics (et internes).

script.PublicMembers;

Les membres privés et protégés resteront inaccessibles.

L’utilisation de l’attribut SerializeField permet de voir que les variables de l’inspecteur despute sont privées. Votre variable forceScript commence par la valeur None (Force) et se transforme en référence de force. Si vous cliquez dessus, il mettra en évidence l’objet cible.

reference_component

Pour que je puisse mettre en cache un composant avec GetComponent ?

Oui, et il est recommandé de le faire pour certains composants régulièrement utilisés.

Lors de l’accès à des composants intégrés tels que Rigidbody, Transform, Renderer,etcetc, Unity permettait un accès « direct » comme ceci:

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

Ils les ont tous supprimés sauf la transformation car il y a toujours une transformation sur un objet de jeu donné.

GetComponent doit être évité dans la mise à jour, donc la mise en cache est recommandée avec quelques lignes de code simples comme:

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);}

Cela économisera un peu de calcul.

Obtenir de nombreux composants

Que se passe-t-il si notre StormTrooper a beaucoup de force en lui-même (peu probable cependant). Dupliquez la Force deux fois pour que StormTrooper en ait trois.

La santé devient:

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; } } } }}

Lancez le jeu, appuyez sur Espace, notre StormTrooper devient un Jedi. GetComponents recherche tous les composants du type et en renvoie un tableau. C’est bien si vous ne ciblez pas un composant spécifique du type mais plus probablement tous.

Voir ce qui suit allumerait et éteindrait les lumières:

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; } } }}

Interaction entre les objets.

Nous pouvons également obtenir que deux objets différents interagissent de manière similaire. Créez un nouvel objet nommé « Sith ». Retirez les composants dupliqués du StormTrooper pour qu’il n’en reste plus qu’un.

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."); } }}

Modifiez le script de force du StormTrooper pour qu’il n’en ait pas la force (les Sith trouveraient cela suspect…), je vous donne un indice que cela se passe dans le script de démarrage.

Ci-dessous se trouve la ligne qui trouve le StormTrooper et obtient sa composante de force.

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

GameObject.Find() trouve d’abord l’objet passé en paramètre, puis recherche le composant requis à l’intérieur de celui-ci. Il est recommandé d’effectuer ces actions au début ou à un moment du jeu où les retards n’affecteront pas le gameplay. GameObject.Find et GetComponent sont des fonctions coûteuses car elles doivent parcourir toute la hiérarchie. Si votre scène ne comprend que 10 objets, ce sera bien, mais si elle en contient des milliers (une armée de clones), vous verrez sûrement un mauvais effet. Envisagez de les appeler dans la fonction de démarrage. Notez que si votre scène contient de nombreux StormTroopers, Uity ne peut pas savoir lequel vous vouliez utiliser et retournera le premier qu’il trouve. Très probablement, vous détenez une référence à l’objet avec lequel vous souhaitez interagir, par exemple un rappel de collision.

Interaction entre les objets de la hiérarchie

Considérez un véhicule,, lors de sa conduite, le personnage y est attaché en tant qu’objet enfant. Le véhicule aurait une particularité si le conducteur a la force (meilleure maniabilité, plus rapide, etc.). Comme le conducteur est attaché, il est en dessous dans la hiérarchie.
Le véhicule peut trouver la composante de force du conducteur avec le même principe que nous avons utilisé précédemment (GameObject.Trouver) mais nous pouvons le faire plus rapidement en indiquant au véhicule que le conducteur est dans sa propre hiérarchie.

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

GameObject est échangé contre transform. En effet, un objet enfant appartient à la transformation du parent donc nous devons regarder dans la transformation pour trouver notre objet. Dans l’exemple, l’objet est juste sous l’objet appelant, si votre Sith est à l’intérieur de l’objet cockpit sous l’objet car, vous devez fournir le chemin:

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

L’objet appelant est la Voiture et est référencé comme ceci dans l’appel. Ensuite, le cockpit est fouillé et Sith est fouillé. Si le chemin est incorrect, Find renvoie null et vous obtenez une exception de référence Nulle. Vous pouvez éviter cela comme ceci :

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.Find renvoie une transformation pendant GameObject.Find renvoie un objet de jeu.

Encore mieux, nous pouvons utiliser:

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

Maintenant, notre véhicule peut savoir que notre Sith a la force et appliquer une caractéristique spéciale.
Un problème à connaître, si l’objet parent contient un script du type que vous recherchez, il le renverra. Donc, en pensant que vous avez affaire au composant enfant, vous avez réellement affaire au composant parent.

Dans cet exemple, nous devons vérifier pour chaque type de pilote s’il s’agit d’un StormTrooper ou d’un Sith. Une meilleure approche aurait été d’utiliser l’héritage afin que la classe de base supérieure contienne la variable force et qu’une seule fonction soit utilisée pour tous les types de pilotes. Mais l’héritage et l’efficacité n’étaient pas notre principal problème ici.

SendMessage et BroadcastMessage

SendMessage et BroadcastMeassage sont deux autres possibilités qui peuvent être plus simples mais aussi plus coûteuses. Le principe reste le même, on a une référence à un objet et on veut modifier un script qu’il contient.

Avec SendMessage, vous pouvez appeler une fonction sur un objet sans avoir à trouver une référence au script. Maintenant, vous me dites: « Qu’est-ce que tout ce tour de confusion si vous pouvez le rendre simple? »

SendMessage utilise la réflexion et est extrêmement lent par rapport à GetComponent et ne doit être pris en compte que dans les cas où cela n’affectera pas le jeu ou si vous n’avez pas d’autre solution.

Et cela devrait sembler logique, avec GetComponent, nous indiquons exactement où se trouve le script en mémoire. Avec SendMessage, le compilateur connaît l’objet, mais il parcourra tous les scripts et fonctions jusqu’à ce qu’il trouve l’heureux gagnant. Cela peut affecter votre jeu si vous l’utilisez souvent sur plusieurs objets et si l’objet a beaucoup de composants et de scripts (qui sont d’ailleurs des composants).

SendMessage ne recherche que l’objet vers lequel l’appel est effectué, BroadcastMessage lance une recherche sur l’objet cible mais aussi sur tous ses enfants. La recherche sera encore plus longue si l’objet contient une grande hiérarchie. Un nouveau venu est apparu avec Unity4 SendMessageUpwards que vous auriez dû deviner fait la même chose que BroadcastMessage mais vers le haut donc tous les parents de l’objet jusqu’à la racine.

Ok considérons que notre StormTrooper a une fonction pour recevoir l’ordre

//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 } }}

Considérons maintenant que notre StormTrooper rencontre un commandant de l’Empire:

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

Comme vous pouvez le voir, le principe reste le même, j’ai besoin d’une référence à l’objet cible et ensuite je lui envoie un message. Tout comme GetComponent, il est préférable que vous ayez mis en cache la référence de l’objet.

Il est également possible d’ajouter un paramètre pour s’assurer que le message trouve un récepteur ou non.

Notez qu’il n’est pas possible d’utiliser une fonction renvoyant une valeur. Vous devrez ensuite revenir en arrière et utiliser GetComponent.

Fonctions interactives

Unity utilise de nombreuses fonctionnalités qui font interagir deux objets ou plus. L’avantage de ces fonctions est qu’elles stockent beaucoup d’informations sur l’autre objet.

  1. Fonctions de collision OnTriggerXXXX/OnCollisionXXXX
  2. Fonctions physiques XXXXcast

Voici la déclaration de collision:

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

L’appel de la fonction crée une variable de type Collision ou Collisionneur. La fonction remplira cette instance d’informations sur la collision et contiendra une référence à l’autre objet avec lequel nous interagissons.

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

Notez que je n’ai pas besoin de rechercher l’autre objet car colReference est de type Collision et contient une référence à cet objet en tant que membre. Maintenant, je peux effectuer mon GetComponent comme d’habitude et accéder à ma variable de santé publique.

Les fonctions de la classe de physique ont un principe similaire. Les données ne sont pas stockées dans une instance de Collision/Collisionneur mais dans une instance de 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"); } } }}

Je déclare une variable de type RaycastHit, je la passe à la fonction. RaycastHit est une structure puis un type de valeur, j’ai besoin du mot clé out pour créer une référence à cette structure afin que la fonction puisse modifier la structure à l’intérieur. Si vous vous posez des questions sur ce processus, consultez l’article Gestion de la mémoire et la section sur le type de valeur / référence.
Ce que je fais ici, c’est vérifier à quel point la surface sur laquelle je place le curseur est plate. Ceci est utile pour les jeux stratégiques où vous souhaitez placer un bâtiment mais pas sur une falaise.
J’utilise le produit scalaire du terrain normal avec le Vecteur3.Et si le résultat est assez proche de 1, ce qui signifie qu’ils sont alignés, le sol est suffisamment plat pour la construction.
Évidemment, d’autres calculs sont nécessaires car ce n’est que pour un point et vous devrez vérifier la normale, mais vous avez l’idée. J’ai créé une interaction entre la position de ma souris et le terrain à l’aide d’un raycast.

Un raycast contient un point de départ et une direction. Dans notre cas, le point de départ est la conversion de la position de la souris en position mondiale, puis une direction basée sur la rotation de la caméra et la matrice de projection. Cette ligne va à l’infini dans la direction définie (vous pouvez également définir une distance maximale) et si elle frappe un collisionneur, les informations de ce collisionneur et de la collision sont stockées dans la structure RaycastHit. La structure contient alors une référence à l’objet hit.