TP3 : FPC et RayCasting
Rémy Frenoy, Yann Soullard, Florian Jeanne, Luca Pelissero-Witoslawski, Olivia Reaney & Baptiste Wojtkowski, Yohan Bouvet, Azzeddine Benabbou
Lors des deux premières séances de TD, nous avons appris à créer différents types d’objets dans Unity, à interagir avec pour les déplacer de différentes manières (AddForce, Translate). Nous en connaissons un petit peu plus sur la gestion de la physique (rigidbody, colliders, triggers). Nous connaissons plusieurs fonctions de mise à jour de l’affichage des scènes (Update, FixedUpdate, LateUpdate). Nous avons également travaillé sur le positionnement de la caméra (fixe, héritage d’un objet, suivi des translations via un script dédié).
Starter Assets
Unity dispose de packages “starter” correspondant à des éléments de base utilisés dans la majorité des projets. C’est de cette façon que nous avons utilisé le nouveau système de gestion des inputs. Si ces packages sont très utiles, ils doivent être utilisés à bon escient car il est important de connaître et maîtriser le fonctionnement global des éléments que l’on importe dans un projet.
Nous allons aujourd’hui utiliser le package « Starter Assets – First Person Character Controller». Créez un nouveau projet 3D URP. Comme je le disais, le bon escient dans ce cas est l’option URP du projet puisque le starter asset fonctionne en URP (Universal Render Pipeline).
Auparavant, les packages standards étaient directement accessibles depuis Unity, ce n’est plus le cas depuis la version 2019. On va donc devoir passer par l’Asset Store qui, depuis la version 2021, n’est plus accessible non plus directement depuis Unity.
Dans votre navigateur préféré, allez sur le Unity Asset Store. Beaucoup d’assets sont payants, mais certains, notamment ceux permettant d’apprendre à utiliser Unity, sont gratuits. Cherchez l’asset “Starter Assets - FirstPerson | Updates in new CharacterController package”. Vérifiez qu’il est bien compatible avec votre version de Unity. Cliquez ensuite sur “Ajouter à mes Assets”. Un autre asset porte presque le même nom : « starter assets – Third Person Character Controller », ne vous trompez pas. Cet asset sert, comme son nom l’indique, à faire une application en vue à la 3ème personne.
Vous pouvez retrouver votre package dans le menu « Window > Package Manager ». Cliquez sur le « Packages : … » (comme on faisait pour l’Input System) puis sélectionner « Packages : My Assets » : tadam ! Tous vos assets achetés se retrouvent ici ! Sélectionnez le « Starter Assets… » et cliquez sur « Download », puis sur import.
Ce package, comme beaucoup d’autre, a des dépendances. Il vous demandera si vous souhaitez les installer automatiquement ou si vous souhaitez le faire vous même plus tard. Cliquez sur « Install/Upgrade ». Unity va installé 2 dépendances (3 si votre projet n’est pas créé en URP) :
- Premièrement, il utilise le New Input System, donc configurez votre projet comme nous l’avions vu au premier TP (il est possible que Unity le fasse autoamatiquement pour vous, vérifiez).
- Deuxième dépendance : « Cinemachine », c’est un package de gestion de caméras dynamiques. Vous pouvez le trouver lui-aussi dans le « Package Manager » à la section « All packages ».
- (troisièmement : le package Universal RP, non on n’y coupera pas !)
Une fois les dépendances installées, un nouveau message apparait :
« Voulez-vous redémarrer pour prendre en compte le New Input System ? »
Deux options possibles :
- Répondez « oui », unity redémarre, mais l’import est incomplet. Refaite-le, mais en cliquant sur « skip » à l’étape des dépendances. Une fois sur la fenêtre vous montrant l’ensemble des fichiers du package, cliquez sur « All » puis sur « Import ».
- Répondez « non », la fenêtre vous montrant l’ensemble des fichiers du package apparait directement, cliquez sur « All » puis sur « Import ». Ensuite, éditez les propriétés du projet pour activer l’utilisation du New Input System. (menu « Edit »-> »Project Setting », onglet « Player » -> « Other Settings », « Active Input Handling » = Input System Package (New) ou Both). Unity redémarrera pour prendre le changement en compte.
Vous pouvez voir que dans la fenêtre Project, dans vos Assets, se trouve un nouveau dossier du nom du package : c’est ici qu’on va trouver les fichiers que l’on va utiliser. Il existe même un fichier Readme.asset qui vous fait un récapitulatif rapide de l’asset dans la fenêtre Inspector.
Si vous lisez attentivement, vous pouvez voir qu’il existe déjà des scènes préconçues pour tester ce que fait l’Asset. Mais on est là pour apprendre donc autant mettre les mains dans le cambouis ! Vous serez libres de tester plus tard si vous voulez…
First Person Controller (FPC)
Nous voilà donc avec… rien du tout, car notre scène est vide (hormis la lumière et la caméra…).
Nous allons utiliser l’asset FirstPersonController, qui correspond à un personnage à la première personne. Dans le dossier StarterAssets, vous trouverez un fichier PDF de documentation qui vous indiquera comment utiliser l’asset (une bonne habitude à prendre pour les projets…). Dans le dossier StarterAssets/ FirstPersonController/Prefabs vous avez plusieurs prefabs à votre disposition, dont deux « caméras ».Glissez l’objet PlayerCapsule sur la scène pour qu’on puisse l’analyser.
Notre objet PlayerCapsule possède une ‘propriété ’‘CharacterController’‘, 3 scripts et un’‘Player Input’‘. Pour les plus observateurs, on peut remarquer que le vert de l’icône du ’‘CharacterController’‘ ressemble beaucoup à celui des Fixed et Hinge Joint du TP2 et des Rigidbody… Eh oui, cette propriété signifie bien que nous avons affaire à un objet qui sera soumis aux lois de la physique ! D’ailleurs, vous pouvez également le déduire grâce au nom du 2ème script ’‘BasicRigidBodyPush’’.
Notre petite capsule a donc un corps qui pourra entrer en collision avec son environnement. Super ! Créons-lui donc un sol pour ne pas qu’il tombe à l’infini ! Ajoutez un plan dans votre scène, en vous assurant que votre capsule (votre personnage) soit bien au-dessus.
Pourquoi utilise-t-on un CharacterController et pas un Rigidbody un peu customisé, comme on a pu le faire dans le TP2 ?
Souvenez-vous : lorsque votre balle rentrait en contact avec un objet, vous aviez un petit effet rebond, c’est normal, c’est de la physique. Dès qu’il y a collision entre des colliders, les forces entrent en jeu. Mais imaginez un personnage qui marche, et dont le petit orteil rencontre un caillou : le fait d’avoir un Rigidbody ferait que votre personnage recule, quand bien même il aurait simplement pu marcher dessus… On a donc besoin d’un Rigidbody un peu plus permissif lorsqu’on se déplace, et là vient le CharacterController !
Mais alors, si c’est la même chose, qu’est-ce qui change ? Un component ‘’Rigidbody’‘ possède une propriété isKinematic, qui, lorsqu’elle est activée (la checkbox est cochée), empêchent les forces de s’appliquer à l’objet (mais on pourra toujours tester les collisions !) Notre propriété ’‘CharacterController’’ est un Rigidbody caché, dont la propriété isKinematic est active. Mais si les forces ne s’appliquent pas, comment on va rétablir la gravité ? Le script FirstPersonController s’en occupe ! Et il nous permet de modifier toutes les valeurs qui permettront à notre personnage de se déplacer (vitesse de marche, de course, hauteur des sauts, etc.)
Ok, on a donc un sol et un corps. Mais comment va-t-on faire pour se déplacer ? Faisons un peu de retro-engineering pour deviner. Dans le dossier InputSystem, ouvrez l’objet StarterAssets.inputactions.
Laissons de côté le saut et le sprint.
Pour se déplacer, nous pouvons donc utiliser les touches ZQSD ou les flèches directionnelles. Pour regarder, on utilise le curseur, donc la souris, et surtout le delta, c’est-à-dire ses déplacements.
Lancez l’application : vous observez le vide, le sol à vos pieds, peut-être même la capsule en fonction de la position de votre caméra. Déplacez-vous… Tiens ? La capsule bouge, mais on est loin de la vue à la première personne ! Il doit nous manquer quelque chose…
Configuration de la caméra du FPC
Revenez dans les Assets > StarterAssets > FirstPersonController > Prefabs.
- Supprimez la MainCamera de la scène et glissez celle des prefabs. Il y a un petit dessin dans la Hierarchy à côté de la MainCamera, ça indique l’utilisation de l’asset CinemachineBrain (il s’agit d’un gestionnaire de caméras dynamique mais ce n’est pas le sujet du TP).
- Glissez le prefab « PlayerFollowCamera » sur la scène, puis dans l’inspector à l’option « Follow » glissez-y le GameObject « PlayerCameraRoot » (c’est un enfant de « PlayerCapsule »).
La caméra se positionne automatiquement à la bonne position. On vérifie cela en lançant l’application : vous pouvez vous déplacer sur le plan grâce aux flèches directionnelles et bouger la tête grâce à la souris (comme un personnage à la première personne en fait…).
Dans le script FirstPersonController, c’est la méthode CameraRotation qui permet de déplacer la caméra lorsque la souris bouge. Elle fait également en sorte que les déplacements de la caméra ne soient pas trop rapides pour une impression visuelle plus agréable (vomir en jouant, c’est pas cool !). Cela vaut aussi pour le mouvement : dans la méthode Move, des interpolations linéaires sont faites grâce à la fonction_ Lerp, et on Clamp les valeurs de déplacement de la souris pour éviter que notre tête ne tourne sur 360° (verticalement).
Le Ray Casting
Le principe
On souhaite désormais pouvoir saisir des objets dans l’environnement virtuel. On va pour cela utiliser le ray casting. Le principe du ray casting est de créer un rayon, depuis une position d’origine et dans une direction particulière. Ce rayon va nous indiquer s’il est entré en collision avec un objet ou non. Pour davantage d’information sur le ray casting, c’est ici.
Le principe du ray casting est simple : la fonction emet un rayon et renvoie True
si le rayon a détecté un objet, sinon elle renvoie False
. Les informations concernant l’objet détecté (s’il y en a un) se retrouvent dans la variable hitInfo
passée en paramètre de la fonction.
Création du rayon et affichage
Commencez par créer un script RayCasting
.
On souhaite que le rayon parte de la caméra, dans la direction de la position du curseur de la souris. Vous trouverez des informations utiles pour générer ce type particulier de ray casting ici.
= mainCamera.ScreenPointToRay(Mouse.current.position.ReadValue()); ray
La Méthode ScreenPointToRay
d’un objet Camera
permet de créer un rayon allant de la caméra à travers un point de l’écran, donné en paramètre. Mouse.current.position.ReadValue()
permet de renvoyer les coordonnées (x,y) de la position du curseur actuel.
Attention : Cette fonction renvoie un rayon mais ne l’émet pas. L’emission du rayon se fait par la fonction Raycast()
Maintenant que le rayon est créé, ce serait bien de pouvoir l’afficher (l’afficher ne veut pas dire le Caster). Pour cela, vous utiliserez une méthode de débuggage qui permet de dessiner un rayon (ou une droite). Vous pouvez utiliser la méthode Debug.DrawRay (ou Debug.DrawLine). Les deux premiers arguments de la méthode DrawRay
sont renseignés à partir du rayon que vous avez créé. Vous pouvez également choisir une couleur pour votre rayon.
Attention ! Ces fonctions n’affichent le rayon que dans la fenêtre Scene, pas dans la fenêtre Game ! Vous pouvez glisser la fenêtre Game à côté de la fenêtre Scene ou changer le layout Unity vers 2 by 3 pour voir les deux fenêtres simultanément.
Caster un rayon
La méthode Physics.Raycast() permet de caster un rayon. Vous noterez qu’on a 4 définitions différentes. Pour ceux qui ne le savent pas, c’est ce qu’on appelle la surcharge d’une fonction : pour faire court, la fonction renvoie toujours la même chose (true ou false) mais les paramètres qu’on lui passe sont différents.
Remarquez que dans la deuxième et la quatrième définition de la fonction Raycast()
, un paramètre nommé RaycastHit hitInfo
est précédé du mot-clé out
. En C#, cela signifie qu’il s’agit d’un paramètre “de sortie”. Autrement dit, la méthode peut modifier sa valeur et renvoyer des informations via ce paramètre. C’est un moyen supplémentaire de retourner des données, en complément de la valeur de retour classique (return
).
Castez votre rayon avec cette fonction et affichez, par exemple, le nom de l’objet avec lequel le rayon est entré en collision.
Application du RayCasting : Saisi d’un objet
A ce stade, vous avez un rayon qui suit les mouvements de la souris. Exploitons-le pour implémenter la saisie des objets. Pour saisir un objet on utilisera le bouton gauche de la souris. L’objet restera saisi jusqu’à ce qu’on relache le bouton gauche. ### Ajout de l’input de la souris Configurons d’abord l’action de la souris. Il va nous falloir un nouvel input : le clic souris.
- Ouvrez l’objet InputActions (Assets/StarterAssets/InputSystem/StarterAssets.InputActions) et créez une nouvelle action Grab.
- Attribuez-lui le clic gauche. Cochez KeyboardMouse pour l’attribuer à la configuration clavier + souris (ce fichier est fait pour plusieurs configurations ou Control Schemes vous pouvez les voir en cliquant sur All Control Schemes en haut à gauche de la fenêtre).
Dorénavant, lorsqu’on clique avec le bouton gauche de la souris, la méthode OnGrab
sera appelée. Cependant, on ne veut pas seulement cliquer et attraper l’objet, on veut pouvoir le maintenir dans la main : on veut non seulement faire quelque chose quand on clique, mais également quand on relâche. Pour cela, on va attribuer une interaction particulière à notre action.
Cliquez sur le petit « + » à côté d’Interactions et sélectionnez « Press ». Il existe un « Hold » qui est trompeur : ce n’est pas « tant que l’on maintient appuyé… », mais un « déclencher après avoir tenu un temps T » !
Les propriétés Interactions et Processors servent à raffiner la façon dont les commandes seront appelées. Ici, lorsqu’on précise que la Trigger Behavior est « Press and Release », cela signifie que la fonction OnGrab()
sera appelée quand on enfonce le bouton (l’équivalent de « key down » pour les développeurs qui ont l’habitude de voir ce genre de script) mais aussi lorsque le bouton est relâché (l’équivalent de « key up »). Entre ces deux moments, la fonction ne sera pas appelée (normal : il n’y a pas de changement à signaler !). Laissons la valeur du « press point » à celle entrée par défaut (à partir de quand on considère que le bouton a été enclenché/relâché).
- Testez que votre fonction OnGrab()
est bien appelée.
A ce stade :
- Vous avez crée un rayon.
- Vous arrivez à l’afficher pour débugger.
- Vous arrivez à le caster.
- Vous arrivez à récupérer les informations sur les objets touchés par ce rayon.
- Vous avez une action qui est appelée lorsque le joueur clique sur le bouton gauche de la souris ou le relache.
C’est bien beau tout ça, mais on n’a rien à saisir… Créons donc une balle (avec un Rigidbody) et ajoutons-là à la scène !
Saisir la balle
Voici une proposition d’algorithme suite à l’appel de la fonction OnGrab()
\\``OnGrab()``
Lorsque l’utilisateur appuie sur le boutton gauche :
Si (le raycast indique une collision avec un objet) alors
Saisir l’objet
Sinon ne rien faire
Sinon (i.e. l'utilisateur relache le bouton gauche)
Si un objet est saisi
Relâcher l’objet
Sinon ne rien faire
Fin si
Saisir l’objet consistera à récupérer son RigidBody et le stocker dans une variable.
Nous voulons déplacer l’objet saisi lorsque le clic est maintenu. En clair : on veut que la position de l’objet saisi soit mise à jour en suivant le regard (le rayon). Pour cela, il nous suffit de le positionner (via la fonction MovePosition par exemple) à chaque frame aux coordonnées suivantes :
.origin + (ray.direction * offset) ray
L’offset
représente la distance initiale entre le personnage et l’objet saisi.
Problème 1 : La gravité sur l’objet saisi
Vous pouvez constater un premier problème : la sphère ayant un rigidbody, les forces continuent de s’y appliquer (notamment la gravité) lorsqu’on la saisit. Il est possible de contourner ce problème en jouant sur la variable isKinematic mentionnée plus tôt. En mettant le booléen isKinematic à true
lorsqu’on saisit la sphère, la gravité ne s’y appliquera plus. Attention toutefois à remettre le booléen à false
lorsque la sphère est relâchée !
Problème 2 : Comment savoir si un objet est saisissable ou non, saisi ou non
Une des grandes problématiques lorsqu’on travaille en environnement virtuel est de rendre les interactions possibles facilement compréhensible pour l’utilisateur. On imagine bien que dans un environnement plus riche que le nôtre, certains objets seront manipulables, d’autres non. De la même manière, il est intéressant d’avoir des indicateurs nous permettant de savoir si l’on est en train de saisir un objet ou non. Nous allons ici utiliser de très simples métaphores visuelles en jouant sur les curseurs. Nous vous proposons trois types de curseurs :
Enregistrez ces trois images dans un dossier « Cursor » de votre projet. Pour pouvoir utiliser une image en tant que curseur, il faut que l’image correspondante fasse partie des assets, mais également que son type soit considéré comme un curseur de souris. Cliquez donc sur une des trois images que vous aurez importé dans vos Assets, et dans l’Inspector, modifiez la propriété Texture Type pour lui donner la valeur Cursor. N’oubliez pas le petit « Apply » en bas – si vous oubliez, un pop-up s’affichera pour vous le signaler dès que vous cliquerez autre part.
Vous pouvez désormais modifier votre script Raycasting
pour changer la texture du curseur lorsque le rayon détecte (ou pas) un objet, ou lorsqu’un objet est saisi.
- On utilisera 3 variables
Texture2D
pour y mettre les 3 curseurs :
public Texture2D cursorOff, cursorGrabbable, cursorGrabbed;
- La fonction Cursor.SetCursor permet de modifier le curseur de la souris
Vous devriez arriver à quelque chose qui ressemble à ça :
Problème 3 : Le plan est un objet, mon programme le considère donc comme saisissable
Eh oui, si vous posez la balle et que vous visez le sol, vous pouvez voir que votre curseur passe au vert… N’essayez pas de cliquer, vous récolteriez une erreur. Pour l’instant, nous détectons tous les objets possédant un Collider et le plan en fait partie. Si vous avez jeté un œil à la fin du TP2, vous devriez avoir une idée concernant la solution : et si on utilisait un tag ? En créant un nouveau tag Grabbable, et en ajoutant un test sur ce tag dans votre script, vous devriez être capable de différencier la sphère (qui sera taguée Grabbable) du plan (qui lui ne le sera pas).
L’utilisation de ce tag a un autre avantage (et non des moindres) : vous pouvez désormais ajouter autant d’objets que vous le souhaitez sur votre scène, et définir lesquels seront saisissables en leur affectant le tag. A titre d’exercice, vous pouvez placer des cubes et des cylindres sur votre scène. Appliquez le tag Grabbable aux cubes mais pas aux cylindres. Tout devrait fonctionner sans problème.
Une autre approche (et sans doute la meilleure dans ce cas) pour obtenir le même résultat consiste à exploiter le paramètre layerMask de la méthode Physics.Raycast. Ce paramètre permet de filtrer les objets détectés, en s’assurant que le rayon ne touche que ceux appartenant aux couches spécifiées dans le masque.
Cela fonctionne de manière similaire aux tags, avec la possibilité d’ajouter des masques personnalisés. D’ailleurs, il existe un layer nommé “Ignore Raycast”, qui empêche un objet d’être détecté par un raycast.
Enfin, gardez à l’esprit que les Layers sont des bitmasks, ce qui signifie qu’ils peuvent être combinés pour affiner encore davantage la détection !
Ajoutez un layer “Grabbable” et appliquez-le à chaque sphère (ou à tout autre objet que vous souhaitez pouvoir attraper).
image Dans votre script, déclarez une variable pour stocker le mask :
private LayerMask mask;
Dans la fonction
Start()
, récupérez le mask correspondant := LayerMask.GetMask("Grabbable"); mask
Enfin, modifiez vos appels à
Physics.Raycast
pour utiliser ce mask :
Pour aller plus loin
GRABGRABGRAB
Amusez-vous à prendre et lâcher plusieurs fois la balle… Elle ne vous paraît pas de plus en plus grosse ? C’est le cas : elle se rapproche de vous quand vous l’attrapez ! En effet, si vous avez regardé ce que contient le RaycastHit pour calculer la distance initiale entre l’objet et la caméra, vous avez sûrement utilisé hit.distance… Cela calcule effectivement la distance entre le départ du rayon et le point de collision du collider, donc en surface ! Or, lorsqu’on déplace la balle, c’est par rapport au centre de celle-ci, et vous avez peut-être oublié (comme moi au début) de prendre cela en compte… C’est ce genre de petit détail qui vous donnera souvent le plus de fil à retordre dans vos projets, donc pensez toujours à tester, retester et tester encore !
Pour régler ça, regardez du côté de Vector3.Distance() pour calculer la vraie valeur de l’offset
AddTorque
Une méthode pour déplacer la balle que nous n’avons pas employée est d’utiliser la fonction AddTorque, qui permet de prendre en compte la spécificité des rotations pour des objets sphériques. En gros, AddForce()
fait bien le taf, AddTorque()
pour une sphère, le fait mieux ! 😉
Figure : Crédits : http://craig.backfire.ca/pages/autos/horsepower
Terrain
Depuis le début, nous utilisons de simples plans comme support de nos scènes. Vous pouvez un outil beaucoup plus évolué en utilisant un terrain plutôt qu’un plan GameObject > 3D Object > Terrain. Dans l’Inspector, vous aurez accès à de nombreuses propriétés vous permettant d’ajouter du relief, des arbres et bien plus encore. Pour en savoir davantage, c’est ici que ça se passe. Vous pouvez également modifier le “ciel” en jouant sur la skybox.
Solution du TP
- Téléchargez et importez le package suivant : Package TP03