NavMesh

Introduction aux NavMeshs

Il est souvent utile d’avoir des personnages ou des objets se déplaçant “tout seuls” dans une scène. Il faut pour cela utiliser des NavMesh et des NavMesh Agents. Un NavMesh est une surface que l’on définit comme navigable (sur l’image suivante, la zone bleue est navigable, les zones grises ne le sont pas), et les NavMesh Agents sont des intelligences artificielles capables de se déplacer sur ces surfaces (et uniquement sur ces surfaces).

Ces outils permettent de construire des scènes complexes, avec notamment des obstacles, des endroits où la navigation sera ralentie (de la boue par exemple), etc…

image

Une scène de navigation complexe, composée d’un mesh de navigation, d’un agent, d’un obstacle et d’un lien off-mesh (permet de sauter d’un point jaune à l’autre) .

Créez un nouveau projet,  Nous allons créer une route sur notre plan. Pour cela, ajoutez un nouveau plan (que vous nommerez “Road”) sur votre scène, ajustez les variables scale de la manière suivante : (5 ; 1 ; 5) pour le plane de base et (1 ; 1 ; 5) pour “Road”. On obtient quelque chose comme ceci:

Scène contenant deux plans

Le plan “Road” sélectionné sur la scène, vous pouvez lui ajouter cette texture :

Texture de la route

Le plan “Road” avec sa jolie texture.

Attention à ce que le plan “Road” soit très légèrement surélevé (de 0.1 par exemple) par rapport à votre plan principal, sans quoi vous obtiendrez des bugs graphiques.

Nous voulons créer un NavMesh sur cette route. Pour cela, ouvrez la fenêtre Navigation (Window > AI > Navigation). La fenêtre navigation s’ouvre dans un onglet à côté de l’inspector. Dans la fenêtre hierarchy, sélectionnez votre objet “Road”, puis dans la fenêtre navigation cochez la case “Navigation static”. Sélectionnez ensuite “Bake” dans le coin inférieur droit de la fenêtre navigation. Vous obtenez le résultat suivant :

Un NavMesh est construit sur la route.

Cette zone bleue signifie qu’un NavMeshAgent pourra se déplacer sur cette zone. Dupliquez maintenant votre route pour en avoir deux et faire une 2×2 voies.

Cependant, si vous retournez dans l’onglet Navigation, catastrophe !!! Le NavMesh n’est plus sur la route… Et il n’a pas été dupliqué en copiant la route… En réalité, le NavMesh est indépendant des autres objets. Il vous faudra redéfinir votre NavMesh si vous modifiez l’environnement. Ainsi, enlevez le NavMesh existant en cliquant sur le bouton “Clear” dans la fenêtre Navigation, et redéfinissez un NavMesh qui couvre les deux routes. Vous devriez obtenir ceci :

Notre infrastructure routière

Attaquons-nous désormais aux NavMeshAgents.

Introduction aux NavMeshAgents

Sur une route, on trouve souvent des voitures. Importez donc le package CarToon
Comme c’est un package qui vient de votre disque dur, il n’est pas géré par le Package Manager (qui ne gère que les packages en ligne sur les serveurs d’Unity). Vous pouvez le glisser de votre explorateur de fichier jusqu’à Unity, ou passer par le menu Assets -> Import Package -> Custom Package.

Depuis la fenêtre Project, glissez-déposez le prefab “NavMeshCar” sur votre scène.

Ok, ce n’est pas la voiture de Batman, mais bon, ça fera l’affaire

Dans la fenêtre inspector, désactivez le script “Car User Control”, puisque le but est justement que la voiture se déplace d’elle-même, sans intervention de l’utilisateur. Créez un script “NavMeshScript”, et placez-le sur la voiture. Aussi, ajoutez un NavMeshAgent à votre voiture ( Component > Navigation ).

Dans ce code, vous devrez définir la destination du NavMeshAgent via la méthode SetDestination(), vers laquelle chaque NavMeshAgent va se diriger. Pour un premier test, vous pouvez la coder “en dur” (dans mon cas, (-2.5f, 0.0f, -4.5f) correspond à l’extrémité de ma route). Plutôt que d’utiliser cette méthode (assez moche vous en conviendrez), utilisez plutôt un GameObject vide et placez le à l’extrémité de la route où se trouve votre voiture. Ce sont alors les coordonnées de cette objet que vous récupérez dans le code pour définir la cible de votre NavMeshAgent. Pensez à détruire les voitures une fois qu’elles sont arrivées à destination, dans le cas contraire, l’accumulation d’objets peut provoquer des ralentissements sur votre ordinateur.

Votre voiture se déplace désormais toute seule sur la route jusqu’à atteindre sa cible. Nous voudrions toutefois instancier des voitures d’abord à une fréquence régulière pour avoir un semblant de trafic sur notre route, puis un une fréquence aléatoire. Depuis votre voiture, créez un prefab “NavMeshCar”. Puisque vous avez déjà instancié dynamiquement des objets sur une scène, utilisez le même principe que dans le CubeGenerator du TD2 pour instancier des NavMeshCar sur un bord de la route, afin qu’elles se déplacent vers l’autre bord.

Pour effectuer des instanciations de manière régulière, consultez la documentation de la fonction invokeRepeating, qui permet d’exécuter régulièrement la fonction donnée par la chaîne de caractère en premier argument.

Pour les effectuer de manière irrégulière, la méthode la plus adaptée est l’utilisation de coroutines. Une coroutine est un processus détaché du fonctionnement du programme. Quand on appelle la fonction StartCoroutine(), la fonction donnée en premier argument est exécutée parallèlement à l’exécution du programme. C’est utile quand on veut paralléliser des processus, ou encore effectuer des requêtes internet et attendre les réponses sans interrompre l’application par exemple. Dans notre cas, on va l’utiliser en conjonction avec WaitForSeconds(n) qui, comme son nom l’indique, suspend l’exécution du processus pendant n secondes. Pour effectuer une génération avec des intervalles de temps aléatoires, lancez une Coroutine dans votre Start. Cette coroutine instanciera un véhicule, tirera un nombre aléatoire s, et s’appellera elle-même après s secondes en utilisant WaitForSeconds().

La syntaxe d’un appel de WaitForSeconds est un peu particulière, arrêtons nous y un instant :

yield return new WaitForSeconds(waitTime);

En réalité, ce que fait cette instruction, c’est :

1 – yield : indique que tout ce qui est après est une unique fonction et on doit attendre la fin de son exécution avant de continuer, et ce, même si la fonction lance des processus parallèles.

2 – return : La fonction après est composée de une seule ligne, qui demande à retourner un objet

3- WaitForSeconds(waitTime) L’objet à retourner est un objet qu’il faut instancier : un objet de type WaitForSeconds qui est un processus unity qui ne fait absolument rien, et ce pendant waitTime secondes.

4 – Une fois que WaitForSeconds a accompli son travail (à savoir, rien), son descripteur est retourné et on n’en fait rien car en réalité, ce n’est pas le retour en tant que tel qui nous intéresse mais bien le fait d’attendre le retour avec yield.

TLDR : Le mot clef yield nous assure que tout ce qui est annoncé dans la ligne a été exécuté au moment du passage à la ligne suivante.

note

Pour plus de propreté, on peut également se renseigner sur la fonction StopCoroutine() qui permet d’interrompre un processus quand on veut dans l’exécution du programme mais on ne le fera pas pendant ce TD.

Le périphérique parisien et sa végétation luxuriante

Triggers et klaxon

En ajoutant un collider sur le prefab de votre NavMeshCar, par exemple un rectangle devant la voiture, en sélectionnant l’option isTrigger et en surchargeant la méthode OnTriggerEnter(), vous pouvez faire en sorte que les voitures klaxonnent lorsqu’elles se rapprochent dangereusement d’un objet. Pour jouer un son, il est possible d’utiliser la fonction AudioSource.Play(). Vous pouvez créer une source audio depuis un objet AudioClip, comme celui-ci pour le klaxon.

Note : les conducteur⋅ice⋅s ne klaxonnent usuellement pas quand ils/elles voient le sol ; utilisez un tag pour les en empêcher.

C’est l’heure du jeu. Nous avons une véritable autoroute. Cependant, c’est un peu vide comme application. Nous allons donc créer un petit jeu. Ajoutez un FirstPersonCharacter comme dans le TD précédent, et positionnez-le sur le bord de la route (pensez à configurer correctement votre (New)Input System pour interagir avec votre personnage!). Le but du jeu sera de traverser les voies sans se faire renverser par une voiture. A chaque fois que le personnage traverse la route sans encombre, il gagne un point. Cependant, s’il se fait toucher par une voiture, son score est décrémenté de un point et il retourne à sa position initiale.

Pour gérer le score, nous allons créer un Game Manager qui stockera le score et les méthodes servant à l’incrémenter ou le décrémenter. Ce script peut être par exemple ajouter à un GameObject vide quelconque que l’on nommera “GM”.

Ensuite, positionnez un trigger de chaque côté de la route, de manière à ce que le personnage soit détecté une fois qu’il arrive de l’autre côté. On pourra par exemple créer un cube faisant toute la largeur du plan (voir image) et désactiver son Mesh Renderer afin de n’avoir que son Collider (dont le isTrigger est positionné à True).

Associez-leur un script qui incrémente le score une fois que le joueur passe d’un côté de la route à l’autre. Cependant, pour ne pas incrémenter le score plus d’une fois à chaque fois, on désactivera le trigger dans lequel le joueur vient de pénétrer, et on activera celui de l’autre côté de la route, ainsi de suite. Le premier trigger, c’est-à-dire celui dans lequel se trouve le personnage au départ, est de base désactivé. Faites néanmoins attention. On ne désactive pas le GameObject contenant le trigger, mais bien le composant en lui-même à l’aide de Collider.enabled.

GUI : Graphical User Interface

Votre jeu marche bien, le score fonctionne correctement mais nous ne l’affichons pas encore au joueur. C’est donc le moment de voir comment l’interface graphique est gérée sous Unity. Il existe différents éléments existants sous Unity pour composer votre GUI, vous pouvez retrouver différents tutoriaux à cette adresse : Doc Unity. Aujourd’hui nous allons seulement nous intéresser à trois types d’éléments de l’interface à savoir les boutons, le texte et les images.

Tout d’abord, il faut savoir que n’importe quel composant de l’interface graphique, donc un bouton ou autre, sera toujours intégré dans un Canvas. Ce Canvas est une zone délimitant notre interface. Il sera généré automatiquement lorsque vous créer un bouton par exemple. Le component Rect Transform d’un canvas n’est pas modifiable, il s’adapte à le fenêtre Game (ou à l’écran si lorsque le jeu compilé est en plein écran).

Dans notre application, nous désirons afficher le score du joueur. Ajoutez donc un texte à votre scène ( GameObject > UI > Text – TextMeshPro ). La première fois, Unity vous proposera d’installer TMP Essentials pour que TextMeshPro fonctionne correctement, ainsi que des exemples. Cliquez sur Import TMP Essentials. Comme tous les éléments d’interface d’Unity, le Gameobject Text possède un Rect Transform qui vous permet de le positionner dans le Canvas et de définir manuellement sa taille. On y retrouve aussi les notions d’ancre ou encore de pivot qui vous permettent de placer votre éléments par rapport à l’objet parent, si et seulement si celui-ci possède également un Rect Transform, ce qui est le cas dans 99.999999% des cas. On veut afficher le score en haut à gauche et ce, quelque soit la taille de l’écran. Nous allons donc figer le texte en haut à gauche de notre Canvas à l’aide de l’ancre correspondante.

image

L’objet Text créé comprend un composant Text (UI), qui lui-même contient un champ texte. Cependant, ce champ est utile si notre texte était statique. Or, nous voulons actualiser le score en temps réel, il faut donc que le texte soit dynamique. Pour cela nous allons créer un script. Ajoutez donc un script au Gameobject Text. Il devra récupérer le score stocké dans notre GameManager (via une variable public par exemple) et l’afficher en modifiant le texte de l’objet Text (vous suivez toujours ?). Cependant, pour pouvoir accéder à la propriété Text.text, nous devons inclure la librairie correspondant à l’interface utilisateur. Ajoutez donc tout en haut de votre script la ligne :

using TMPro;

Une fois ceci terminé, votre score doit se mettre correctement à jour et s’afficher dans la foulée.

Chargement et lancement du jeu

Créez une nouvelle scène et ajoutez lui une image (GameObject > UI > Image). Modifiez sa taille de façon à ce qu’elle remplisse le Canvas. Vous pouvez faire cela directement dans le Rect Transform, au niveau des ancres prédéfinies en maintenant la touche Alt enfoncée. Vous pouvez changer la couleur de fond avec celle que vous préférez. Si vous le souhaitez, vous pouvez ajouter une image à votre projet, et comme pour les cursors du TD précédent, vous devez changer la nature de l’image dans Unity. Sélectionner votre image dans la fenêtre projet et changer le paramètre Texture Type à la valeur Sprite (2d and UI).

image

Ajoutez ensuite un texte et deux boutons à votre canvas comme ceci :

image

Les couleurs des boutons et du texte sont à mettre suivant goûts personnels ! Nous devons cependant associer des actions aux boutons. Notre bouton Play devra charger la scène principale contenant notre jeu, alors que Quit fermera l’application.

Créez tout d’abord un script que nous appellerons AppManager, et contenant deux méthodes publiques :

Quit() qui servira à fermer l’application à l’aide de EditorApplication.isPlaying si vous êtes dans l’éditeur ou Application.Quit() dans un build ; Play() qui chargera donc le jeu à l’aide de SceneManager.LoadScene() dans l’éditeur ou Application.LoadLevel() pour un build. Vous pouvez ajouter ce script au canvas par exemple, ce n’est pas très important ici. Ensuite, nous devons ajouter nos deux scènes créées dans les paramètres du projets afin qu’elles soient prises en compte pendant le build du projet. Pour cela, allez dans File > Build Settings. En ayant au préalable sauvegardé votre scène, cliquez sur le bouton “Add Current”. Chargez votre scène principale et refaites la même opération. Vos scènes sont alors toutes les deux ajoutées au build du projet.

Ensuite, il ne vous reste plus qu’à relier le script aux boutons. Sélectionnez un des boutons, dans l’inspector vous verez un petit tableau On Click(), cliquez sur le + puis glissez le canvas depuis la hiérarchie jusqu’à la case none (object) (c’est comme nos variable public), ensuite vous aurez accès à tous les éléments, components, scripts, class etc. pour déclenchez une action On click(). Vous l’aurez compris, nous allons choisir la fonction AppManager.Quit() sur le bouton du même nom, et la fonction AppManager.Play() sur le bouton du même nom. Vous devez obtenir ceci :

image

Pour info :

Dans le script que nous venons de créer, Application.Quit() ne fonctionnera que si vous effectuez un build du projet, c’est à dire si vous générez un exécutable indépendant de Unity. Ainsi, si vous lancez l’application comme d’habitude dans l’éditeur, cette méthode de fonctionnera pas (comme précisé dans la doc Unity de la méthode Quit() ) ;

Pour la méthode LoadLevel(), vous pouvez mettre en paramètre le nom de votre scène principale ou son identifiant. Pour connaître ce dernier, il suffit d’aller dans les paramètres du build comme expliqué précédemment et de relever le numéro associer à la scène correspondante. Vous remarquerez que vous pouvez ajouter autant d’appels que vous le souhaitez dans On Click(), et qu’il y a un sélecteur pour définir si l’action est exécutée lorsque l’on est dans l’éditeur, la Runtime (la build) ou les deux. Vous pouvez donc avoir une méthode pour la version éditeur, et une méthode pour la version build.

Pour aller plus loin Notre NavMesh est très rudimentaire (pour un cas aussi simple, il est même possible de faire sans, mais c’était l’occasion de vous familiariser avec cet outil qui peut s’avérer très utile et puissant). Vous pouvez l’améliorer de nombreuses façons, par exemple en faisant une route plus complexe (vous pouvez utiliser l’asset EasyRoads3D Free), ou en considérant le personnage comme un obstacle mobile, les voitures essayant alors de l’éviter plutôt que d’aller tout droit.