Unity est un game engine qui, bien que originellement créé pour développer rapidement des jeux vidéo 2D ou 3D, a été étendu dans différents domaines tels que l’automobile, l’architecture, l’ingénierie, les expériences marketing, et même le cinéma.

Le logiciel est téléchargeable à l’adresse suivante : https://unity3d.com/get-unity/download

Il dispose d’une version gratuite pour les particuliers et entreprises au CA de moins de 100.000€  et de plusieurs modèles payants pour les entreprises.

Le logiciel dispose de son moteur qui va simplifier les calculs physiques, les rendus graphiques, l’audio, l’interface utilisateur,etc.

On découpe le programme en différentes scènes. Au lancement du programme généré, la première scène démarre, puis celles appelées par l’utilisateur. On va disposer différents objets 2D/3D dans la scène, les GameObject. Les GameObject sont ensuite configurables via des Component, des classes qui vont ajouter à l’objet son rendu graphique, ses fonctionnalités etc.

Dans cette scène il y a 4 GameObject. Par exemple, le Cube possède plusieurs Component :

  • un Transform (obligatoire) qui indique sa position rotation échelle et qui permet de mettre un système de parenting (le Cube est l’enfant de Plane)
  • Un MeshFilter et un MeshRenderer qui gèrent la géométrie et l’affichage du cube
  • Un BoxCollider qui gère la boite de collisions pour le moteur physique
  • Un script créé par l’utilisateur nommé “MyScript”.

Unity dispose d’un système de scripting, le plus souvent en C# ou en Javascript, qui va permettre à l’utilisateur de créer ses propres Component. C’est ce que nous allons creuser dans cet article.

 

Scripting

Lorsque l’on créé un script, il va être pré-rempli par défaut avec un squelette de la classe créée, avec plusieurs éléments :

La classe hérite de MonoBehaviour, la classe de base pour tous les scripts créés par l’utilisateur. C’est ce qui va nous permettre d’obtenir toutes les méthodes événementielles que nous allons voir en détail dans cet article.

Lorsqu’on est initié au système de scripting de Unity, on nous présente généralement deux fonctions :

  • La fonction Start
    • Est appelé à l’initialisation de la scène courante.
    • S’exécute une fois.
  • La fonction Update
    • Est appelée à chaque frame, par défaut, et si la puissance de la machine le permet, 60 fois par seconde. Ce paramètre est modifiable dans les options.

Néanmoins le moteur de Unity dispose de bien plus d’événements. On retrouve d’abord l’initialisation liée aux GameObject actifs, celle des objets en cours d’activation, puis les différentes boucles appelées régulièrement que l’on peut découper en 3 parties :

  • Les calculs du moteur physique
  • La logique de jeu, incluant la fonction Update susmentionnée
  • Le rendu graphique

Et enfin les fonctions de désactivation sont appelées. On parlera ici des fonctions callback que l’utilisateur peut remplir, et non des fonctions internes à Unity.

L’ordre de ces événements est toujours respecté au sein d’un script. Pour connaître l’ordre d’exécution entre les différents scripts créés, il faut aller dans Editer -> Propriétés du projet, et aller dans la catégorie Ordre d’exécution des scripts. Si l’ordre n’est pas défini il sera sélectionné par Unity de manière arbitraire.

Je vous invite à voir le schéma complet des évenements à cette adresse bien qu’on affichera étape par étape au fur et à mesure que nous les découvrons :

https://docs.unity3d.com/uploads/Main/monobehaviour_flowchart.svg

 

Initialisation

Les fonctions d’initialisation sont appelées lorsqu’une scène démarre.

Il y a d’autres méthodes utilisées que la méthode Start lors du démarrage d’une scène :

  • Awake : La toute première fonction à être appelée. Si le GameObject n’est pas actif, cette fonction sera appelée au tout début de son activation.
  • OnEnable : Au moment où le GameObject est activé, c’est à dire dans trois cas possibles : Lorsque la scène démarre, si le GameObject est instancié par un autre composant, ou enfin si on active le GameObject précédemment désactivé. Cette fonction est également appelée avant Start.
  • OnLevelWasLoaded : Fonction appelée quand un niveau ou sous-niveau a été chargé.
  • Reset : Fonction réservée aux scripts exécutables au sein de l’éditeur, avec l’attribut [ExecuteInEditMode]. Cette fonction est appelée la première fois qu’on attache le script à un GameObject et à chaque appel manuel via clic droit sur un Component -> Reset
  • Start : Fonction appelée juste avant la première Frame, si le GameObject est activé.

Comment savoir quelle fonction utiliser ? Cela va dépendre de ce que l’on souhaite faire :

OnLevelWasLoaded est souvent utilisée par les scripts qui ne sont pas détruits entre deux scènes comme les Singleton gérant certaines données de sauvegarde ou des statistiques utilisateurs, voir même pour certains scripts utilisant le réseau.

Awake va être utilisée pour certaines initialisations un peu lourdes que l’on souhaite absolument terminer avant les premiers appels Start. Par exemple, charger une dépendance vers un autre script va être primordial dans un Awake afin de ne pas avoir une nullexception dans le start de celui ci dans le cas d’une interdépendance.

OnEnable pouvant être appelé durant n’importe quel moment de la scène il est important de le garder en tête pour réinitialiser certaines variables durant l’exécution de la scène.

Initialisation

 

Boucle de jeu

En fonction des variables, des interactions que l’on veut manipuler et surveiller, il est important de choisir la bonne fonction à utiliser. Bien que techniquement il soit possible de tout faire au sein de la fonction Update, il est recommandé dans un souci d’optimisation d’utiliser à bon escient les diverses possibilités offertes par Unity :

Moteur physique et animations

  • FixedUpdate : La toute première méthode appelée dans la boucle, mais aussi la méthode la plus appelée. Étant exécutée juste avant les calculs du moteur physique il est recommandé de faire les calculs physiques dans cette fonction. Si le programme est gourmand en ressources et que le framerate diminue, cette fonction sera toujours appelée aussi régulièrement, car elle fonctionne sur un timer différent et indépendant.

 

Viennent ensuite les callback du système d’animations. Ces événements concernent les scripts dérivant de StateMachineBehaviour et non MonoBehaviour qui concerne le reste des événements : OnStateEnter / Exit / Update / Move / IK. Ces événements concernent les machines à état, le plus souvent lorsqu’un Animator est attaché à l’objet.

  • OnTrigger / OnCollision: Les différentes méthodes OnTriggerBegin, OnTriggerStay, OnTriggerExit, et réciproquement OnCollisionBegin, OnCollisionStay, OnCollisionExit,sont appelées après FixedUpdate et les différentes méthodes d’animation. Elles sont appelées en cas de collision entre la hitbox du GameObject avec un autre. Si aucun trigger/collider n’est défini les méthodes ne sont pas appelée

On voit qu’avec WaitForFixedUpdate il peut y avoir plusieurs exécutions des calculs physiques

 

Logique de jeu et interactions

Juste avant l’appel de Update, on récupère les informations des différents input (souris, clavier, manette etc).

  • Update : La plupart de la logique de jeu peut être écrite dans cette méthode, appelée une fois par frame. Pour la plupart des calculs dépendant du framerate il ne faudra pas oublier d’utiliser Time.deltatime, indiquant le temps écoulé en secondes depuis la dernière frame.
  • LateUpdate : Cette méthode est appelée juste après Update. L’intérêt d’une telle méthode est qu’elle attend que l’exécution d’Update soit bien terminée, ce qui est utile pour utiliser certains résultats calculés dans Update.

 

Rendu

Les méthodes de rendu sont appelées en fin de boucle, après les calculs du moteur physique et la logique de jeu. Certaines méthodes sont réservées aux scripts attachés à une caméra :

  • OnPostRender
  • OnPreCull
  • OnPreRender
  • OnRenderImage
  • OnRenderObject

Voici les différentes méthodes par ordre d’appel :

Rendu

 

  • OnPreCull : Appelée avant le culling de la caméra, c’est à dire ce qui définit les objets visibles par celle-çi. C’est ici qu’on modifiera certains paramètres de la caméra, comme le Field Of View, la focale etc. La méthode est appelée uniquement si le script est attaché à la caméra.
  • OnBecameVisible/OnBecameInvisible : Appelée au moment où l’objet devient visible / invisible par rapport à la caméra.
  • OnWillRenderObject : Appelée si l’objet est visible, pour chaque caméra de la scène. La fonction peut être appelée plusieurs fois par frame. Pour accéder à la caméra concernée, utiliser current
  • OnPreRender : Appelée juste avant que la caméra affiche la scène. La méthode est appelée uniquement si le script est attaché à la caméra. Si les paramètres de la caméra sont changés ici, ils ne seront pris en compte qu’à la prochaine frame, d’où l’importance de le faire dans OnPreCull.
  • OnRenderObject: Appelée une fois que l’objet a été affiché et une fois que toute la scène a été rendue.
  • OnPostRender: Appelée une fois que la caméra a terminé de rendre la scène. La méthode est appelée uniquement si le script est attaché à la caméra. Utilisée notamment pour appliquer des effets sur l’image rendue par la caméra (manipuler les couleurs etc). La méthode est moins lourde que OnRenderImage, qui nécessite une réécriture complète de la RenderTexture. Néanmoins on utilisera plutôt cette méthode pour reinitialiser les paramètres modifiés dans OnPreRender.
  • OnRenderImage: La dernière méthode appelée, une fois que le rendu est terminé. On l’utilisera pour appliquer tous les effets de post-processing.
  • OnGUI: Appelée plusieurs fois par frame à chaque event du GUI. D’abord pour le Draw de chaque élément d’UI, puis pour les Input.
  • OnDrawGizmos : Méthode utilisée pour l’éditeur, afin de permettre au développeur de personnaliser différentes visualisations pour les objets de la scène. Permet de dessiner des formes géométriques primitives afin de sélectionner plus facilement les objets voulus.

            Exemple : une sphère rouge dessinée uniquement dans l’éditeur afin de sélectionner un objet invisible

 

Fin de boucle

Avant de retourner à la méthode FixedUpdate, on attend la fin de frame actuelle via Coroutine puis on regarde si le jeu est en pause via OnApplicationPause, c’est à dire si l’application passe en arrière plan. Si le paramètre Run In Background est coché dans les Player Settings, la méthode ne sera pas appelée.

Trois méthodes peuvent être appelées selon la manière de quitter le script :

  • OnDestroy : Si on appelle la méthode Destroy sur l’objet ou lors d’un changement de scène, cette méthode sera utilisée à la fin de la dernière frame de vie de l’objet.
  • OnApplicationQuit : Méthode appelée sur tous les objets juste avant que l’application ne quitte.
  • OnDisable : Si l’objet est désactivé, cette méthode sera appelée juste avant. Si réactivation il y a, on retournera sur OnEnable, donc la méthode Awake ne sera pas appelée de nouveau.

 

Les coroutines

Les coroutines sont des fonctions pouvant interrompre leur exécution et reprendre au moment souhaité. L’appel initial est produit après la fin de la fonction Update. Ces fonctions sont utiles notamment sur des fonctions un peu lourdes que l’on souhaite appeler régulièrement mais pas à chaque Update.

Pour cela on va utiliser le mot clé yield pour indiquer que la méthode utilisée est un itérateur, puisque nous allons “itérer” dans le temps. Ainsi, à la lecture du mot yield, la méthode va suspendre son exécution et le résultat retourné va être interprété par Unity pour connaître le prochain appel de la méthode.

Différents moyens de suspendre l’exécution, en voici quelques-uns :

  • yield (yield return null ou yield return new WaitForNextFrame()) : La coroutine interrompt son exécution et reprendre à la prochaine frame après Update.

Exemple : Augmenter la taille d’un objet au fil du temps. On surveille un timer et on revient régulièrement au sein du while avec yield

 

  • yield WaitForSeconds : La coroutine attend un certain nombre de secondes, puis reprend à la prochaine frame.

Exemple : Une coroutine qui attend un fondu au noir durant 1 seconde pour téléporter la caméra et refaire un fondu.

 

  • yield WaitForFixedUpdate : Continue après que tous les scripts aient terminé leur FixedUpdate.
  • yield WWW : Continue après qu’un téléchargement www soit terminé.
  • yield StartCoroutine : empile la coroutine souhaité et attend qu’elle soit terminée avant de reprendre son exécution.

 

Conclusion

Si vos scripts Unity sont gourmands pour le processeur, il peut être pertinent de voir ce qui fait “bouchon” au sein de chacun. Cela peut-être un mauvais enchaînement de coroutines, une surcharge de Update… Il faut toujours garder à l’esprit de ne pas utiliser les fonctions coûteuses à chaque frame, telles que FindObjectOfType, ou les méthodes de Linq sur les manipulations de collections, sous peine d’avoir un programme lent et saccadé.

Bien qu’il soit possible de s’en sortir dans la plupart des applications avec les fonctions Start et Update, connaître la structure de la pile événementielle permet à la fois de mieux optimiser son programme, mais aussi d’être plus à l’aise avec le moteur, que ce soit pour débugger ou implémenter des fonctionnalités plus rapidement et plus facilement.

Sources

https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnPostRender.html

https://docs.unity3d.com/Manual/ExecutionOrder.html

https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnRenderObject.html

https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnPreRender.html

https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnWillRenderObject.html

https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnPreCull.html

https://docs.unity3d.com/ScriptReference/ExecuteInEditMode.html

https://docs.unity3d.com/Manual/class-MonoManager.html

https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnRenderImage.html

https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnDrawGizmos.html

https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnApplicationPause.html

https://docs.unity3d.com/Manual/Coroutines.html

https://unity3d.com/what-is-a-game-engine

https://docs.microsoft.com/fr-fr/dotnet/csharp/language-reference/keywords/yield

 

Partager
Faire suivre