# TP2 : Gestion de la physique et fonctions de mise à jour Florian Jeanne, Rémy Frenoy, Yann Soullard, Baptiste Wojtkowski (Maj 2020 & 2021), Yohan Bouvet (Maj 2022 & 2023), Azzeddine Benabbou (2024) ## Retour sur le TP 1 Au cours du TP1, vous avez appris à créer un _GameObject_ simple comme une sphère et à la déplacer à l’aide de la méthode AddForce(). Dans ce deuxième TD, nous allons étudier comment fonctionne la physique dans Unity, et notamment les collisions entre objets. ## Update – FixedUpdate – LateUpdate Si vous reprenez votre code, dans le script BallController, les AddForce() étaient appelées dans la méthode KeyboardMovements(), qui était elle-même appelée dans la méthode Update(). Cependant, ce n’est pas tout à faire correct. Pourquoi ? La réponse dans la suite du texte. Une _frame_ est une image projetée à un instant donné et pour une certaine durée. Au cinéma par exemple, les films sont projetés avec une fréquence de 24 images par secondes, ou 24 _frames per second_ en Anglais. Dans un projet Unity, une frame est donc une capture de votre scène à un instant donné. **Update()** est appelée juste avant le rendu d’une _frame_. Aussi, si le temps de calcul pour une _frame_ est plus long que celui de la _frame_ suivante, l’intervalle de temps entre les appels de la méthode Update() sera différent. **FixedUpdate()** quand à elle, est appelée à intervalles de temps réguliers (toutes les 0.02 seconde soit à 50Hz), indépendamment du rendu de la _frame_. Cette méthode est appelée juste avant les calculs liés à la physique. Vous voyez où je veux en venir ? En effet, c’est bien dans cette méthode qu’il va falloir appeler nos méthodes agissant sur la physique des GameObjects comme AddForce(). Reprenez maintenant votre script ``BallController``, et appelez ``rb.AddForce(moveValue.x, 0, moveValue.y);`` dans ``FixedUpdate()`` au lieu de ``Update()``. ```csharp void Update() { } void FixedUpdate() { rb.AddForce(moveValue.x, 0, moveValue.y); } ``` La question se pose également pour la gestion de notre caméra. En effet, on modifie la position de la caméra à chaque frame en fonction de la position de notre balle. Il existe une méthode pour cela, LateUpdate(). **LateUpdate()** est utilisée pour des caméras de suivi, l’animation procédurale, ou lorsque l’on souhaite connaître le(s) dernier(s) état(s) d’un objet. Elle est également appelée à chaque _frame_, une fois que tous les éléments présents dans Update() ont été traités. Ainsi, dans notre situation, on est sûr que la balle a bien été déplacée avant d’utiliser sa position actuelle pour modifier celle de la caméra. Ainsi, si nous récapitulons tout ça : - Start() est appelée une seule fois à la première exécution du script qui le contient ; - FixedUpdate() est appelée à intervalles réguliers avant le moindre calcul physique ; - Update() est appelée avant le rendu de la _frame_ ; - LateUpdate() est appelée à la suite de Update(). Bien entendu, il existe beaucoup plus de méthodes événementielles dans Unity dont l’exécution est prédéterminée. Pour votre information : [Execution Order of Event Functions](http://docs.unity3d.com/Manual/ExecutionOrder.html). ## Prefabs Ouvrez la scène de votre projet du TP1 en important le package que vous aviez créé. Nous allons créer des murs pour empêcher notre balle de tomber. Pour cela, créez un cube à partir des _GameObject_ Unity (**GameObject > 3D Object > Cube**). Modifiez les dimensions et la position du cube pour qu’il agisse comme un mur sur l’un des bords du terrain “ground”. ![image](https://hackmd.io/_uploads/r1uvB7dt6.png) Il nous reste donc trois murs à faire. On pourrait tout simplement refaire la même manipulation trois fois de suite. Ou alors… On pourrait utiliser ce que l’on appelle un [_prefab_](http://docs.unity3d.com/Manual/Prefabs.html). Un prefab est un objet Unity qui conserve un _GameObject_ quelconque, avec ses propriétés et ses composants. Il agit comme un _template_ (ou modèle) à partir duquel vous allez pouvoir créer différentes instances dans la scène. Ainsi, si vous modifiez par la suite votre prefab en changeant la valeur de l’une de ses propriétés, cela se répercutera sur toutes les instances présentes dans le projet. Il existe plusieurs manière de créer votre prefab. Un simple glisser/déposer depuis votre _Hierarchy_ vers la fenêtre du projet créera automatiquement un prefab, ou alors en effectuant un clic droit dans le projet, _Create ->_ Prefab. Dans ce cas, votre prefab sera vide. Il vous faudra effectuer un glisser/déposer de votre _Hierarchy_ vers le prefab. Créez donc un prefab de votre mur et générez trois instances de celui-ci par un glisser/déposer (encore et toujours) vers votre scène ou votre _Hierarchy_. Enfin, disposez les nouveaux murs tout autour du terrain afin que notre balle ne puisse plus tomber. Vous pouvez également ajouter une texture sur votre prefab de mur, et elle s’appliquera automatiquement à toutes les instances présentes, comme ceci : ![image](https://hackmd.io/_uploads/rJgYFm_FT.png) Vous pouvez utiliser une texture de briques de la bibliothèque de [textures Pixar](https://renderman.pixar.com/pixar-one-twenty-eight) sous CC-BY 4, n’hésitez pas à y faire un tour pour vos projets ![image](https://hackmd.io/_uploads/SkhKc1vtT.png) Wall (1) ? Wall (2) ? Mais qu’est-ce que… ? Il va falloir réorganiser tout ça ! Cependant, il n’existe pas de “dossier” dans la _Hierarchy_ à proprement parler, on utilise donc des GameObject vide à la place. Pour cela, **GameObject > Create Empty**. ![image](https://hackmd.io/_uploads/S1Hq5JPFa.png) Attention ! Un GameObject vide possède lui aussi un component _Transform_ ! Veillez à le positionner correctement dans votre scène AVANT de lui attribuer ses GameObjects enfants. Dans notre cas, position (0, 0.5, 0) sera très bien. Si vous aviez placé vos mur avec y = 0.5 pour qu’ils reposent sur le sol, vous remarquerez que maintenant pour chacun, y = 0. **Le parent a une position absolue dans la scène, et maintenant ses enfants ont des positions relatives au parent.** ## AddForce vs Translate Jusqu’à présent, nous déplacions notre balle à l’aide de la méthode AddForce(), qui s’applique sur le _rigidbody_ de celle-ci. Cependant, il existe une autre méthode pour effectuer un déplacement. Celle-ci, au lieu d’appliquer des forces sur notre objet, modifie directement sa position en agissant sur son composant _transform_. La méthode à appeler est [Translate()](http://docs.unity3d.com/ScriptReference/Transform.Translate.html). Elle s’applique sur un objet ne possédant pas de _rigidbody_. Créez donc un nouveau _GameObject_, un cube par exemple, auquel vous attachez un nouveau script CubeController. Dans ce script, vous créez une méthode KeyboardMovements(). Elle est assez similaire à celle de BallController, sauf qu’au lieu d’appeler des AddForce(), vous appelez ici des Translate() sur votre _transform_ pour le déplacement, et éventuellement des [Rotate()](http://docs.unity3d.com/ScriptReference/Transform.Rotate.html) si vous voulez faire des rotations. Utilisez par exemple les touches « Z », « Q », « S », « D » à la place des flèches directionnelles pour ne pas interférer avec le déplacement de la balle. Finalement, n'oubliez d'ajouter une variable speed pour controler la vitesse de déplacement de votre cube Une fois la méthode terminée, sauvegardez. Pour ne pas qu’il y ait des collisions entre la balle et le cube, désactivez la balle. Pour cela, il faut la sélectionner, et dans l’_inspector_, décocher la _checkbox_ située à côté du nom du _GameObject._ ![image](https://hackmd.io/_uploads/Hk0ocJPF6.png) Désormais, votre balle n’apparaîtra plus dans la scène. Lancez donc l’application et déplacez-vous. Les différences entre les deux méthodes sont assez significatives. Avec AddForce(), vous prenez en compte la physique de Unity. Ainsi, les collisions sont gérées et la gravité est prise en compte. Avec Translate(), vous modifiez la position de l’objet à chaque _frame_. Il y a donc pas réellement de déplacement. L’objet se “téléporte” à chaque _frame_. ## Collisions Néanmoins, si vous réactivez la balle, les collisions ont bien lieu entre celle-ci et le cube. Ceci est dû à la manière dont est gérée la physique dans Unity, et notamment les collisions. :::info Pour qu’une collision ait lieu entre deux objets, il faut qu’ils aient tous les deux des composants _Collider_ et qu’au moins l’un d’entre eux possède un _Rigidbody_ ::: Ainsi, si l’on reprend notre balle et notre cube, les collisions sont bien gérées entre la balle et les autres éléments de la scène, puisqu’elle possède un _Rigidbody_ et que par défaut tous les _GameObjects_ non-vides ont un _collider_. Notre cube cependant, passe au travers des murs puisque ni celui-ci, ni les murs n’ont de _Rigidbody_. Les [_colliders_](http://docs.unity3d.com/ScriptReference/Collider.html) sont représentés en vert dans votre scène lorsque vous sélectionnez un _gameObject_. Ils peuvent avoir une forme primitive comme une [sphère](http://docs.unity3d.com/ScriptReference/SphereCollider.html), une [_box_](http://docs.unity3d.com/ScriptReference/BoxCollider.html) ou une [capsule](http://docs.unity3d.com/ScriptReference/CapsuleCollider.html). Pour des formes plus complexes il existe deux méthodes. Prenons l’exemple d’une table. Si l’on prend un _BoxCollider_ faisant la taille de la table, on ne pourra pas faire passer notre balle en dessous de celle-ci puisqu’elle entrera en collision avec son _collider_. Cependant, si l’on divise la table entre les quatre pieds et le plateau, et que l’on applique un _collider_ adéquat à chaque partie, notre balle pourra passer en dessous. La deuxième solution consiste à appliquer un [_MeshCollider_](http://docs.unity3d.com/ScriptReference/MeshCollider.html) qui viendra épouser la forme définie par le _mesh_ (objet 3D). Cependant, cette solution peut devenir coûteuse en ressources si plusieurs _GameObjects_ complexes sont dans une même scène. ![image](https://hackmd.io/_uploads/HJxGo1DF6.png) ![image](https://hackmd.io/_uploads/Bkwzi1Pt6.png) De base, Unity gère les collisions entre deux objets de manière assez simple. Mais vous pouvez déclencher des actions spécifiques que vous avez au préalable définies. Dans notre cas, par exemple, le contact entre la balle et le cube de notre scène pourrait détruire le cube. Reprenons notre script de la balle, ``BallController``. Nous allons redéfinir la méthode [OnCollisonEnter()](http://docs.unity3d.com/ScriptReference/Collider.OnCollisionEnter.html) qui est appelée lorsqu’il  y a une collision : ```csharp void OnCollisionEnter(Collision collideEvent) { // instructions à réaliser lorsqu'il y a une collision } ``` Ici, le paramètre _collideEvent_ est de type [Collision](http://docs.unity3d.com/ScriptReference/Collision.html). Si vous regardez la documentation, vous verrez que l’on peut récupérer l’objet avec lequel notre _gameObject_ entre en collision (``collideEvent.gameObject``). C'est très bien car nous pouvons ensuite utiliser la méthode [Destroy()](http://docs.unity3d.com/ScriptReference/Object.Destroy.html) pour le détruire. Toutefois, nous ne souhaitons pas détruire tous les objets qui rentrent en collision avec la balle, juste le cube. Par conséquent, avant de détruire notre objet nous vérifierons qu'il s'agit bien d'un ``gameObject`` dont le nom est "Cube" (voir propriétés de la classe [GameObject](https://docs.unity3d.com/ScriptReference/GameObject.html)) Ce type de fonction est appelé automatiquement en réponse à un évènement spécial qui s’est déclenché dans votre scène (ici, une collision). Il n’y a pas besoin de l’appelée dans votre Update(). ## Triggers Les _Triggers_ ressemblent très fortement aux collisions. En effet, les _Triggers_ sont un type particulier de _Colliders_. Ils ne vont pas chercher à détecter les collisions, mais plus simplement détecter lorsqu’un _collider_ entre dans l’espace d’un autre _collider_ sans qu’il n’y ait de collision et donc de réaction physique si un _RigidBody_ est présent sur l’un des deux. Pour créer un _trigger_, il suffit de cocher la case _IsTrigger_ d’un _collider_ dans l’_inspector_ (ou de modifier la propriété _IsTrigger_ d’un _collider_ en modifiant sa valeur pour qu’elle soit égale à _true_. Comme il n’y a pas de collision dans un _Trigger_, c’est la méthode [_OnTriggerEnter()_](http://docs.unity3d.com/ScriptReference/Collider.OnTriggerEnter.html) qui est appelée lorsqu’un _collider_ pénètre dans l’espace. ```csharp void OnTriggerEnter(Collider collider) { // instructions à réaliser lorsqu'il y a une collision } ``` ## Time.DeltaTime Maintenant que nos collisions sont gérées, revenons à notre déplacement. Vous avez sûrement remarqué que si l’on augmente un peu la valeur de speed, le cube se déplace très rapidement. La raison est simple. Le cube se déplace en fonction des appels d’Update(). Du coup, si vous avez un ``speed`` égal à 2, le cube se déplace à raison de 2m par frame. Ce qui est assez rapide sachant que la durée d’une _frame_ est en moyenne d’environ 17ms (16,666666666.. ms pour les plus puristes d’entre vous, et pour 60fps). Pour connaître la durée d’une _frame_, il suffit d’afficher le paramètre [Time.deltaTime](http://docs.unity3d.com/ScriptReference/Time-deltaTime.html) dans la console grâce à la méthode [Debug.Log()](http://docs.unity3d.com/ScriptReference/Debug.Log.html) en l’appelant dans Update(). Si vous l’appelez dans FixedUpdate(), la valeur sera toujours égale à 20ms, puisqu’elle est appelée indépendamment du rendu des _frames_. Mais quel est le rapport avec le déplacement du cube ? Sachant que le cube se déplace en mètres par frame, et que l’on connaît la durée d’une _frame_, si l’on multiplie notre variable dans les translate() et les rotate() par cette durée ``Time.deltaTime``, alors le déplacement s’effectuera en mètres par seconde. Je m’explique. Par exemple, si tSpeed = 2, en le multipliant par Time.deltaTime = 0.017, avec l’appel de la méthode Translate() le cube se déplacera de : 2 * 0.017 = 0.034 mètres par frame Ainsi, au bout d’une seconde (ou 1000ms) on aura 1 / 0.017 = 58.8 soit 59 appels d’Update(). Du coup, votre cube se sera déplacé de 0.034 * 58.8 = 1.9992 soit 2 mètres par  seconde (soit 7.2 km/h) . La multiplication par Time.deltaTime nous permet donc bien de déterminer la vitesse en mètre par seconde. Aussi, visuellement les déplacements seront plus fluides car constants. ## Un tag « Target », ça en jette vachement plus On souhaite désormais ajouter des cibles sur la scène que l’on devra “manger” avec notre boule. Commençons par ajouter un cube sur la scène. Puisqu’il a un rigidbody, et que le script de la boule détruit les objets nommés “Cube”, notre cube disparaît bien lors de la collision. Il ne peut cependant y avoir qu’un objet nommé “Cube”, alors que l’on souhaite avoir de nombreuses cibles à détruire. Plutôt que de détruire les objets nommés “Cube”, nous allons créer un [tag](http://docs.unity3d.com/Manual/Tags.html) “Target”, et nous détruirons tous les objets ayant ce tag. Pour cela, sélectionnez le cube, puis sélectionnez _Add Tag_ dans la liste déroulante _Tag_. ![image](https://hackmd.io/_uploads/SkEB3yDFa.png) Figure : jout d’un nouveau tag Il faut alors créer un nouveau tag, que nous appellerons “Target”. ![image](https://hackmd.io/_uploads/Hy8WRkDK6.png) Figure : Création du tag Vous pouvez désormais retourner dans l’inspector du Cube, et lui affecter le tag nouvellement créé. Nous devons maintenant modifier le code de la balle (et plus précisément la méthode _onCollisionEnter_) afin qu’elle détruise non pas les objets nommés “Cube”, mais les objets tagés “Target”. Une petite lecture des fonctions publiques d’un [gameObject](http://docs.unity3d.com/ScriptReference/GameObject.html) devrait vous aider. Une fois la méthode modifiée, lancez votre application, le cube devrait être détruit lors d’une collision avec la balle. Nous avons désormais la possibilité de détruire n’importe quel objet disposé sur la scène, à partir du moment où il possède le tag “Target”. Plutôt cool, non ? Peut-être, mais pour l’instant nous n’avons qu’un pauvre cube à détruire … ## Instanciation automatique des cubes On souhaite avoir de nombreux cubes sur la scène, et surtout on souhaite que de nouveaux cubes apparaissent lorsque l’on en détruit (un vrai génocide de cube !). Créez un prefab “Cube” à partir de l’objet Cube, puis supprimez l’objet Cube de la scène. Créez un gameObject vide que l’on nommera “CubeFactory”. Ce manager sera chargé de construire des cubes à des positions aléatoires sur la scène, tout en ne dépassant pas une limite de cubes que nous fixerons à 10. Créez donc un script “CubeGenerator” et assignez-le à l’objet CubeFactory. Dans ce script, nous souhaitons faire les choses suivantes : - Au démarrage de l’application, le script doit récupérer les bornes du plan et la taille du prefab Cube. - A chaque mise à jour, le script doit générer un cube s’il y en a moins de 10 sur la scène. Le cube doit être placé à une position aléatoire **sur le plan de la scène**. En d'autres termes, le script doit instancier le cube avec une valeur aléatoire de x (comprise entre le min et le max du x du plan) et une valeur aléatoire du z (comprise entre le min et le max du z du plan). :::warning C'est le centre du cube qui sera positionné. Par conséquent, il faudra prendre en compte aussi la taille du cube dans le calcul du x et du z. ::: Informations utiles : - Les informations concernant les bornes peuvent être retrouvées depuis les [_bounds_](http://docs.unity3d.com/ScriptReference/Bounds.html) du [_renderer_](http://docs.unity3d.com/ScriptReference/Renderer.html). Le Renderer d’un gameObjet étant récupérable en appelant la méthode _GetComponent_ sur cet objet. ``` Renderer objectRenderer = object.GetComponent<Renderer>(); ``` - Il est possible de générer une variable aléatoire comprise entre une valeur maximale _min_ et une valeur maximale _max_ grâce à la méthode [_Range_](https://docs.unity3d.com/ScriptReference/Random.Range.html) de la classe [_Random_](https://docs.unity3d.com/ScriptReference/Random.html). - La fonction à utiliser pour créer un objet depuis un prefab est la fonction [_Instantiate_](http://docs.unity3d.com/ScriptReference/Object.Instantiate.html). Ne vous occupez pas du troisième argument de cette fonction pour le moment, et utilisez [Quaternion.identity](http://docs.unity3d.com/ScriptReference/Quaternion-identity.html)). - Dans le script *CubeGenerator*, nous utiliserons une variable _cubeCounter_ qui contiendra le nombre de cubes présents dans la scène. A chaque création de cube, on incrémentera cette variable. - Dans _CubeController_ toujours, nous créerons une méthode ``OnCubeDestroyed()`` qui décrémentera le compteur de Cube. Cette méthode publique sera appelée dans la méthode ``OnCollisionEnter()`` du _BallController_ qui est responsable de la destruction des cubes ([SendMessage](https://docs.unity3d.com/ScriptReference/GameObject.SendMessage.html) pourrait être utilisée). - Prenez l’habitude d’utiliser la console pour debugger votre application. Il peut être judicieux d’écrire un message lorsque l’on détruit un cube, ainsi que lorsque l’on en crée un nouveau. ``` Debug.Log("Création d'un cube en (" + x + ", " + y + " , " + z + ")"); ``` ![image](https://hackmd.io/_uploads/SyjQR1Pt6.png) Si tout va bien, on obtient quelque chose comme ça : <!-- ![image](https://hackmd.io/_uploads/H1qVRyvt6.png) --> Des petits cubes générés aléatoirement ## Pour aller plus loin ### Boule de démolition Le cours c’est sympathique, mais la pratique c’est mieux. Après cette longue et fastidieuse partie de cours, nous allons enfin pouvoir tester toutes ces notions. Nous allons créer une boule de démolition. Pour cela, nous allons utiliser une sphère qui sera attachée à l’aide d’une chaîne. Comment créer une chaîne ? C’est ce que nous allons voir tout de suite. Tout d’abord, supprimez le cube de votre scène, nous n’avons plus besoin. ![image](https://hackmd.io/_uploads/ByaEiywY6.png) _I came in like a wreeeeecking baaaall…_ La boule de démolition est composée d’une sphère et de plusieurs (ici quatre) cylindres ou capsules (au choix), et d’un cube qui sert de socle. Commencez par positionner une sphère légèrement au dessus du plan, elle ne doit pas le toucher. Puis, créez un cylindre (ou une capsule évidemment) que vous positionnez de façon à ce qu’il soit légèrement enfoncé dans la sphère. Vous devrez bien évidemment modifier sa taille pour qu’il soit plus petit que la sphère. Enfin, ajoutez lui un _rigidBody_. Ajoutez un composant _Fixed Joint_ (**Component > Physics > Fixed Joint**) à votre sphère. Pour fonctionner, ce composant a besoin d’un autre _rigidBody_. Faites donc glisser le cylindre sur la variable _Connected Body_ pour connecter la sphère et le cylindre. Les deux objets sont maintenant liés, mais nous devons également créer les autres maillons de la chaîne. Créez donc un second cylindre. Vous pouvez dupliquer le premier à l’aide du raccourci Ctrl + D. Cette fois-ci au lieu d’avoir un _Fixed Joint_, nous allons utiliser un _Hinge Joint_ sur le premier cylindre. Faites glisser le second cylindre sur la variable correspondante comme précédemment. Répétez ces opérations plusieurs fois pour avoir par exemple quatre cylindre liés entre eux et une chaîne conséquente. Ajoutez un cube à la fin de la chaîne, avec un _rigidBody_ et liez-le au dernier cylindre. Seulement, à cause du rigidBody, notre cube tombe à cause de son poids. Néanmoins, on peut figer sa position et ses rotations dans les contraintes du _rigidBody_. Cochez donc toutes les cases. Les _Fixed Joint_ créent des liaisons fortes entre les objets (de la même manière que la hiérarchie). Les _Hinge Joint_ permettent de créer des liaisons pivot (un seul degré de liberté). Pour plus de réalisme, vous pouvez par exemple faire un _Hinge Joint_ sur X pour le cylindre 2, sur Y pour le cylindre 3, et à nouveau sur X sur le cylindre 4. | | | |---|---| |![image](https://hackmd.io/_uploads/H1K8o1DYp.png)| ![image](https://hackmd.io/_uploads/ByDdoJDF6.png) | |Pour la capsule 2 (ou cylindre)|_Pour le capsule 3 (ou cylindre)_| Votre boule de démolition est terminée. Seulement, elle ne bouge pas… On ne va pas démolir grand chose. Créez donc un nouveau script qui va nous servir à faire bouger la sphère et que l’on appellera DemolitionBallController. Cette fois-ci, on ne bougera pas la sphère manuellement. La sphère doit constamment recevoir une force pour qu’elle se déplace sans interruption. De plus, lorsqu’elle entre en collision avec notre balle, cette dernière doit retourner à sa position initiale avec une [vitesse](http://docs.unity3d.com/ScriptReference/Rigidbody-velocity.html) nulle. On pourrait par exemple, générer une force aléatoire à l’aide de la méthode [Random.Range()](http://docs.unity3d.com/ScriptReference/Random.Range.html). ### Aller encore plus loin Il existe de nombreuses petites choses à améliorer dans cette application. Lorsque la boule de démolition percute un cube, elle peut rester coincée. Lorsque la boule que l’on déplace percute un cube, elle est ralentie (Mario ne ralentie pas lorsqu’il percute un champignon !!). Voici des pistes d’amélioration : - Ne pas gérer la destruction des cubes par des collisions mais par des triggers. - Déléguer cette gestion aux scripts des cubes, pas aux scripts de la boule ni de la boule de démolition. - Ajouter un tag “Friend” à la boule de démolition (elle protège les cubes, elle est leur amie) et “Ennemy” à la boule que l’on déplace (elle veut détruire les cubes, elle est méchante). Dans les scripts des cubes qui gèrent désormais les interactions, vous pouvez différencier le tag “Friend” qui ne va pas entraîner la destruction du cube, contrairement au tag “Ennemy”, qui lui détruira bien le cube. ## Solution du TP - Téléchargez et importez le package suivant : [Package TP02](http://azed.in/courses/virtual_reality/tp/solutions/TP02.unitypackage)