Chapitre 3 : scripting #
Dans Unity, l’intelligence de l’application 3D se programme grâce à des scripts C#.
Lorsque ces scripts sont attachés à un GameObject, alors Unity les exécute en fonction des événements qui interviennent.
Hello World #
Pour attacher un script à un objet dans la scène 3D, il faut sélectionner le GameObject puis cliquer sur le bouton Add Component.
Ensuite, on renseigne le nom du script que l’on souhaite attacher.
La figure suivante résume ce que vous devriez obtenir :
S’il n’est pas présent, alors il est automatiquement créé en cliquant sur New Script puis sur Create and Add.
Le script peut ensuite être ouvert dans votre IDE en double-cliquant dessus.
Il ressemble à ce qui suit :
| |
Le script contient déjà 2 fonctions importantes : void Start() et void Update().
Comme indiqué dans les commentaires :
void Start()est appelée 1 seule fois, durant la frame où il est activé (en général au début de l’exécution);void Update()est appelée à chaque frame.
Il est très facile d’afficher un message à chaque appel à void Start() ainsi qu’à void Update().
Pour cela, il suffit de modifier le script pour rajouter un appel à la fonction Log() de la classe Debug1 comme indiqué dans le code qui suit :
| |
Lorsque l’on exécute l’application, on peut regarder ce qui est affiché dans la fenêtre Console :
Hello Start() est affiché une seul fois au début, et que le message Hello Update() est affiché en continu, comme attendu.
Comme vous l’avez sans doute déjà remarqué, un Script est en fait un Component.
Il doit donc être attaché à un GameObject pour être exécuté.
De plus, il faut noter que les membres publics d’un Script sont automatiquement accessibles depuis l’éditeur de Unity.
C’est très pratique lorsque l’on souhaite pouvoir modifier et tester rapidement des paramètres du script à l’exécution.
Enfin, il faut remarquer que le Script hérite de la classe MonoBehavior.
C’est la classe MonoBehavior qui gère en particulier le comportement général du Script (quand il est instancié, quelles fonctions sont appelées, quand elles sont appelées, etc.).
Constructor et Destructor
Unity gère lui-même la création et la destruction des
Scripts. Il ne faut donc surtout pas implémenter les constructeurs au risque de perturber le fonctionnement complet de votre application.
Event Functions #
Les Scripts ont des points d’entrées (des fonctions) prédéfinis qui vont être automatiquement appelés par Unity en réponse à certains événements : ce sont les Event Functions2.
Remarque
les fonctions
void Start()etvoid Update()sont desEvent Functions.
L’ordre d’exécution des Event Functions est important.
La figure suivante résume les principales Event Functions et surtout leur ordre d’exécution :
Elles sont documentées dans la classe MonoBehavior3.
Il faut noter qu’elles sont très nombreuses et que seul un sous-ensemble d’entre elles est en général utilisé.
Remarque
On voit par exemple que toutes les fonctions
void Update()exécutées avant les fonctionsvoid LateUpdate().
ATTENTION
L’ordre d’exécution des mêmes
Event Functions(toutes lesvoid Update()par exemple) n’est pas défini.
Time.DeltaTime #
Il est important de garder à l’esprit que la fonction void Update() est appelée à chaque frame, c’est-à-dire aussi souvent que possible.
Par conséquent, elle ne tient pas compte du temps écoulé entre 2 frames rendues.
Ainsi, le résultat de la fonction void Update() peut dépendre du frame rate de votre application.
Par exemple, lorsqu’elle est chargée de déplacer un GameObject d’une certaine distance à chaque frame, alors :
- si le frame rate est élevé (par exemple sur une machine puissante), elle sera appelée très souvent. Le
GameObjectse déplacera donc très vite. - Au contraire, si le frame rate est faible, elle sera appelée moins souvent. Le
GameObjectse déplacera dans ce cas plus lentement.
En général, avoir une application 3D dont le comportement dépend de la machine sur laquelle elle est exécutée n’est pas souhaitable.
Pour corriger ce problème, il faut utiliser Time.DeltaTime qui nous renseigne sur le temps écoulé depuis le dernier appel à void Update().
Ainsi, il est facile de rendre l’exécution de cette fonction indépendante du frame rate :
| |
Modifier des GameObjects à l’exécution
#
Un script peut modifier les Components :
- appartenant au même
GameObject(celui auquel il est attaché); - appartenant à d’autres
GameObjectsqu’il connaît au préalable; - appartenant à d’autres
GameObjectsqui sont instanciés (et détruits) dynamiquement.
Modifier les Components du même GameObject
#
Un Script a accès aux Components attachés au même GameObject en utilisant leur type :
public Component GetComponent<Type>();public Component[] GetComponents<Type>();RigidBody attaché au même GameObject que le Script, on peut faire : | |
Modifier les Components d’autres GameObjects connus
#
Il est possible pour un Script de modifier des Components appartenant à d’autres GameObjects.
En particulier, si le GameObject est connu au préalable, et si le lien entre le Script et le GameObject (ou le Component) à interroger ou modifier est permanent, alors on peut tout simplement rajouter un membre public dans le Script (le GameObject ou directement le Component que l’on attend) :
| |
Comme les membres sont publics, alors ils sont directement visibles dans l’éditeur.
Il suffit donc de faire un drag-and-drop des GameObjects que l’on souhaite utiliser :
Ensuite, dans le Script, on peut tout simplement utiliser les GameObjects (ou les Components) liés de la manière suivante :
| |
Modifier les Components d’autres GameObjects créés dynamiquement
#
Si les GameObjects sont créés et détruits dynamiquement, alors il est préférable de tous les grouper en les parentant au même GameObject.
Ce dernier va donc servir de conteneur, que l’on passe au script en utilisant un membre public.
Il ne reste plus qu’à itérer sur tous les Components du conteneur ayant le type désiré :
| |
ATTENTION
Les méthodes
GetComponent<Type>()etGetComponents<Type>()sont coûteuses en temps de calcul. Il faut donc éviter de les appeler trop souvent à l’exécution. En particulier, dans les exemples précédents, il est préférable de les appeler une seule fois dans la fonctionvoid Start()et de stocker le résultat dans une variable membre.
Recherche de GameObjects à l’exécution
#
Il est possible de rechercher des GameObjects à l’exécution en utilisant leur nom ou leur tag grâce au fonctions suivantes :
public static GameObject Find(string name);
public static GameObject FindWithTag(string tag);
public static GameObject[] FindGameObjectsWithTag(string tag);GameObjects qui ont comme Tag “Cubes” : | |
Instancier et détruire des GameObjects à l’exécution
#
Pour instancier des GameObjects à l’exécution, il faut utiliser la méthode Instantiate4 :
public static Object Instantiate(Object original);
public static Object Instantiate(Object original, Transform parent);
public static Object Instantiate(Object original, Transform parent, bool instantiateInWorldSpace);
public static Object Instantiate(Object original, Vector3 position, Quaternion rotation);
public static Object Instantiate(Object original, Vector3 position, Quaternion rotation, Transform parent);original est souvent un Prefab passé en paramètre du script.Pour détruire des GameObjects à l’exécution, il faut utiliser la méthode Destroy5 :
public static void Destroy(Object obj, float t = 0.0F);User Inputs #
Pour une application 3D interactive, il est important de pouvoir écouter et récupérer les entrées de l’utilisateur.
Dans Unity, ces entrées sont configurables en allant dans le menu Edit > Projects Settings :
Input Manager, vous obtenez une fenêtre similaire à celle montrée dans la figure suivante :
Dans votre Script, il est ensuite facile de vérifier si l’utilisateur est en train de “se déplacer”, en interrogeant l’état de l’Axis comme dans le code qui suit :
| |
Coroutines #
Les Event Functions dans les Scripts sont synchrones : elles vont bloquer l’application le temps qu’elles s’exécutent.
On peut rendre certaines fonctions non-bloquantes en utilisant des Coroutines6.
C’est utile par exemple :
- pour répartir de longs calculs sur plusieurs frames;
- pour simuler un timer qui va exécuter une action après un certain délai.
Pour séparer les longs calculs sur plusieurs frames par exemple, il faut :
- définir une
Coroutine. Ceci se fait en définissant une méthode qui retourne un objet de typeIEnumerator; - dans la
Coroutine, rajouter un point de synchronisation avec Unity pour lui rendre la main jusqu’à la prochaine frame. Ceci se fait en utilisant le mot-clefyield; - enfin, il faut démarrer la
Coroutine. Ces 3 étapes sont résumées dans le code qui suit :1 2 3 4 5 6 7 8 9 10 11 12 13void Start() { StartCoroutine("LongProcess"); } IEnumerator LongProcess() { for (int i = 0; i < 100; ++i) { Debug.Log(gameObject.name + ": Iteration #" + i); yield return null; } }
On peut aussi créer un timer avec un délai en utilisant la même technique, mais cette fois-ci dans le yield, on retourne un objet de type WaitForSeconds qui va suspendre l’exécution de la méthode pour un certain nombre de secondes.
Par exemple, si on souhaite faire “exploser” un GameObject un certain nombre de secondes après que l’utilisateur a appuyer sur la touche du haut, alors on pourrait faire comme ce qui suit :
| |
Comme vu ci-dessus, il est possible de démarrer une coroutine en passant son nom sous forme de string à la méthode StartCoroutine.
Cette façon de faire à l’avantage d’être simple et suffit dans la grande majorité des cas.
Cependant, elle présente 2 désavantages majeurs :
- Elle est plus coûteuse en temps d’exécution.
- On ne peut passer qu’un seul paramètre à la
Coroutine.
La 2ème approche consiste à passer directement la Coroutine avec ses paramètres au moment d’appeler StartCoroutine.
Enfin, il est possible d’arrêter une Coroutine avec la méthode StopCoroutine7.
L’exemple suivant montre comment démarrer et arrêter une Coroutine à la demande.
Il montre aussi comment passer 2 (ou plus) paramètres à une Coroutine :
| |
Remarque
Il faut noter qu’ici, on stocke le
IEnumeratorretourner par laCoroutineavant de la démarrer. C’est nécessaire pour pouvoir l’arrêter par la suite. Il est aussi possible d’appelerStopCoroutineen passant le nom de laCoroutinesous forme destring.