Tangram VR

Tangram VR

"Tangram VR" est un jeu de dans lequel le joueur doit placer des tetrominos sur des grilles afin de reproduire la forme qui lui est donnée.

Genre: VR / Puzzle
Moteur de jeu: Unity 6
Taille d'équipe: 5
Durée: 3 février 2025 - 7 février 2025
Plateforme: PC
Dans le cadre d'une semaine thématique dédiée à la découverte de la VR sur Unity 6, nous devions concevoir un prototype de jeu de type puzzle game.
Contexte :
Ce mini projet a été réalisé lors d'une semaine thématique de mon école destinée à nous présenter et nous initier au game design et à la programmation VR. Il s'agit de ma première expérience dans la réalisation d'un jeu en VR que j'ai eue.
Cette semaine c'est divisé en deux parties, la première le lundi et mardi où nous nous sommes concentrés sur le game design, puis le mercredi au vendredi soir pour la programmation et le rendu vendredi soir.
Rôle :
Au sein de mon groupe, j'ai occupé le poste de programmeur et j'ai participé à la première phase de game design, à savoir la proposition du projet et sur quelle idée de jeu nous allions partir. Parmi les propositions de toute l'équipe nous sommes finalement partis sur celle que j'avais proposée.
Phase d'itération Game Design :
L'idée s'inspirait du puzzle du tangram et de la série des jeux "IQ" de la société Smart Games. Dans ma proposition, le joueur évoluait entre 3 étages où il devait replacer, en bougeant et en tournant, les blocs présents à chaque étage, sans pouvoir les changer d'étage, afin d'obtenir une forme définie par un pattern donné.
Finalement, après avoir retravaillé cette idée et rendu le tout plus intuitif, il a été décidé de placer les "étages" au niveau du joueur et face à lui afin d'utiliser au mieux la dimension VR et de lui offrir une meilleure lisibilité.
Les trois grilles se décomposent en deux parties, deux d'entre elles servent à placer les Tetrominos (la grille AVANT et ARRIERE), la troisième grille affiche un résultat (grille FUSION) en superposant les deux autres.
Ainsi, le pattern que le joueur doit trouver est composé de trois couleurs : bleu, pour la grille AVANT , rouge pour la grille ARRIERE et violet pour la combinaison, la fusion, des deux.
Schéma prototype
Schéma prototype
Schéma du concept final, sans la superposition des couleurs
Schéma du concept final, sans la superposition des couleurs
Prise en main du plugin :
Pour ce projet, l'un des plus gros défis que j'ai rencontrés fut la prise en main et l'apprentissage des outils VR. Pour faciliter cette tâche, nous avons utilisé les packages XR interaction Toolkit et XR plugin management qui disposent de composants déjà opérationnels pour le contrôle d'un avatar en VR et de diverses interactions avec les objets.
Parmi celles-ci, j'ai utilisé le XR Grab Interactable, permettant de rendre les objets manipulables par le joueur et le XR Socket Interactor, permettant à l'objet sur lequel il est placé de devenir un réceptacle recevant des objets un socket.
Génération des grilles :

Afin de pouvoir moduler la taille de nos grilles pour prévoir d'autres puzzles de taille différente, j'ai fait le choix de générer dynamiquement nos grilles.
En donnant le nombre de cellules par colonne et pas ligne, un script va instancier une prefab de cellule portant un
XR Socket Interactor pour obtenir la taille voulue. Je renseigne également la couche de la grille, s'il s'agit de celle avant, arrière ou celle de la fusion.
Gestion des tétrominos
Un des problèmes majeurs que j'ai pu rencontrer était dû au placement de nos objets et du répétable qui devait les récupérer.
Par défaut le XR Socket fonctionne ainsi, il place l'objet qu'il détecte avec une box de détection sur lui même dans un sens précis, ce qui dans mon cas pose deux problèmes :
- Le sens de rotation des tétrominos n'est pas bon quand ils sont placés, de même que la preview de l'objet et la position qu'il aura une fois placé dont je parlerai plus tard.
- Dès qu'un socket détecte un objet, il va vouloir le récupérer, les tétrominos prenant la place de plusieurs cellules et donc plusieurs sockets, ils se déplacent de cellule en cellule qui les récupèrent en chaîne.
Le souci est que je ne peux pas directement régler ces problèmes en modifiant le code du XR Socket car celui-ci ou en changeant la rotation ou la position de mes tétrominos car le script est toujours réécrit par le plugin. En revanche, il dispose de plusieurs événements unity dont je peux me servir pour override ce qu'il fait.
Placement et changement de couleur
Dans un premier temps j'ai géré la rotation du placement en utilisant un event du XR Socket qui se lance au moment où il récupère un objet, comme je ne peux pas impacter mon objet directement, je vais changer la rotation de l'objet parent dans lequel il est stocké.
À ce moment, je stocke la rotation de mon objet a sous forme d'angle en Euler, puis puis j'arrondis chaque axe de rotation à l’angle multiple de 90° le plus proche pour m'assurer que l'objet est bien aligné aux cellules et soit "clipé" à la grille. Ensuite, j'applique cet angle à l'objet du socket qui sert de point d'attache du socket, là où mon objet est placé en enfant.
Dans cette fonction je gère également la couleur du tétromino en récupérant le numéro du layer de la grille où se trouve la cellule sur laquelle je place mon tétromino. Je donne ensuite ce numéro à une fonction d'un script TetroColor placé sur chacun de mes objets qui va appliquer une couleur.
Gestion des sockets
Pour empêcher les sockets de récupérer en chaîne mon objet, j'ai décidé de désactiver les sockets "parasites" lors du placement de mon objet.
Pour cela, je fais en sorte que seul un bloc du tétrominos soit détecté comme
bloc dit principal avec un tag. La taille des boîtes de détection des sockets étant trop petite pour que deux sockets détectent un même bloc, si une cellule détecte un bloc qui n'est pas le principal, elle se désactive.
Cela permet également au joueur de parfaitement placer le tétromino où il le souhaitait.
Pour se faire j'ai dans un premier temps demander à mon groupe de séparer les tetrominos en bloc distinct afin qu'ils soient détecté séparément, un bloc a ensuite été définis comme principale sur chacun.
Ensuite, chaque cellule tire un raycast (en bleu) qui va désactiver son socket si elle détecte un objet qui n'est pas le bloc principale, dans le cas contraire, le socket est activé.​​​​​​
Raycast (bleu) de détection
Raycast (bleu) de détection
Tétrominos et leur bloc Principal
Tétrominos et leur bloc Principal
Raycast (rose) qui change la valeur de la cellule
Raycast (rose) qui change la valeur de la cellule
Script qui compare le dictionnaire de combinaison avec celui de résultat
Script qui compare le dictionnaire de combinaison avec celui de résultat
Grille de fusion et victoire
À ce stade, mes tétrominos se placent bien sur les grilles, il me reste à gérer la combinaison des deux grilles afin de gérer le résultat que le joueur va donner.
Pour cela je gère trois dictionnaires, chacun ayant pour clé une coordonnée de la grille et un chiffre en valeur : 0 quand il est vide, 1 quand la cellule de la grille AVANT est remplie, 2 quand c'est celle de la grille ARRIERE.
Les trois dictionnaires sont créés au lancement du jeu, en prenant la taille que font les grilles et en instanciant une clé de type Vector2(x, y) et la valeur 0 par défaut. De même, une variable de coordonnées est ajoutée à chaque cellule lors de la création des grilles.
Pour remplir le dictionnaire de AVANT et ARRIERE, je le fais dans le même script que le contrôle d'activation ou de désactivation des sockets, un raycast (en rose) est tiré de chaque cellule, détecte si c'est un tétromino et si c'est le cas, change la valeur du dictionnaire de la grille associée, à la coordonnée, la clé correspondante.
Pour la grille de fusion, à chaque fois qu'un tétromino est posé ou retiré d'une cellule, une fonction va se lancer pour additionner les valeurs de chaque coordonnée. Puis, chaque cellule de la grille de combinaison prend une couleur en fonction de la valeur correspondant à  sa coordonnée :
0 rien, 1 bleu, 2, rouge et 3 violet.
Ensuite, une fonction va comparer les valeurs du dictionnaire de combinaison avec celui du pattern que l'on a préalablement rempli avec la réponse attendue.
Plutôt que de faire une comparaison de toutes les coordonnées à chaque fois, je vérifie si je trouve une coordonnée qui n'est pas égale, à ce moment-là, je stoppe ma fonction de comparaison. Dans le cas contraire, si aucune coordonnée différente n'est trouvée, alors elles sont identiques et le joueur a gagné.
Un affichage pour la victoire n'a malheureusement pas eu le temps d'être implémenté.

Détection des cellules et preview de la position

Meilleure lisibilité
Afin de donner au joueur plus de lisibilité sur ce ses interactions, nous j'ai choisi d'ajouter deux éléments visuels : une preview de la position et rotation du tetromino une fois sur la grille, et une surbrillance des tetrominos pour bien visualiser leur forme quand ils sont positionnés sur une grille, pour savoir ce que l'on va déplacer.
Preview
De la même manière que la rotation des tétrominos, le XR Socket affiche une preview de l'objet mais uniquement quand il entre dans la zone de détection, il ne tourne pas, et accéder à sa variable pour changer sa rotation ne m'a pas paru faisable dans le temps que j'avais. J'ai alors décidé de la faire moi même en reprenant une partie du code que j'avais déjà fait pour la rotation du socket.
Pour ce faire j'ai un scriptable objet où j'ai associé mes tétrominos avec une version d'eux en "preview" et un script GeneratePreview dans ma cellule.
Dans un événement Hover du socket des cellules je lance une fonction qui va récupérer l'objet à afficher en fonction du tétromino qui entre dans la zone de détection. Il va l'instancier à la position du socket et lui appliquer la même logique de rotation que pour le placement du tétromino dans le socket en récupérant son angle de rotation.
​​​​​​​Surbrillance
Au départ j'ai voulu utiliser la surbrillance qui étaitfournie sur les objets avec lesquelson pouvait interagir avec le XR interaction. Cependant, je me suis vite aperçu que celui-ci était trop complexe car il dépend de plusieurs scriptsinterdépendants entre eux.
J'ai donc opté pour un choix plus simple, en utilisant un shader qui applique une couleur en émissive en plus de la couleur de base appliquée sur le tétrominos. Ainsi, par défaut, cette émissive a son alpha à 0 et je le mets à 0,5 quand le joueur vise le tétromino.
Tool d'édition des patterns
J'ai réalisé un tool permettant de créer un scriptable objet d'un pattern pour un puzzle, en renseignant sa taille et l'image du pattern qui doit être affichée au joueur.

Dans la même fenêtre on peut modifier les patterns existants et ainsi renseigner la valeur que chaque cellule doit avoir.
Custom Editor
J'ai également créé une prefab afin d'afficher le pattern à réaliser, le script ResultCheckerController est placé sur cette prefab et on lui ajoute la tâche de charger le dictionnaire de Solution.
Un inspecteur custom est mis en place afin d'afficher une liste des différentes datas, les scriptables objets, de patterns que l'on a créés. L'image du pattern à réaliser est ensuite transformée en matériau et appliquée sur le Mesh Render qu'on lui a associé.

Voir aussi

Back to Top